luci-base: form.js: allow to disable descriptions row in TableSection
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / form.js
1 'use strict';
2 'require ui';
3 'require uci';
4 'require rpc';
5 'require dom';
6 'require baseclass';
7
8 var scope = this;
9
10 var callSessionAccess = rpc.declare({
11         object: 'session',
12         method: 'access',
13         params: [ 'scope', 'object', 'function' ],
14         expect: { 'access': false }
15 });
16
17 var CBIJSONConfig = baseclass.extend({
18         __init__: function(data) {
19                 data = Object.assign({}, data);
20
21                 this.data = {};
22
23                 var num_sections = 0,
24                     section_ids = [];
25
26                 for (var sectiontype in data) {
27                         if (!data.hasOwnProperty(sectiontype))
28                                 continue;
29
30                         if (L.isObject(data[sectiontype])) {
31                                 this.data[sectiontype] = Object.assign(data[sectiontype], {
32                                         '.anonymous': false,
33                                         '.name': sectiontype,
34                                         '.type': sectiontype
35                                 });
36
37                                 section_ids.push(sectiontype);
38                                 num_sections++;
39                         }
40                         else if (Array.isArray(data[sectiontype])) {
41                                 for (var i = 0, index = 0; i < data[sectiontype].length; i++) {
42                                         var item = data[sectiontype][i],
43                                             anonymous, name;
44
45                                         if (!L.isObject(item))
46                                                 continue;
47
48                                         if (typeof(item['.name']) == 'string') {
49                                                 name = item['.name'];
50                                                 anonymous = false;
51                                         }
52                                         else {
53                                                 name = sectiontype + num_sections;
54                                                 anonymous = true;
55                                         }
56
57                                         if (!this.data.hasOwnProperty(name))
58                                                 section_ids.push(name);
59
60                                         this.data[name] = Object.assign(item, {
61                                                 '.index': num_sections++,
62                                                 '.anonymous': anonymous,
63                                                 '.name': name,
64                                                 '.type': sectiontype
65                                         });
66                                 }
67                         }
68                 }
69
70                 section_ids.sort(L.bind(function(a, b) {
71                         var indexA = (this.data[a]['.index'] != null) ? +this.data[a]['.index'] : 9999,
72                             indexB = (this.data[b]['.index'] != null) ? +this.data[b]['.index'] : 9999;
73
74                         if (indexA != indexB)
75                                 return (indexA - indexB);
76
77                         return (a > b);
78                 }, this));
79
80                 for (var i = 0; i < section_ids.length; i++)
81                         this.data[section_ids[i]]['.index'] = i;
82         },
83
84         load: function() {
85                 return Promise.resolve(this.data);
86         },
87
88         save: function() {
89                 return Promise.resolve();
90         },
91
92         get: function(config, section, option) {
93                 if (section == null)
94                         return null;
95
96                 if (option == null)
97                         return this.data[section];
98
99                 if (!this.data.hasOwnProperty(section))
100                         return null;
101
102                 var value = this.data[section][option];
103
104                 if (Array.isArray(value))
105                         return value;
106
107                 if (value != null)
108                         return String(value);
109
110                 return null;
111         },
112
113         set: function(config, section, option, value) {
114                 if (section == null || option == null || option.charAt(0) == '.')
115                         return;
116
117                 if (!this.data.hasOwnProperty(section))
118                         return;
119
120                 if (value == null)
121                         delete this.data[section][option];
122                 else if (Array.isArray(value))
123                         this.data[section][option] = value;
124                 else
125                         this.data[section][option] = String(value);
126         },
127
128         unset: function(config, section, option) {
129                 return this.set(config, section, option, null);
130         },
131
132         sections: function(config, sectiontype, callback) {
133                 var rv = [];
134
135                 for (var section_id in this.data)
136                         if (sectiontype == null || this.data[section_id]['.type'] == sectiontype)
137                                 rv.push(this.data[section_id]);
138
139                 rv.sort(function(a, b) { return a['.index'] - b['.index'] });
140
141                 if (typeof(callback) == 'function')
142                         for (var i = 0; i < rv.length; i++)
143                                 callback.call(this, rv[i], rv[i]['.name']);
144
145                 return rv;
146         },
147
148         add: function(config, sectiontype, sectionname) {
149                 var num_sections_type = 0, next_index = 0;
150
151                 for (var name in this.data) {
152                         num_sections_type += (this.data[name]['.type'] == sectiontype);
153                         next_index = Math.max(next_index, this.data[name]['.index']);
154                 }
155
156                 var section_id = sectionname || sectiontype + num_sections_type;
157
158                 if (!this.data.hasOwnProperty(section_id)) {
159                         this.data[section_id] = {
160                                 '.name': section_id,
161                                 '.type': sectiontype,
162                                 '.anonymous': (sectionname == null),
163                                 '.index': next_index + 1
164                         };
165                 }
166
167                 return section_id;
168         },
169
170         remove: function(config, section) {
171                 if (this.data.hasOwnProperty(section))
172                         delete this.data[section];
173         },
174
175         resolveSID: function(config, section_id) {
176                 return section_id;
177         },
178
179         move: function(config, section_id1, section_id2, after) {
180                 return uci.move.apply(this, [config, section_id1, section_id2, after]);
181         }
182 });
183
184 /**
185  * @class AbstractElement
186  * @memberof LuCI.form
187  * @hideconstructor
188  * @classdesc
189  *
190  * The `AbstractElement` class serves as abstract base for the different form
191  * elements implemented by `LuCI.form`. It provides the common logic for
192  * loading and rendering values, for nesting elements and for defining common
193  * properties.
194  *
195  * This class is private and not directly accessible by user code.
196  */
197 var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.prototype */ {
198         __init__: function(title, description) {
199                 this.title = title || '';
200                 this.description = description || '';
201                 this.children = [];
202         },
203
204         /**
205          * Add another form element as children to this element.
206          *
207          * @param {AbstractElement} element
208          * The form element to add.
209          */
210         append: function(obj) {
211                 this.children.push(obj);
212         },
213
214         /**
215          * Parse this elements form input.
216          *
217          * The `parse()` function recursively walks the form element tree and
218          * triggers input value reading and validation for each encountered element.
219          *
220          * Elements which are hidden due to unsatisified dependencies are skipped.
221          *
222          * @returns {Promise<void>}
223          * Returns a promise resolving once this element's value and the values of
224          * all child elements have been parsed. The returned promise is rejected
225          * if any parsed values are not meeting the validation constraints of their
226          * respective elements.
227          */
228         parse: function() {
229                 var args = arguments;
230                 this.children.forEach(function(child) {
231                         child.parse.apply(child, args);
232                 });
233         },
234
235         /**
236          * Render the form element.
237          *
238          * The `render()` function recursively walks the form element tree and
239          * renders the markup for each element, returning the assembled DOM tree.
240          *
241          * @abstract
242          * @returns {Node|Promise<Node>}
243          * May return a DOM Node or a promise resolving to a DOM node containing
244          * the form element's markup, including the markup of any child elements.
245          */
246         render: function() {
247                 L.error('InternalError', 'Not implemented');
248         },
249
250         /** @private */
251         loadChildren: function(/* ... */) {
252                 var tasks = [];
253
254                 if (Array.isArray(this.children))
255                         for (var i = 0; i < this.children.length; i++)
256                                 if (!this.children[i].disable)
257                                         tasks.push(this.children[i].load.apply(this.children[i], arguments));
258
259                 return Promise.all(tasks);
260         },
261
262         /** @private */
263         renderChildren: function(tab_name /*, ... */) {
264                 var tasks = [],
265                     index = 0;
266
267                 if (Array.isArray(this.children))
268                         for (var i = 0; i < this.children.length; i++)
269                                 if (tab_name === null || this.children[i].tab === tab_name)
270                                         if (!this.children[i].disable)
271                                                 tasks.push(this.children[i].render.apply(
272                                                         this.children[i], this.varargs(arguments, 1, index++)));
273
274                 return Promise.all(tasks);
275         },
276
277         /**
278          * Strip any HTML tags from the given input string.
279          *
280          * @param {string} input
281          * The input string to clean.
282          *
283          * @returns {string}
284          * The cleaned input string with HTML removes removed.
285          */
286         stripTags: function(s) {
287                 if (typeof(s) == 'string' && !s.match(/[<>]/))
288                         return s;
289
290                 var x = E('div', {}, s);
291                 return x.textContent || x.innerText || '';
292         },
293
294         /**
295          * Format the given named property as title string.
296          *
297          * This function looks up the given named property and formats its value
298          * suitable for use as element caption or description string. It also
299          * strips any HTML tags from the result.
300          *
301          * If the property value is a string, it is passed to `String.format()`
302          * along with any additional parameters passed to `titleFn()`.
303          *
304          * If the property value is a function, it is invoked with any additional
305          * `titleFn()` parameters as arguments and the obtained return value is
306          * converted to a string.
307          *
308          * In all other cases, `null` is returned.
309          *
310          * @param {string} property
311          * The name of the element property to use.
312          *
313          * @param {...*} fmt_args
314          * Extra values to format the title string with.
315          *
316          * @returns {string|null}
317          * The formatted title string or `null` if the property did not exist or
318          * was neither a string nor a function.
319          */
320         titleFn: function(attr /*, ... */) {
321                 var s = null;
322
323                 if (typeof(this[attr]) == 'function')
324                         s = this[attr].apply(this, this.varargs(arguments, 1));
325                 else if (typeof(this[attr]) == 'string')
326                         s = (arguments.length > 1) ? ''.format.apply(this[attr], this.varargs(arguments, 1)) : this[attr];
327
328                 if (s != null)
329                         s = this.stripTags(String(s)).trim();
330
331                 if (s == null || s == '')
332                         return null;
333
334                 return s;
335         }
336 });
337
338 /**
339  * @constructor Map
340  * @memberof LuCI.form
341  * @augments LuCI.form.AbstractElement
342  *
343  * @classdesc
344  *
345  * The `Map` class represents one complete form. A form usually maps one UCI
346  * configuraton file and is divided into multiple sections containing multiple
347  * fields each.
348  *
349  * It serves as main entry point into the `LuCI.form` for typical view code.
350  *
351  * @param {string} config
352  * The UCI configuration to map. It is automatically loaded along when the
353  * resulting map instance.
354  *
355  * @param {string} [title]
356  * The title caption of the form. A form title is usually rendered as separate
357  * headline element before the actual form contents. If omitted, the
358  * corresponding headline element will not be rendered.
359  *
360  * @param {string} [description]
361  * The description text of the form which is usually rendered as text
362  * paragraph below the form title and before the actual form conents.
363  * If omitted, the corresponding paragraph element will not be rendered.
364  */
365 var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
366         __init__: function(config /*, ... */) {
367                 this.super('__init__', this.varargs(arguments, 1));
368
369                 this.config = config;
370                 this.parsechain = [ config ];
371                 this.data = uci;
372         },
373
374         /**
375          * Toggle readonly state of the form.
376          *
377          * If set to `true`, the Map instance is marked readonly and any form
378          * option elements added to it will inherit the readonly state.
379          *
380          * If left unset, the Map will test the access permission of the primary
381          * uci configuration upon loading and mark the form readonly if no write
382          * permissions are granted.
383          *
384          * @name LuCI.form.Map.prototype#readonly
385          * @type boolean
386          */
387
388         /**
389          * Find all DOM nodes within this Map which match the given search
390          * parameters. This function is essentially a convenience wrapper around
391          * `querySelectorAll()`.
392          *
393          * This function is sensitive to the amount of arguments passed to it;
394          * if only one argument is specified, it is used as selector-expression
395          * as-is. When two arguments are passed, the first argument is treated
396          * as attribute name, the second one as attribute value to match.
397          *
398          * As an example, `map.findElements('input')` would find all `<input>`
399          * nodes while `map.findElements('type', 'text')` would find any DOM node
400          * with a `type="text"` attribute.
401          *
402          * @param {string} selector_or_attrname
403          * If invoked with only one parameter, this argument is a
404          * `querySelectorAll()` compatible selector expression. If invoked with
405          * two parameters, this argument is the attribute name to filter for.
406          *
407          * @param {string} [attrvalue]
408          * In case the function is invoked with two parameters, this argument
409          * specifies the attribute value to match.
410          *
411          * @throws {InternalError}
412          * Throws an `InternalError` if more than two function parameters are
413          * passed.
414          *
415          * @returns {NodeList}
416          * Returns a (possibly empty) DOM `NodeList` containing the found DOM nodes.
417          */
418         findElements: function(/* ... */) {
419                 var q = null;
420
421                 if (arguments.length == 1)
422                         q = arguments[0];
423                 else if (arguments.length == 2)
424                         q = '[%s="%s"]'.format(arguments[0], arguments[1]);
425                 else
426                         L.error('InternalError', 'Expecting one or two arguments to findElements()');
427
428                 return this.root.querySelectorAll(q);
429         },
430
431         /**
432          * Find the first DOM node within this Map which matches the given search
433          * parameters. This function is essentially a convenience wrapper around
434          * `findElements()` which only returns the first found node.
435          *
436          * This function is sensitive to the amount of arguments passed to it;
437          * if only one argument is specified, it is used as selector-expression
438          * as-is. When two arguments are passed, the first argument is treated
439          * as attribute name, the second one as attribute value to match.
440          *
441          * As an example, `map.findElement('input')` would find the first `<input>`
442          * node while `map.findElement('type', 'text')` would find the first DOM
443          * node with a `type="text"` attribute.
444          *
445          * @param {string} selector_or_attrname
446          * If invoked with only one parameter, this argument is a `querySelector()`
447          * compatible selector expression. If invoked with two parameters, this
448          * argument is the attribute name to filter for.
449          *
450          * @param {string} [attrvalue]
451          * In case the function is invoked with two parameters, this argument
452          * specifies the attribute value to match.
453          *
454          * @throws {InternalError}
455          * Throws an `InternalError` if more than two function parameters are
456          * passed.
457          *
458          * @returns {Node|null}
459          * Returns the first found DOM node or `null` if no element matched.
460          */
461         findElement: function(/* ... */) {
462                 var res = this.findElements.apply(this, arguments);
463                 return res.length ? res[0] : null;
464         },
465
466         /**
467          * Tie another UCI configuration to the map.
468          *
469          * By default, a map instance will only load the UCI configuration file
470          * specified in the constructor but sometimes access to values from
471          * further configuration files is required. This function allows for such
472          * use cases by registering further UCI configuration files which are
473          * needed by the map.
474          *
475          * @param {string} config
476          * The additional UCI configuration file to tie to the map. If the given
477          * config already is in the list of required files, it will be ignored.
478          */
479         chain: function(config) {
480                 if (this.parsechain.indexOf(config) == -1)
481                         this.parsechain.push(config);
482         },
483
484         /**
485          * Add a configuration section to the map.
486          *
487          * LuCI forms follow the structure of the underlying UCI configurations,
488          * means that a map, which represents a single UCI configuration, is
489          * divided into multiple sections which in turn contain an arbitrary
490          * number of options.
491          *
492          * While UCI itself only knows two kinds of sections - named and anonymous
493          * ones - the form class offers various flavors of form section elements
494          * to present configuration sections in different ways. Refer to the
495          * documentation of the different section classes for details.
496          *
497          * @param {LuCI.form.AbstractSection} sectionclass
498          * The section class to use for rendering the configuration section.
499          * Note that this value must be the class itself, not a class instance
500          * obtained from calling `new`. It must also be a class dervied from
501          * `LuCI.form.AbstractSection`.
502          *
503          * @param {...string} classargs
504          * Additional arguments which are passed as-is to the contructor of the
505          * given section class. Refer to the class specific constructor
506          * documentation for details.
507          *
508          * @returns {LuCI.form.AbstractSection}
509          * Returns the instantiated section class instance.
510          */
511         section: function(cbiClass /*, ... */) {
512                 if (!CBIAbstractSection.isSubclass(cbiClass))
513                         L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
514
515                 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
516                 this.append(obj);
517                 return obj;
518         },
519
520         /**
521          * Load the configuration covered by this map.
522          *
523          * The `load()` function first loads all referenced UCI configurations,
524          * then it recursively walks the form element tree and invokes the
525          * load function of each child element.
526          *
527          * @returns {Promise<void>}
528          * Returns a promise resolving once the entire form completed loading all
529          * data. The promise may reject with an error if any configuration failed
530          * to load or if any of the child elements load functions rejected with
531          * an error.
532          */
533         load: function() {
534                 var doCheckACL = (!(this instanceof CBIJSONMap) && this.readonly == null);
535
536                 return Promise.all([
537                         doCheckACL ? callSessionAccess('uci', this.config, 'write') : true,
538                         this.data.load(this.parsechain || [ this.config ])
539                 ]).then(L.bind(function(res) {
540                         if (res[0] === false)
541                                 this.readonly = true;
542
543                         return this.loadChildren();
544                 }, this));
545         },
546
547         /**
548          * Parse the form input values.
549          *
550          * The `parse()` function recursively walks the form element tree and
551          * triggers input value reading and validation for each child element.
552          *
553          * Elements which are hidden due to unsatisified dependencies are skipped.
554          *
555          * @returns {Promise<void>}
556          * Returns a promise resolving once the entire form completed parsing all
557          * input values. The returned promise is rejected if any parsed values are
558          * not meeting the validation constraints of their respective elements.
559          */
560         parse: function() {
561                 var tasks = [];
562
563                 if (Array.isArray(this.children))
564                         for (var i = 0; i < this.children.length; i++)
565                                 tasks.push(this.children[i].parse());
566
567                 return Promise.all(tasks);
568         },
569
570         /**
571          * Save the form input values.
572          *
573          * This function parses the current form, saves the resulting UCI changes,
574          * reloads the UCI configuration data and redraws the form elements.
575          *
576          * @param {function} [cb]
577          * An optional callback function that is invoked after the form is parsed
578          * but before the changed UCI data is saved. This is useful to perform
579          * additional data manipulation steps before saving the changes.
580          *
581          * @param {boolean} [silent=false]
582          * If set to `true`, trigger an alert message to the user in case saving
583          * the form data failes. Otherwise fail silently.
584          *
585          * @returns {Promise<void>}
586          * Returns a promise resolving once the entire save operation is complete.
587          * The returned promise is rejected if any step of the save operation
588          * failed.
589          */
590         save: function(cb, silent) {
591                 this.checkDepends();
592
593                 return this.parse()
594                         .then(cb)
595                         .then(this.data.save.bind(this.data))
596                         .then(this.load.bind(this))
597                         .catch(function(e) {
598                                 if (!silent) {
599                                         ui.showModal(_('Save error'), [
600                                                 E('p', {}, [ _('An error occurred while saving the form:') ]),
601                                                 E('p', {}, [ E('em', { 'style': 'white-space:pre' }, [ e.message ]) ]),
602                                                 E('div', { 'class': 'right' }, [
603                                                         E('button', { 'click': ui.hideModal }, [ _('Dismiss') ])
604                                                 ])
605                                         ]);
606                                 }
607
608                                 return Promise.reject(e);
609                         }).then(this.renderContents.bind(this));
610         },
611
612         /**
613          * Reset the form by re-rendering its contents. This will revert all
614          * unsaved user inputs to their initial form state.
615          *
616          * @returns {Promise<Node>}
617          * Returns a promise resolving to the toplevel form DOM node once the
618          * re-rendering is complete.
619          */
620         reset: function() {
621                 return this.renderContents();
622         },
623
624         /**
625          * Render the form markup.
626          *
627          * @returns {Promise<Node>}
628          * Returns a promise resolving to the toplevel form DOM node once the
629          * rendering is complete.
630          */
631         render: function() {
632                 return this.load().then(this.renderContents.bind(this));
633         },
634
635         /** @private */
636         renderContents: function() {
637                 var mapEl = this.root || (this.root = E('div', {
638                         'id': 'cbi-%s'.format(this.config),
639                         'class': 'cbi-map',
640                         'cbi-dependency-check': L.bind(this.checkDepends, this)
641                 }));
642
643                 dom.bindClassInstance(mapEl, this);
644
645                 return this.renderChildren(null).then(L.bind(function(nodes) {
646                         var initialRender = !mapEl.firstChild;
647
648                         dom.content(mapEl, null);
649
650                         if (this.title != null && this.title != '')
651                                 mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
652
653                         if (this.description != null && this.description != '')
654                                 mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
655
656                         if (this.tabbed)
657                                 dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
658                         else
659                                 dom.append(mapEl, nodes);
660
661                         if (!initialRender) {
662                                 mapEl.classList.remove('flash');
663
664                                 window.setTimeout(function() {
665                                         mapEl.classList.add('flash');
666                                 }, 1);
667                         }
668
669                         this.checkDepends();
670
671                         var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
672
673                         for (var i = 0; i < tabGroups.length; i++)
674                                 ui.tabs.initTabGroup(tabGroups[i].childNodes);
675
676                         return mapEl;
677                 }, this));
678         },
679
680         /**
681          * Find a form option element instance.
682          *
683          * @param {string} name_or_id
684          * The name or the full ID of the option element to look up.
685          *
686          * @param {string} [section_id]
687          * The ID of the UCI section containing the option to look up. May be
688          * omitted if a full ID is passed as first argument.
689          *
690          * @param {string} [config]
691          * The name of the UCI configuration the option instance is belonging to.
692          * Defaults to the main UCI configuration of the map if omitted.
693          *
694          * @returns {Array<LuCI.form.AbstractValue,string>|null}
695          * Returns a two-element array containing the form option instance as
696          * first item and the corresponding UCI section ID as second item.
697          * Returns `null` if the option could not be found.
698          */
699         lookupOption: function(name, section_id, config_name) {
700                 var id, elem, sid, inst;
701
702                 if (name.indexOf('.') > -1)
703                         id = 'cbid.%s'.format(name);
704                 else
705                         id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
706
707                 elem = this.findElement('data-field', id);
708                 sid  = elem ? id.split(/\./)[2] : null;
709                 inst = elem ? dom.findClassInstance(elem) : null;
710
711                 return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
712         },
713
714         /** @private */
715         checkDepends: function(ev, n) {
716                 var changed = false;
717
718                 for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
719                         if (s.checkDepends(ev, n))
720                                 changed = true;
721
722                 if (changed && (n || 0) < 10)
723                         this.checkDepends(ev, (n || 10) + 1);
724
725                 ui.tabs.updateTabs(ev, this.root);
726         },
727
728         /** @private */
729         isDependencySatisfied: function(depends, config_name, section_id) {
730                 var def = false;
731
732                 if (!Array.isArray(depends) || !depends.length)
733                         return true;
734
735                 for (var i = 0; i < depends.length; i++) {
736                         var istat = true,
737                             reverse = depends[i]['!reverse'],
738                             contains = depends[i]['!contains'];
739
740                         for (var dep in depends[i]) {
741                                 if (dep == '!reverse' || dep == '!contains') {
742                                         continue;
743                                 }
744                                 else if (dep == '!default') {
745                                         def = true;
746                                         istat = false;
747                                 }
748                                 else {
749                                         var res = this.lookupOption(dep, section_id, config_name),
750                                             val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
751
752                                         var equal = contains
753                                                 ? isContained(val, depends[i][dep])
754                                                 : isEqual(val, depends[i][dep]);
755
756                                         istat = (istat && equal);
757                                 }
758                         }
759
760                         if (istat ^ reverse)
761                                 return true;
762                 }
763
764                 return def;
765         }
766 });
767
768 /**
769  * @constructor JSONMap
770  * @memberof LuCI.form
771  * @augments LuCI.form.Map
772  *
773  * @classdesc
774  *
775  * A `JSONMap` class functions similar to [LuCI.form.Map]{@link LuCI.form.Map}
776  * but uses a multidimensional JavaScript object instead of UCI configuration
777  * as data source.
778  *
779  * @param {Object<string, Object<string, *>|Array<Object<string, *>>>} data
780  * The JavaScript object to use as data source. Internally, the object is
781  * converted into an UCI-like format. Its toplevel keys are treated like UCI
782  * section types while the object or array-of-object values are treated as
783  * section contents.
784  *
785  * @param {string} [title]
786  * The title caption of the form. A form title is usually rendered as separate
787  * headline element before the actual form contents. If omitted, the
788  * corresponding headline element will not be rendered.
789  *
790  * @param {string} [description]
791  * The description text of the form which is usually rendered as text
792  * paragraph below the form title and before the actual form conents.
793  * If omitted, the corresponding paragraph element will not be rendered.
794  */
795 var CBIJSONMap = CBIMap.extend(/** @lends LuCI.form.JSONMap.prototype */ {
796         __init__: function(data /*, ... */) {
797                 this.super('__init__', this.varargs(arguments, 1, 'json'));
798
799                 this.config = 'json';
800                 this.parsechain = [ 'json' ];
801                 this.data = new CBIJSONConfig(data);
802         }
803 });
804
805 /**
806  * @class AbstractSection
807  * @memberof LuCI.form
808  * @augments LuCI.form.AbstractElement
809  * @hideconstructor
810  * @classdesc
811  *
812  * The `AbstractSection` class serves as abstract base for the different form
813  * section styles implemented by `LuCI.form`. It provides the common logic for
814  * enumerating underlying configuration section instances, for registering
815  * form options and for handling tabs to segment child options.
816  *
817  * This class is private and not directly accessible by user code.
818  */
819 var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractSection.prototype */ {
820         __init__: function(map, sectionType /*, ... */) {
821                 this.super('__init__', this.varargs(arguments, 2));
822
823                 this.sectiontype = sectionType;
824                 this.map = map;
825                 this.config = map.config;
826
827                 this.optional = true;
828                 this.addremove = false;
829                 this.dynamic = false;
830         },
831
832         /**
833          * Access the parent option container instance.
834          *
835          * In case this section is nested within an option element container,
836          * this property will hold a reference to the parent option instance.
837          *
838          * If this section is not nested, the property is `null`.
839          *
840          * @name LuCI.form.AbstractSection.prototype#parentoption
841          * @type LuCI.form.AbstractValue
842          * @readonly
843          */
844
845         /**
846          * Enumerate the UCI section IDs covered by this form section element.
847          *
848          * @abstract
849          * @throws {InternalError}
850          * Throws an `InternalError` exception if the function is not implemented.
851          *
852          * @returns {string[]}
853          * Returns an array of UCI section IDs covered by this form element.
854          * The sections will be rendered in the same order as the returned array.
855          */
856         cfgsections: function() {
857                 L.error('InternalError', 'Not implemented');
858         },
859
860         /**
861          * Filter UCI section IDs to render.
862          *
863          * The filter function is invoked for each UCI section ID of a given type
864          * and controls whether the given UCI section is rendered or ignored by
865          * the form section element.
866          *
867          * The default implementation always returns `true`. User code or
868          * classes extending `AbstractSection` may overwrite this function with
869          * custom implementations.
870          *
871          * @abstract
872          * @param {string} section_id
873          * The UCI section ID to test.
874          *
875          * @returns {boolean}
876          * Returns `true` when the given UCI section ID should be handled and
877          * `false` when it should be ignored.
878          */
879         filter: function(section_id) {
880                 return true;
881         },
882
883         /**
884          * Load the configuration covered by this section.
885          *
886          * The `load()` function recursively walks the section element tree and
887          * invokes the load function of each child option element.
888          *
889          * @returns {Promise<void>}
890          * Returns a promise resolving once the values of all child elements have
891          * been loaded. The promise may reject with an error if any of the child
892          * elements load functions rejected with an error.
893          */
894         load: function() {
895                 var section_ids = this.cfgsections(),
896                     tasks = [];
897
898                 if (Array.isArray(this.children))
899                         for (var i = 0; i < section_ids.length; i++)
900                                 tasks.push(this.loadChildren(section_ids[i])
901                                         .then(Function.prototype.bind.call(function(section_id, set_values) {
902                                                 for (var i = 0; i < set_values.length; i++)
903                                                         this.children[i].cfgvalue(section_id, set_values[i]);
904                                         }, this, section_ids[i])));
905
906                 return Promise.all(tasks);
907         },
908
909         /**
910          * Parse this sections form input.
911          *
912          * The `parse()` function recursively walks the section element tree and
913          * triggers input value reading and validation for each encountered child
914          * option element.
915          *
916          * Options which are hidden due to unsatisified dependencies are skipped.
917          *
918          * @returns {Promise<void>}
919          * Returns a promise resolving once the values of all child elements have
920          * been parsed. The returned promise is rejected if any parsed values are
921          * not meeting the validation constraints of their respective elements.
922          */
923         parse: function() {
924                 var section_ids = this.cfgsections(),
925                     tasks = [];
926
927                 if (Array.isArray(this.children))
928                         for (var i = 0; i < section_ids.length; i++)
929                                 for (var j = 0; j < this.children.length; j++)
930                                         tasks.push(this.children[j].parse(section_ids[i]));
931
932                 return Promise.all(tasks);
933         },
934
935         /**
936          * Add an option tab to the section.
937          *
938          * The child option elements of a section may be divided into multiple
939          * tabs to provide a better overview to the user.
940          *
941          * Before options can be moved into a tab pane, the corresponding tab
942          * has to be defined first, which is done by calling this function.
943          *
944          * Note that once tabs are defined, user code must use the `taboption()`
945          * method to add options to specific tabs. Option elements added by
946          * `option()` will not be assigned to any tab and not be rendered in this
947          * case.
948          *
949          * @param {string} name
950          * The name of the tab to register. It may be freely chosen and just serves
951          * as an identifier to differentiate tabs.
952          *
953          * @param {string} title
954          * The human readable caption of the tab.
955          *
956          * @param {string} [description]
957          * An additional description text for the corresponding tab pane. It is
958          * displayed as text paragraph below the tab but before the tab pane
959          * contents. If omitted, no description will be rendered.
960          *
961          * @throws {Error}
962          * Throws an exeption if a tab with the same `name` already exists.
963          */
964         tab: function(name, title, description) {
965                 if (this.tabs && this.tabs[name])
966                         throw 'Tab already declared';
967
968                 var entry = {
969                         name: name,
970                         title: title,
971                         description: description,
972                         children: []
973                 };
974
975                 this.tabs = this.tabs || [];
976                 this.tabs.push(entry);
977                 this.tabs[name] = entry;
978
979                 this.tab_names = this.tab_names || [];
980                 this.tab_names.push(name);
981         },
982
983         /**
984          * Add a configuration option widget to the section.
985          *
986          * Note that [taboption()]{@link LuCI.form.AbstractSection#taboption}
987          * should be used instead if this form section element uses tabs.
988          *
989          * @param {LuCI.form.AbstractValue} optionclass
990          * The option class to use for rendering the configuration option. Note
991          * that this value must be the class itself, not a class instance obtained
992          * from calling `new`. It must also be a class dervied from
993          * [LuCI.form.AbstractSection]{@link LuCI.form.AbstractSection}.
994          *
995          * @param {...*} classargs
996          * Additional arguments which are passed as-is to the contructor of the
997          * given option class. Refer to the class specific constructor
998          * documentation for details.
999          *
1000          * @throws {TypeError}
1001          * Throws a `TypeError` exception in case the passed class value is not a
1002          * descendent of `AbstractValue`.
1003          *
1004          * @returns {LuCI.form.AbstractValue}
1005          * Returns the instantiated option class instance.
1006          */
1007         option: function(cbiClass /*, ... */) {
1008                 if (!CBIAbstractValue.isSubclass(cbiClass))
1009                         throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
1010
1011                 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
1012                 this.append(obj);
1013                 return obj;
1014         },
1015
1016         /**
1017          * Add a configuration option widget to a tab of the section.
1018          *
1019          * @param {string} tabname
1020          * The name of the section tab to add the option element to.
1021          *
1022          * @param {LuCI.form.AbstractValue} optionclass
1023          * The option class to use for rendering the configuration option. Note
1024          * that this value must be the class itself, not a class instance obtained
1025          * from calling `new`. It must also be a class dervied from
1026          * [LuCI.form.AbstractSection]{@link LuCI.form.AbstractSection}.
1027          *
1028          * @param {...*} classargs
1029          * Additional arguments which are passed as-is to the contructor of the
1030          * given option class. Refer to the class specific constructor
1031          * documentation for details.
1032          *
1033          * @throws {ReferenceError}
1034          * Throws a `ReferenceError` exception when the given tab name does not
1035          * exist.
1036          *
1037          * @throws {TypeError}
1038          * Throws a `TypeError` exception in case the passed class value is not a
1039          * descendent of `AbstractValue`.
1040          *
1041          * @returns {LuCI.form.AbstractValue}
1042          * Returns the instantiated option class instance.
1043          */
1044         taboption: function(tabName /*, ... */) {
1045                 if (!this.tabs || !this.tabs[tabName])
1046                         throw L.error('ReferenceError', 'Associated tab not declared');
1047
1048                 var obj = this.option.apply(this, this.varargs(arguments, 1));
1049                 obj.tab = tabName;
1050                 this.tabs[tabName].children.push(obj);
1051                 return obj;
1052         },
1053
1054         /** @private */
1055         renderUCISection: function(section_id) {
1056                 var renderTasks = [];
1057
1058                 if (!this.tabs)
1059                         return this.renderOptions(null, section_id);
1060
1061                 for (var i = 0; i < this.tab_names.length; i++)
1062                         renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
1063
1064                 return Promise.all(renderTasks)
1065                         .then(this.renderTabContainers.bind(this, section_id));
1066         },
1067
1068         /** @private */
1069         renderTabContainers: function(section_id, nodes) {
1070                 var config_name = this.uciconfig || this.map.config,
1071                     containerEls = E([]);
1072
1073                 for (var i = 0; i < nodes.length; i++) {
1074                         var tab_name = this.tab_names[i],
1075                             tab_data = this.tabs[tab_name],
1076                             containerEl = E('div', {
1077                                 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
1078                                 'data-tab': tab_name,
1079                                 'data-tab-title': tab_data.title,
1080                                 'data-tab-active': tab_name === this.selected_tab
1081                             });
1082
1083                         if (tab_data.description != null && tab_data.description != '')
1084                                 containerEl.appendChild(
1085                                         E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
1086
1087                         containerEl.appendChild(nodes[i]);
1088                         containerEls.appendChild(containerEl);
1089                 }
1090
1091                 return containerEls;
1092         },
1093
1094         /** @private */
1095         renderOptions: function(tab_name, section_id) {
1096                 var in_table = (this instanceof CBITableSection);
1097                 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
1098                         var optionEls = E([]);
1099                         for (var i = 0; i < nodes.length; i++)
1100                                 optionEls.appendChild(nodes[i]);
1101                         return optionEls;
1102                 });
1103         },
1104
1105         /** @private */
1106         checkDepends: function(ev, n) {
1107                 var changed = false,
1108                     sids = this.cfgsections();
1109
1110                 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
1111                         for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
1112                                 var isActive = o.isActive(sid),
1113                                     isSatisified = o.checkDepends(sid);
1114
1115                                 if (isActive != isSatisified) {
1116                                         o.setActive(sid, !isActive);
1117                                         isActive = !isActive;
1118                                         changed = true;
1119                                 }
1120
1121                                 if (!n && isActive)
1122                                         o.triggerValidation(sid);
1123                         }
1124                 }
1125
1126                 return changed;
1127         }
1128 });
1129
1130
1131 var isEqual = function(x, y) {
1132         if (x != null && y != null && typeof(x) != typeof(y))
1133                 return false;
1134
1135         if ((x == null && y != null) || (x != null && y == null))
1136                 return false;
1137
1138         if (Array.isArray(x)) {
1139                 if (x.length != y.length)
1140                         return false;
1141
1142                 for (var i = 0; i < x.length; i++)
1143                         if (!isEqual(x[i], y[i]))
1144                                 return false;
1145         }
1146         else if (typeof(x) == 'object') {
1147                 for (var k in x) {
1148                         if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
1149                                 return false;
1150
1151                         if (!isEqual(x[k], y[k]))
1152                                 return false;
1153                 }
1154
1155                 for (var k in y)
1156                         if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
1157                                 return false;
1158         }
1159         else if (x != y) {
1160                 return false;
1161         }
1162
1163         return true;
1164 };
1165
1166 var isContained = function(x, y) {
1167         if (Array.isArray(x)) {
1168                 for (var i = 0; i < x.length; i++)
1169                         if (x[i] == y)
1170                                 return true;
1171         }
1172         else if (L.isObject(x)) {
1173                 if (x.hasOwnProperty(y) && x[y] != null)
1174                         return true;
1175         }
1176         else if (typeof(x) == 'string') {
1177                 return (x.indexOf(y) > -1);
1178         }
1179
1180         return false;
1181 };
1182
1183 /**
1184  * @class AbstractValue
1185  * @memberof LuCI.form
1186  * @augments LuCI.form.AbstractElement
1187  * @hideconstructor
1188  * @classdesc
1189  *
1190  * The `AbstractValue` class serves as abstract base for the different form
1191  * option styles implemented by `LuCI.form`. It provides the common logic for
1192  * handling option input values, for dependencies among options and for
1193  * validation constraints that should be applied to entered values.
1194  *
1195  * This class is private and not directly accessible by user code.
1196  */
1197 var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractValue.prototype */ {
1198         __init__: function(map, section, option /*, ... */) {
1199                 this.super('__init__', this.varargs(arguments, 3));
1200
1201                 this.section = section;
1202                 this.option = option;
1203                 this.map = map;
1204                 this.config = map.config;
1205
1206                 this.deps = [];
1207                 this.initial = {};
1208                 this.rmempty = true;
1209                 this.default = null;
1210                 this.size = null;
1211                 this.optional = false;
1212         },
1213
1214         /**
1215          * If set to `false`, the underlying option value is retained upon saving
1216          * the form when the option element is disabled due to unsatisfied
1217          * dependency constraints.
1218          *
1219          * @name LuCI.form.AbstractValue.prototype#rmempty
1220          * @type boolean
1221          * @default true
1222          */
1223
1224         /**
1225          * If set to `true`, the underlying ui input widget is allowed to be empty,
1226          * otherwise the option element is marked invalid when no value is entered
1227          * or selected by the user.
1228          *
1229          * @name LuCI.form.AbstractValue.prototype#optional
1230          * @type boolean
1231          * @default false
1232          */
1233
1234         /**
1235          * Sets a default value to use when the underlying UCI option is not set.
1236          *
1237          * @name LuCI.form.AbstractValue.prototype#default
1238          * @type *
1239          * @default null
1240          */
1241
1242         /**
1243          * Specifies a datatype constraint expression to validate input values
1244          * against. Refer to {@link LuCI.validation} for details on the format.
1245          *
1246          * If the user entered input does not match the datatype validation, the
1247          * option element is marked as invalid.
1248          *
1249          * @name LuCI.form.AbstractValue.prototype#datatype
1250          * @type string
1251          * @default null
1252          */
1253
1254         /**
1255          * Specifies a custom validation function to test the user input for
1256          * validity. The validation function must return `true` to accept the
1257          * value. Any other return value type is converted to a string and
1258          * displayed to the user as validation error message.
1259          *
1260          * If the user entered input does not pass the validation function, the
1261          * option element is marked as invalid.
1262          *
1263          * @name LuCI.form.AbstractValue.prototype#validate
1264          * @type function
1265          * @default null
1266          */
1267
1268         /**
1269          * Override the UCI configuration name to read the option value from.
1270          *
1271          * By default, the configuration name is inherited from the parent Map.
1272          * By setting this property, a deviating configuration may be specified.
1273          *
1274          * The default is null, means inheriting from the parent form.
1275          *
1276          * @name LuCI.form.AbstractValue.prototype#uciconfig
1277          * @type string
1278          * @default null
1279          */
1280
1281         /**
1282          * Override the UCI section name to read the option value from.
1283          *
1284          * By default, the section ID is inherited from the parent section element.
1285          * By setting this property, a deviating section may be specified.
1286          *
1287          * The default is null, means inheriting from the parent section.
1288          *
1289          * @name LuCI.form.AbstractValue.prototype#ucisection
1290          * @type string
1291          * @default null
1292          */
1293
1294         /**
1295          * Override the UCI option name to read the value from.
1296          *
1297          * By default, the elements name, which is passed as third argument to
1298          * the constructor, is used as UCI option name. By setting this property,
1299          * a deviating UCI option may be specified.
1300          *
1301          * The default is null, means using the option element name.
1302          *
1303          * @name LuCI.form.AbstractValue.prototype#ucioption
1304          * @type string
1305          * @default null
1306          */
1307
1308         /**
1309          * Mark grid section option element as editable.
1310          *
1311          * Options which are displayed in the table portion of a `GridSection`
1312          * instance are rendered as readonly text by default. By setting the
1313          * `editable` property of a child option element to `true`, that element
1314          * is rendered as full input widget within its cell instead of a text only
1315          * preview.
1316          *
1317          * This property has no effect on options that are not children of grid
1318          * section elements.
1319          *
1320          * @name LuCI.form.AbstractValue.prototype#editable
1321          * @type boolean
1322          * @default false
1323          */
1324
1325         /**
1326          * Move grid section option element into the table, the modal popup or both.
1327          *
1328          * If this property is `null` (the default), the option element is
1329          * displayed in both the table preview area and the per-section instance
1330          * modal popup of a grid section. When it is set to `false` the option
1331          * is only shown in the table but not the modal popup. When set to `true`,
1332          * the option is only visible in the modal popup but not the table.
1333          *
1334          * This property has no effect on options that are not children of grid
1335          * section elements.
1336          *
1337          * @name LuCI.form.AbstractValue.prototype#modalonly
1338          * @type boolean
1339          * @default null
1340          */
1341
1342         /**
1343          * Make option element readonly.
1344          *
1345          * This property defaults to the readonly state of the parent form element.
1346          * When set to `true`, the underlying widget is rendered in disabled state,
1347          * means its contents cannot be changed and the widget cannot be interacted
1348          * with.
1349          *
1350          * @name LuCI.form.AbstractValue.prototype#readonly
1351          * @type boolean
1352          * @default false
1353          */
1354
1355         /**
1356          * Override the cell width of a table or grid section child option.
1357          *
1358          * If the property is set to a numeric value, it is treated as pixel width
1359          * which is set on the containing cell element of the option, essentially
1360          * forcing a certain column width. When the property is set to a string
1361          * value, it is applied as-is to the CSS `width` property.
1362          *
1363          * This property has no effect on options that are not children of grid or
1364          * table section elements.
1365          *
1366          * @name LuCI.form.AbstractValue.prototype#width
1367          * @type number|string
1368          * @default null
1369          */
1370
1371         /**
1372          * Add a dependency contraint to the option.
1373          *
1374          * Dependency constraints allow making the presence of option elements
1375          * dependant on the current values of certain other options within the
1376          * same form. An option element with unsatisfied dependencies will be
1377          * hidden from the view and its current value is omitted when saving.
1378          *
1379          * Multiple constraints (that is, multiple calls to `depends()`) are
1380          * treated as alternatives, forming a logical "or" expression.
1381          *
1382          * By passing an object of name => value pairs as first argument, it is
1383          * possible to depend on multiple options simultaneously, allowing to form
1384          * a logical "and" expression.
1385          *
1386          * Option names may be given in "dot notation" which allows to reference
1387          * option elements outside of the current form section. If a name without
1388          * dot is specified, it refers to an option within the same configuration
1389          * section. If specified as <code>configname.sectionid.optionname</code>,
1390          * options anywhere within the same form may be specified.
1391          *
1392          * The object notation also allows for a number of special keys which are
1393          * not treated as option names but as modifiers to influence the dependency
1394          * constraint evaluation. The associated value of these special "tag" keys
1395          * is ignored. The recognized tags are:
1396          *
1397          * <ul>
1398          *   <li>
1399          *    <code>!reverse</code><br>
1400          *    Invert the dependency, instead of requiring another option to be
1401          *    equal to the dependency value, that option should <em>not</em> be
1402          *    equal.
1403          *   </li>
1404          *   <li>
1405          *    <code>!contains</code><br>
1406          *    Instead of requiring an exact match, the dependency is considered
1407          *    satisfied when the dependency value is contained within the option
1408          *    value.
1409          *   </li>
1410          *   <li>
1411          *    <code>!default</code><br>
1412          *    The dependency is always satisfied
1413          *   </li>
1414          * </ul>
1415          *
1416          * Examples:
1417          *
1418          * <ul>
1419          *  <li>
1420          *   <code>opt.depends("foo", "test")</code><br>
1421          *   Require the value of `foo` to be `test`.
1422          *  </li>
1423          *  <li>
1424          *   <code>opt.depends({ foo: "test" })</code><br>
1425          *   Equivalent to the previous example.
1426          *  </li>
1427          *  <li>
1428          *   <code>opt.depends({ foo: "test", bar: "qrx" })</code><br>
1429          *   Require the value of `foo` to be `test` and the value of `bar` to be
1430          *   `qrx`.
1431          *  </li>
1432          *  <li>
1433          *   <code>opt.depends({ foo: "test" })<br>
1434          *         opt.depends({ bar: "qrx" })</code><br>
1435          *   Require either <code>foo</code> to be set to <code>test</code>,
1436          *   <em>or</em> the <code>bar</code> option to be <code>qrx</code>.
1437          *  </li>
1438          *  <li>
1439          *   <code>opt.depends("test.section1.foo", "bar")</code><br>
1440          *   Require the "foo" form option within the "section1" section to be
1441          *   set to "bar".
1442          *  </li>
1443          *  <li>
1444          *   <code>opt.depends({ foo: "test", "!contains": true })</code><br>
1445          *   Require the "foo" option value to contain the substring "test".
1446          *  </li>
1447          * </ul>
1448          *
1449          * @param {string|Object<string, string|boolean>} optionname_or_depends
1450          * The name of the option to depend on or an object describing multiple
1451          * dependencies which must be satified (a logical "and" expression).
1452          *
1453          * @param {string} optionvalue
1454          * When invoked with a plain option name as first argument, this parameter
1455          * specifies the expected value. In case an object is passed as first
1456          * argument, this parameter is ignored.
1457          */
1458         depends: function(field, value) {
1459                 var deps;
1460
1461                 if (typeof(field) === 'string')
1462                         deps = {}, deps[field] = value;
1463                 else
1464                         deps = field;
1465
1466                 this.deps.push(deps);
1467         },
1468
1469         /** @private */
1470         transformDepList: function(section_id, deplist) {
1471                 var list = deplist || this.deps,
1472                     deps = [];
1473
1474                 if (Array.isArray(list)) {
1475                         for (var i = 0; i < list.length; i++) {
1476                                 var dep = {};
1477
1478                                 for (var k in list[i]) {
1479                                         if (list[i].hasOwnProperty(k)) {
1480                                                 if (k.charAt(0) === '!')
1481                                                         dep[k] = list[i][k];
1482                                                 else if (k.indexOf('.') !== -1)
1483                                                         dep['cbid.%s'.format(k)] = list[i][k];
1484                                                 else
1485                                                         dep['cbid.%s.%s.%s'.format(
1486                                                                 this.uciconfig || this.section.uciconfig || this.map.config,
1487                                                                 this.ucisection || section_id,
1488                                                                 k
1489                                                         )] = list[i][k];
1490                                         }
1491                                 }
1492
1493                                 for (var k in dep) {
1494                                         if (dep.hasOwnProperty(k)) {
1495                                                 deps.push(dep);
1496                                                 break;
1497                                         }
1498                                 }
1499                         }
1500                 }
1501
1502                 return deps;
1503         },
1504
1505         /** @private */
1506         transformChoices: function() {
1507                 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
1508                         return null;
1509
1510                 var choices = {};
1511
1512                 for (var i = 0; i < this.keylist.length; i++)
1513                         choices[this.keylist[i]] = this.vallist[i];
1514
1515                 return choices;
1516         },
1517
1518         /** @private */
1519         checkDepends: function(section_id) {
1520                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1521                     active = this.map.isDependencySatisfied(this.deps, config_name, section_id);
1522
1523                 if (active)
1524                         this.updateDefaultValue(section_id);
1525
1526                 return active;
1527         },
1528
1529         /** @private */
1530         updateDefaultValue: function(section_id) {
1531                 if (!L.isObject(this.defaults))
1532                         return;
1533
1534                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1535                     cfgvalue = L.toArray(this.cfgvalue(section_id))[0],
1536                     default_defval = null, satisified_defval = null;
1537
1538                 for (var value in this.defaults) {
1539                         if (!this.defaults[value] || this.defaults[value].length == 0) {
1540                                 default_defval = value;
1541                                 continue;
1542                         }
1543                         else if (this.map.isDependencySatisfied(this.defaults[value], config_name, section_id)) {
1544                                 satisified_defval = value;
1545                                 break;
1546                         }
1547                 }
1548
1549                 if (satisified_defval == null)
1550                         satisified_defval = default_defval;
1551
1552                 var node = this.map.findElement('id', this.cbid(section_id));
1553                 if (node && node.getAttribute('data-changed') != 'true' && satisified_defval != null && cfgvalue == null)
1554                         dom.callClassMethod(node, 'setValue', satisified_defval);
1555
1556                 this.default = satisified_defval;
1557         },
1558
1559         /**
1560          * Obtain the internal ID ("cbid") of the element instance.
1561          *
1562          * Since each form section element may map multiple underlying
1563          * configuration sections, the configuration section ID is required to
1564          * form a fully qualified ID pointing to the specific element instance
1565          * within the given specific section.
1566          *
1567          * @param {string} section_id
1568          * The configuration section ID
1569          *
1570          * @throws {TypeError}
1571          * Throws a `TypeError` exception when no `section_id` was specified.
1572          *
1573          * @returns {string}
1574          * Returns the element ID.
1575          */
1576         cbid: function(section_id) {
1577                 if (section_id == null)
1578                         L.error('TypeError', 'Section ID required');
1579
1580                 return 'cbid.%s.%s.%s'.format(
1581                         this.uciconfig || this.section.uciconfig || this.map.config,
1582                         section_id, this.option);
1583         },
1584
1585         /**
1586          * Load the underlying configuration value.
1587          *
1588          * The default implementation of this method reads and returns the
1589          * underlying UCI option value (or the related JavaScript property for
1590          * `JSONMap` instances). It may be overwritten by user code to load data
1591          * from nonstandard sources.
1592          *
1593          * @param {string} section_id
1594          * The configuration section ID
1595          *
1596          * @throws {TypeError}
1597          * Throws a `TypeError` exception when no `section_id` was specified.
1598          *
1599          * @returns {*|Promise<*>}
1600          * Returns the configuration value to initialize the option element with.
1601          * The return value of this function is filtered through `Promise.resolve()`
1602          * so it may return promises if overridden by user code.
1603          */
1604         load: function(section_id) {
1605                 if (section_id == null)
1606                         L.error('TypeError', 'Section ID required');
1607
1608                 return this.map.data.get(
1609                         this.uciconfig || this.section.uciconfig || this.map.config,
1610                         this.ucisection || section_id,
1611                         this.ucioption || this.option);
1612         },
1613
1614         /**
1615          * Obtain the underlying `LuCI.ui` element instance.
1616          *
1617          * @param {string} section_id
1618          * The configuration section ID
1619          *
1620          * @throws {TypeError}
1621          * Throws a `TypeError` exception when no `section_id` was specified.
1622          *
1623          * @return {LuCI.ui.AbstractElement|null}
1624          * Returns the `LuCI.ui` element instance or `null` in case the form
1625          * option implementation does not use `LuCI.ui` widgets.
1626          */
1627         getUIElement: function(section_id) {
1628                 var node = this.map.findElement('id', this.cbid(section_id)),
1629                     inst = node ? dom.findClassInstance(node) : null;
1630                 return (inst instanceof ui.AbstractElement) ? inst : null;
1631         },
1632
1633         /**
1634          * Query the underlying configuration value.
1635          *
1636          * The default implementation of this method returns the cached return
1637          * value of [load()]{@link LuCI.form.AbstractValue#load}. It may be
1638          * overwritten by user code to obtain the configuration value in a
1639          * different way.
1640          *
1641          * @param {string} section_id
1642          * The configuration section ID
1643          *
1644          * @throws {TypeError}
1645          * Throws a `TypeError` exception when no `section_id` was specified.
1646          *
1647          * @returns {*}
1648          * Returns the configuration value.
1649          */
1650         cfgvalue: function(section_id, set_value) {
1651                 if (section_id == null)
1652                         L.error('TypeError', 'Section ID required');
1653
1654                 if (arguments.length == 2) {
1655                         this.data = this.data || {};
1656                         this.data[section_id] = set_value;
1657                 }
1658
1659                 return this.data ? this.data[section_id] : null;
1660         },
1661
1662         /**
1663          * Query the current form input value.
1664          *
1665          * The default implementation of this method returns the current input
1666          * value of the underlying [LuCI.ui]{@link LuCI.ui.AbstractElement} widget.
1667          * It may be overwritten by user code to handle input values differently.
1668          *
1669          * @param {string} section_id
1670          * The configuration section ID
1671          *
1672          * @throws {TypeError}
1673          * Throws a `TypeError` exception when no `section_id` was specified.
1674          *
1675          * @returns {*}
1676          * Returns the current input value.
1677          */
1678         formvalue: function(section_id) {
1679                 var elem = this.getUIElement(section_id);
1680                 return elem ? elem.getValue() : null;
1681         },
1682
1683         /**
1684          * Obtain a textual input representation.
1685          *
1686          * The default implementation of this method returns the HTML escaped
1687          * current input value of the underlying
1688          * [LuCI.ui]{@link LuCI.ui.AbstractElement} widget. User code or specific
1689          * option element implementations may overwrite this function to apply a
1690          * different logic, e.g. to return `Yes` or `No` depending on the checked
1691          * state of checkbox elements.
1692          *
1693          * @param {string} section_id
1694          * The configuration section ID
1695          *
1696          * @throws {TypeError}
1697          * Throws a `TypeError` exception when no `section_id` was specified.
1698          *
1699          * @returns {string}
1700          * Returns the text representation of the current input value.
1701          */
1702         textvalue: function(section_id) {
1703                 var cval = this.cfgvalue(section_id);
1704
1705                 if (cval == null)
1706                         cval = this.default;
1707
1708                 return (cval != null) ? '%h'.format(cval) : null;
1709         },
1710
1711         /**
1712          * Apply custom validation logic.
1713          *
1714          * This method is invoked whenever incremental validation is performed on
1715          * the user input, e.g. on keyup or blur events.
1716          *
1717          * The default implementation of this method does nothing and always
1718          * returns `true`. User code may overwrite this method to provide
1719          * additional validation logic which is not covered by data type
1720          * constraints.
1721          *
1722          * @abstract
1723          * @param {string} section_id
1724          * The configuration section ID
1725          *
1726          * @param {*} value
1727          * The value to validate
1728          *
1729          * @returns {*}
1730          * The method shall return `true` to accept the given value. Any other
1731          * return value is treated as failure, converted to a string and displayed
1732          * as error message to the user.
1733          */
1734         validate: function(section_id, value) {
1735                 return true;
1736         },
1737
1738         /**
1739          * Test whether the input value is currently valid.
1740          *
1741          * @param {string} section_id
1742          * The configuration section ID
1743          *
1744          * @returns {boolean}
1745          * Returns `true` if the input value currently is valid, otherwise it
1746          * returns `false`.
1747          */
1748         isValid: function(section_id) {
1749                 var elem = this.getUIElement(section_id);
1750                 return elem ? elem.isValid() : true;
1751         },
1752
1753         /**
1754          * Test whether the option element is currently active.
1755          *
1756          * An element is active when it is not hidden due to unsatisfied dependency
1757          * constraints.
1758          *
1759          * @param {string} section_id
1760          * The configuration section ID
1761          *
1762          * @returns {boolean}
1763          * Returns `true` if the option element currently is active, otherwise it
1764          * returns `false`.
1765          */
1766         isActive: function(section_id) {
1767                 var field = this.map.findElement('data-field', this.cbid(section_id));
1768                 return (field != null && !field.classList.contains('hidden'));
1769         },
1770
1771         /** @private */
1772         setActive: function(section_id, active) {
1773                 var field = this.map.findElement('data-field', this.cbid(section_id));
1774
1775                 if (field && field.classList.contains('hidden') == active) {
1776                         field.classList[active ? 'remove' : 'add']('hidden');
1777                         return true;
1778                 }
1779
1780                 return false;
1781         },
1782
1783         /** @private */
1784         triggerValidation: function(section_id) {
1785                 var elem = this.getUIElement(section_id);
1786                 return elem ? elem.triggerValidation() : true;
1787         },
1788
1789         /**
1790          * Parse the option element input.
1791          *
1792          * The function is invoked when the `parse()` method has been invoked on
1793          * the parent form and triggers input value reading and validation.
1794          *
1795          * @param {string} section_id
1796          * The configuration section ID
1797          *
1798          * @returns {Promise<void>}
1799          * Returns a promise resolving once the input value has been read and
1800          * validated or rejecting in case the input value does not meet the
1801          * validation constraints.
1802          */
1803         parse: function(section_id) {
1804                 var active = this.isActive(section_id),
1805                     cval = this.cfgvalue(section_id),
1806                     fval = active ? this.formvalue(section_id) : null;
1807
1808                 if (active && !this.isValid(section_id)) {
1809                         var title = this.stripTags(this.title).trim();
1810                         return Promise.reject(new TypeError(_('Option "%s" contains an invalid input value.').format(title || this.option)));
1811                 }
1812
1813                 if (fval != '' && fval != null) {
1814                         if (this.forcewrite || !isEqual(cval, fval))
1815                                 return Promise.resolve(this.write(section_id, fval));
1816                 }
1817                 else {
1818                         if (!active || this.rmempty || this.optional) {
1819                                 return Promise.resolve(this.remove(section_id));
1820                         }
1821                         else if (!isEqual(cval, fval)) {
1822                                 var title = this.stripTags(this.title).trim();
1823                                 return Promise.reject(new TypeError(_('Option "%s" must not be empty.').format(title || this.option)));
1824                         }
1825                 }
1826
1827                 return Promise.resolve();
1828         },
1829
1830         /**
1831          * Write the current input value into the configuration.
1832          *
1833          * This function is invoked upon saving the parent form when the option
1834          * element is valid and when its input value has been changed compared to
1835          * the initial value returned by
1836          * [cfgvalue()]{@link LuCI.form.AbstractValue#cfgvalue}.
1837          *
1838          * The default implementation simply sets the given input value in the
1839          * UCI configuration (or the associated JavaScript object property in
1840          * case of `JSONMap` forms). It may be overwritten by user code to
1841          * implement alternative save logic, e.g. to transform the input value
1842          * before it is written.
1843          *
1844          * @param {string} section_id
1845          * The configuration section ID
1846          *
1847          * @param {string|string[]}     formvalue
1848          * The input value to write.
1849          */
1850         write: function(section_id, formvalue) {
1851                 return this.map.data.set(
1852                         this.uciconfig || this.section.uciconfig || this.map.config,
1853                         this.ucisection || section_id,
1854                         this.ucioption || this.option,
1855                         formvalue);
1856         },
1857
1858         /**
1859          * Remove the corresponding value from the configuration.
1860          *
1861          * This function is invoked upon saving the parent form when the option
1862          * element has been hidden due to unsatisfied dependencies or when the
1863          * user cleared the input value and the option is marked optional.
1864          *
1865          * The default implementation simply removes the associated option from the
1866          * UCI configuration (or the associated JavaScript object property in
1867          * case of `JSONMap` forms). It may be overwritten by user code to
1868          * implement alternative removal logic, e.g. to retain the original value.
1869          *
1870          * @param {string} section_id
1871          * The configuration section ID
1872          */
1873         remove: function(section_id) {
1874                 return this.map.data.unset(
1875                         this.uciconfig || this.section.uciconfig || this.map.config,
1876                         this.ucisection || section_id,
1877                         this.ucioption || this.option);
1878         }
1879 });
1880
1881 /**
1882  * @class TypedSection
1883  * @memberof LuCI.form
1884  * @augments LuCI.form.AbstractSection
1885  * @hideconstructor
1886  * @classdesc
1887  *
1888  * The `TypedSection` class maps all or - if `filter()` is overwritten - a
1889  * subset of the underlying UCI configuration sections of a given type.
1890  *
1891  * Layout wise, the configuration section instances mapped by the section
1892  * element (sometimes referred to as "section nodes") are stacked beneath
1893  * each other in a single column, with an optional section remove button next
1894  * to each section node and a section add button at the end, depending on the
1895  * value of the `addremove` property.
1896  *
1897  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
1898  * The configuration form this section is added to. It is automatically passed
1899  * by [section()]{@link LuCI.form.Map#section}.
1900  *
1901  * @param {string} section_type
1902  * The type of the UCI section to map.
1903  *
1904  * @param {string} [title]
1905  * The title caption of the form section element.
1906  *
1907  * @param {string} [description]
1908  * The description text of the form section element.
1909  */
1910 var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSection.prototype */ {
1911         __name__: 'CBI.TypedSection',
1912
1913         /**
1914          * If set to `true`, the user may add or remove instances from the form
1915          * section widget, otherwise only preexisting sections may be edited.
1916          * The default is `false`.
1917          *
1918          * @name LuCI.form.TypedSection.prototype#addremove
1919          * @type boolean
1920          * @default false
1921          */
1922
1923         /**
1924          * If set to `true`, mapped section instances are treated as anonymous
1925          * UCI sections, which means that section instance elements will be
1926          * rendered without title element and that no name is required when adding
1927          * new sections. The default is `false`.
1928          *
1929          * @name LuCI.form.TypedSection.prototype#anonymous
1930          * @type boolean
1931          * @default false
1932          */
1933
1934         /**
1935          * When set to `true`, instead of rendering section instances one below
1936          * another, treat each instance as separate tab pane and render a tab menu
1937          * at the top of the form section element, allowing the user to switch
1938          * among instances. The default is `false`.
1939          *
1940          * @name LuCI.form.TypedSection.prototype#tabbed
1941          * @type boolean
1942          * @default false
1943          */
1944
1945         /**
1946          * Override the caption used for the section add button at the bottom of
1947          * the section form element. If set to a string, it will be used as-is,
1948          * if set to a function, the function will be invoked and its return value
1949          * is used as caption, after converting it to a string. If this property
1950          * is not set, the default is `Add`.
1951          *
1952          * @name LuCI.form.TypedSection.prototype#addbtntitle
1953          * @type string|function
1954          * @default null
1955          */
1956
1957         /**
1958          * Override the UCI configuration name to read the section IDs from. By
1959          * default, the configuration name is inherited from the parent `Map`.
1960          * By setting this property, a deviating configuration may be specified.
1961          * The default is `null`, means inheriting from the parent form.
1962          *
1963          * @name LuCI.form.TypedSection.prototype#uciconfig
1964          * @type string
1965          * @default null
1966          */
1967
1968         /** @override */
1969         cfgsections: function() {
1970                 return this.map.data.sections(this.uciconfig || this.map.config, this.sectiontype)
1971                         .map(function(s) { return s['.name'] })
1972                         .filter(L.bind(this.filter, this));
1973         },
1974
1975         /** @private */
1976         handleAdd: function(ev, name) {
1977                 var config_name = this.uciconfig || this.map.config;
1978
1979                 this.map.data.add(config_name, this.sectiontype, name);
1980                 return this.map.save(null, true);
1981         },
1982
1983         /** @private */
1984         handleRemove: function(section_id, ev) {
1985                 var config_name = this.uciconfig || this.map.config;
1986
1987                 this.map.data.remove(config_name, section_id);
1988                 return this.map.save(null, true);
1989         },
1990
1991         /** @private */
1992         renderSectionAdd: function(extra_class) {
1993                 if (!this.addremove)
1994                         return E([]);
1995
1996                 var createEl = E('div', { 'class': 'cbi-section-create' }),
1997                     config_name = this.uciconfig || this.map.config,
1998                     btn_title = this.titleFn('addbtntitle');
1999
2000                 if (extra_class != null)
2001                         createEl.classList.add(extra_class);
2002
2003                 if (this.anonymous) {
2004                         createEl.appendChild(E('button', {
2005                                 'class': 'cbi-button cbi-button-add',
2006                                 'title': btn_title || _('Add'),
2007                                 'click': ui.createHandlerFn(this, 'handleAdd'),
2008                                 'disabled': this.map.readonly || null
2009                         }, [ btn_title || _('Add') ]));
2010                 }
2011                 else {
2012                         var nameEl = E('input', {
2013                                 'type': 'text',
2014                                 'class': 'cbi-section-create-name',
2015                                 'disabled': this.map.readonly || null
2016                         });
2017
2018                         dom.append(createEl, [
2019                                 E('div', {}, nameEl),
2020                                 E('input', {
2021                                         'class': 'cbi-button cbi-button-add',
2022                                         'type': 'submit',
2023                                         'value': btn_title || _('Add'),
2024                                         'title': btn_title || _('Add'),
2025                                         'click': ui.createHandlerFn(this, function(ev) {
2026                                                 if (nameEl.classList.contains('cbi-input-invalid'))
2027                                                         return;
2028
2029                                                 return this.handleAdd(ev, nameEl.value);
2030                                         }),
2031                                         'disabled': this.map.readonly || null
2032                                 })
2033                         ]);
2034
2035                         ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
2036                 }
2037
2038                 return createEl;
2039         },
2040
2041         /** @private */
2042         renderSectionPlaceholder: function() {
2043                 return E([
2044                         E('em', _('This section contains no values yet')),
2045                         E('br'), E('br')
2046                 ]);
2047         },
2048
2049         /** @private */
2050         renderContents: function(cfgsections, nodes) {
2051                 var section_id = null,
2052                     config_name = this.uciconfig || this.map.config,
2053                     sectionEl = E('div', {
2054                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
2055                                 'class': 'cbi-section',
2056                                 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
2057                                 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
2058                         });
2059
2060                 if (this.title != null && this.title != '')
2061                         sectionEl.appendChild(E('legend', {}, this.title));
2062
2063                 if (this.description != null && this.description != '')
2064                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
2065
2066                 for (var i = 0; i < nodes.length; i++) {
2067                         if (this.addremove) {
2068                                 sectionEl.appendChild(
2069                                         E('div', { 'class': 'cbi-section-remove right' },
2070                                                 E('button', {
2071                                                         'class': 'cbi-button',
2072                                                         'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
2073                                                         'data-section-id': cfgsections[i],
2074                                                         'click': ui.createHandlerFn(this, 'handleRemove', cfgsections[i]),
2075                                                         'disabled': this.map.readonly || null
2076                                                 }, [ _('Delete') ])));
2077                         }
2078
2079                         if (!this.anonymous)
2080                                 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
2081
2082                         sectionEl.appendChild(E('div', {
2083                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
2084                                 'class': this.tabs
2085                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
2086                                 'data-section-id': cfgsections[i]
2087                         }, nodes[i]));
2088                 }
2089
2090                 if (nodes.length == 0)
2091                         sectionEl.appendChild(this.renderSectionPlaceholder());
2092
2093                 sectionEl.appendChild(this.renderSectionAdd());
2094
2095                 dom.bindClassInstance(sectionEl, this);
2096
2097                 return sectionEl;
2098         },
2099
2100         /** @override */
2101         render: function() {
2102                 var cfgsections = this.cfgsections(),
2103                     renderTasks = [];
2104
2105                 for (var i = 0; i < cfgsections.length; i++)
2106                         renderTasks.push(this.renderUCISection(cfgsections[i]));
2107
2108                 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
2109         }
2110 });
2111
2112 /**
2113  * @class TableSection
2114  * @memberof LuCI.form
2115  * @augments LuCI.form.TypedSection
2116  * @hideconstructor
2117  * @classdesc
2118  *
2119  * The `TableSection` class maps all or - if `filter()` is overwritten - a
2120  * subset of the underlying UCI configuration sections of a given type.
2121  *
2122  * Layout wise, the configuration section instances mapped by the section
2123  * element (sometimes referred to as "section nodes") are rendered as rows
2124  * within an HTML table element, with an optional section remove button in the
2125  * last column and a section add button below the table, depending on the
2126  * value of the `addremove` property.
2127  *
2128  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
2129  * The configuration form this section is added to. It is automatically passed
2130  * by [section()]{@link LuCI.form.Map#section}.
2131  *
2132  * @param {string} section_type
2133  * The type of the UCI section to map.
2134  *
2135  * @param {string} [title]
2136  * The title caption of the form section element.
2137  *
2138  * @param {string} [description]
2139  * The description text of the form section element.
2140  */
2141 var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.prototype */ {
2142         __name__: 'CBI.TableSection',
2143
2144         /**
2145          * If set to `true`, the user may add or remove instances from the form
2146          * section widget, otherwise only preexisting sections may be edited.
2147          * The default is `false`.
2148          *
2149          * @name LuCI.form.TableSection.prototype#addremove
2150          * @type boolean
2151          * @default false
2152          */
2153
2154         /**
2155          * If set to `true`, mapped section instances are treated as anonymous
2156          * UCI sections, which means that section instance elements will be
2157          * rendered without title element and that no name is required when adding
2158          * new sections. The default is `false`.
2159          *
2160          * @name LuCI.form.TableSection.prototype#anonymous
2161          * @type boolean
2162          * @default false
2163          */
2164
2165         /**
2166          * Override the caption used for the section add button at the bottom of
2167          * the section form element. If set to a string, it will be used as-is,
2168          * if set to a function, the function will be invoked and its return value
2169          * is used as caption, after converting it to a string. If this property
2170          * is not set, the default is `Add`.
2171          *
2172          * @name LuCI.form.TableSection.prototype#addbtntitle
2173          * @type string|function
2174          * @default null
2175          */
2176
2177         /**
2178          * Override the per-section instance title caption shown in the first
2179          * column of the table unless `anonymous` is set to true. If set to a
2180          * string, it will be used as `String.format()` pattern with the name of
2181          * the underlying UCI section as first argument, if set to a function, the
2182          * function will be invoked with the section name as first argument and
2183          * its return value is used as caption, after converting it to a string.
2184          * If this property is not set, the default is the name of the underlying
2185          * UCI configuration section.
2186          *
2187          * @name LuCI.form.TableSection.prototype#sectiontitle
2188          * @type string|function
2189          * @default null
2190          */
2191
2192         /**
2193          * Override the per-section instance modal popup title caption shown when
2194          * clicking the `More…` button in a section specifying `max_cols`. If set
2195          * to a string, it will be used as `String.format()` pattern with the name
2196          * of the underlying UCI section as first argument, if set to a function,
2197          * the function will be invoked with the section name as first argument and
2198          * its return value is used as caption, after converting it to a string.
2199          * If this property is not set, the default is the name of the underlying
2200          * UCI configuration section.
2201          *
2202          * @name LuCI.form.TableSection.prototype#modaltitle
2203          * @type string|function
2204          * @default null
2205          */
2206
2207         /**
2208          * Override the UCI configuration name to read the section IDs from. By
2209          * default, the configuration name is inherited from the parent `Map`.
2210          * By setting this property, a deviating configuration may be specified.
2211          * The default is `null`, means inheriting from the parent form.
2212          *
2213          * @name LuCI.form.TableSection.prototype#uciconfig
2214          * @type string
2215          * @default null
2216          */
2217
2218         /**
2219          * Specify a maximum amount of columns to display. By default, one table
2220          * column is rendered for each child option of the form section element.
2221          * When this option is set to a positive number, then no more columns than
2222          * the given amount are rendered. When the number of child options exceeds
2223          * the specified amount, a `More…` button is rendered in the last column,
2224          * opening a modal dialog presenting all options elements in `NamedSection`
2225          * style when clicked.
2226          *
2227          * @name LuCI.form.TableSection.prototype#max_cols
2228          * @type number
2229          * @default null
2230          */
2231
2232         /**
2233          * If set to `true`, alternating `cbi-rowstyle-1` and `cbi-rowstyle-2` CSS
2234          * classes are added to the table row elements. Not all LuCI themes
2235          * implement these row style classes. The default is `false`.
2236          *
2237          * @name LuCI.form.TableSection.prototype#rowcolors
2238          * @type boolean
2239          * @default false
2240          */
2241
2242         /**
2243          * Enables a per-section instance row `Edit` button which triggers a certain
2244          * action when clicked. If set to a string, the string value is used
2245          * as `String.format()` pattern with the name of the underlying UCI section
2246          * as first format argument. The result is then interpreted as URL which
2247          * LuCI will navigate to when the user clicks the edit button.
2248          *
2249          * If set to a function, this function will be registered as click event
2250          * handler on the rendered edit button, receiving the section instance
2251          * name as first and the DOM click event as second argument.
2252          *
2253          * @name LuCI.form.TableSection.prototype#extedit
2254          * @type string|function
2255          * @default null
2256          */
2257
2258         /**
2259          * If set to `true`, a sort button is added to the last column, allowing
2260          * the user to reorder the section instances mapped by the section form
2261          * element.
2262          *
2263          * @name LuCI.form.TableSection.prototype#sortable
2264          * @type boolean
2265          * @default false
2266          */
2267
2268         /**
2269          * If set to `true`, the header row with the options descriptions will
2270          * not be displayed. By default, descriptions row is automatically displayed
2271          * when at least one option has a description.
2272          *
2273          * @name LuCI.form.TableSection.prototype#nodescriptions
2274          * @type boolean
2275          * @default false
2276          */
2277
2278         /**
2279          * The `TableSection` implementation does not support option tabbing, so
2280          * its implementation of `tab()` will always throw an exception when
2281          * invoked.
2282          *
2283          * @override
2284          * @throws Throws an exception when invoked.
2285          */
2286         tab: function() {
2287                 throw 'Tabs are not supported by TableSection';
2288         },
2289
2290         /** @private */
2291         renderContents: function(cfgsections, nodes) {
2292                 var section_id = null,
2293                     config_name = this.uciconfig || this.map.config,
2294                     max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
2295                     has_more = max_cols < this.children.length,
2296                     sectionEl = E('div', {
2297                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
2298                                 'class': 'cbi-section cbi-tblsection',
2299                                 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
2300                                 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
2301                         }),
2302                         tableEl = E('div', {
2303                                 'class': 'table cbi-section-table'
2304                         });
2305
2306                 if (this.title != null && this.title != '')
2307                         sectionEl.appendChild(E('h3', {}, this.title));
2308
2309                 if (this.description != null && this.description != '')
2310                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
2311
2312                 tableEl.appendChild(this.renderHeaderRows(max_cols));
2313
2314                 for (var i = 0; i < nodes.length; i++) {
2315                         var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
2316
2317                         if (sectionname == null)
2318                                 sectionname = cfgsections[i];
2319
2320                         var trEl = E('div', {
2321                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
2322                                 'class': 'tr cbi-section-table-row',
2323                                 'data-sid': cfgsections[i],
2324                                 'draggable': this.sortable ? true : null,
2325                                 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
2326                                 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
2327                                 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
2328                                 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
2329                                 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
2330                                 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
2331                                 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
2332                                 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
2333                                 'data-section-id': cfgsections[i]
2334                         });
2335
2336                         if (this.extedit || this.rowcolors)
2337                                 trEl.classList.add(!(tableEl.childNodes.length % 2)
2338                                         ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
2339
2340                         for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
2341                                 trEl.appendChild(nodes[i].firstChild);
2342
2343                         trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
2344                         tableEl.appendChild(trEl);
2345                 }
2346
2347                 if (nodes.length == 0)
2348                         tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
2349                                 E('div', { 'class': 'td' },
2350                                         E('em', {}, _('This section contains no values yet')))));
2351
2352                 sectionEl.appendChild(tableEl);
2353
2354                 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
2355
2356                 dom.bindClassInstance(sectionEl, this);
2357
2358                 return sectionEl;
2359         },
2360
2361         /** @private */
2362         renderHeaderRows: function(max_cols, has_action) {
2363                 var has_titles = false,
2364                     has_descriptions = false,
2365                     max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
2366                     has_more = max_cols < this.children.length,
2367                     anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
2368                     trEls = E([]);
2369
2370                 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
2371                         if (opt.modalonly)
2372                                 continue;
2373
2374                         has_titles = has_titles || !!opt.title;
2375                         has_descriptions = has_descriptions || !!opt.description;
2376                 }
2377
2378                 if (has_titles) {
2379                         var trEl = E('div', {
2380                                 'class': 'tr cbi-section-table-titles ' + anon_class,
2381                                 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
2382                         });
2383
2384                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
2385                                 if (opt.modalonly)
2386                                         continue;
2387
2388                                 trEl.appendChild(E('div', {
2389                                         'class': 'th cbi-section-table-cell',
2390                                         'data-widget': opt.__name__
2391                                 }));
2392
2393                                 if (opt.width != null)
2394                                         trEl.lastElementChild.style.width =
2395                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
2396
2397                                 if (opt.titleref)
2398                                         trEl.lastElementChild.appendChild(E('a', {
2399                                                 'href': opt.titleref,
2400                                                 'class': 'cbi-title-ref',
2401                                                 'title': this.titledesc || _('Go to relevant configuration page')
2402                                         }, opt.title));
2403                                 else
2404                                         dom.content(trEl.lastElementChild, opt.title);
2405                         }
2406
2407                         if (this.sortable || this.extedit || this.addremove || has_more || has_action)
2408                                 trEl.appendChild(E('div', {
2409                                         'class': 'th cbi-section-table-cell cbi-section-actions'
2410                                 }));
2411
2412                         trEls.appendChild(trEl);
2413                 }
2414
2415                 if (has_descriptions && !this.nodescriptions) {
2416                         var trEl = E('div', {
2417                                 'class': 'tr cbi-section-table-descr ' + anon_class
2418                         });
2419
2420                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
2421                                 if (opt.modalonly)
2422                                         continue;
2423
2424                                 trEl.appendChild(E('div', {
2425                                         'class': 'th cbi-section-table-cell',
2426                                         'data-widget': opt.__name__
2427                                 }, opt.description));
2428
2429                                 if (opt.width != null)
2430                                         trEl.lastElementChild.style.width =
2431                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
2432                         }
2433
2434                         if (this.sortable || this.extedit || this.addremove || has_more || has_action)
2435                                 trEl.appendChild(E('div', {
2436                                         'class': 'th cbi-section-table-cell cbi-section-actions'
2437                                 }));
2438
2439                         trEls.appendChild(trEl);
2440                 }
2441
2442                 return trEls;
2443         },
2444
2445         /** @private */
2446         renderRowActions: function(section_id, more_label) {
2447                 var config_name = this.uciconfig || this.map.config;
2448
2449                 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
2450                         return E([]);
2451
2452                 var tdEl = E('div', {
2453                         'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
2454                 }, E('div'));
2455
2456                 if (this.sortable) {
2457                         dom.append(tdEl.lastElementChild, [
2458                                 E('div', {
2459                                         'title': _('Drag to reorder'),
2460                                         'class': 'btn cbi-button drag-handle center',
2461                                         'style': 'cursor:move',
2462                                         'disabled': this.map.readonly || null
2463                                 }, '☰')
2464                         ]);
2465                 }
2466
2467                 if (this.extedit) {
2468                         var evFn = null;
2469
2470                         if (typeof(this.extedit) == 'function')
2471                                 evFn = L.bind(this.extedit, this);
2472                         else if (typeof(this.extedit) == 'string')
2473                                 evFn = L.bind(function(sid, ev) {
2474                                         location.href = this.extedit.format(sid);
2475                                 }, this, section_id);
2476
2477                         dom.append(tdEl.lastElementChild,
2478                                 E('button', {
2479                                         'title': _('Edit'),
2480                                         'class': 'cbi-button cbi-button-edit',
2481                                         'click': evFn
2482                                 }, [ _('Edit') ])
2483                         );
2484                 }
2485
2486                 if (more_label) {
2487                         dom.append(tdEl.lastElementChild,
2488                                 E('button', {
2489                                         'title': more_label,
2490                                         'class': 'cbi-button cbi-button-edit',
2491                                         'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
2492                                 }, [ more_label ])
2493                         );
2494                 }
2495
2496                 if (this.addremove) {
2497                         var btn_title = this.titleFn('removebtntitle', section_id);
2498
2499                         dom.append(tdEl.lastElementChild,
2500                                 E('button', {
2501                                         'title': btn_title || _('Delete'),
2502                                         'class': 'cbi-button cbi-button-remove',
2503                                         'click': ui.createHandlerFn(this, 'handleRemove', section_id),
2504                                         'disabled': this.map.readonly || null
2505                                 }, [ btn_title || _('Delete') ])
2506                         );
2507                 }
2508
2509                 return tdEl;
2510         },
2511
2512         /** @private */
2513         handleDragInit: function(ev) {
2514                 scope.dragState = { node: ev.target };
2515         },
2516
2517         /** @private */
2518         handleDragStart: function(ev) {
2519                 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
2520                         scope.dragState = null;
2521                         ev.preventDefault();
2522                         return false;
2523                 }
2524
2525                 scope.dragState.node = dom.parent(scope.dragState.node, '.tr');
2526                 ev.dataTransfer.setData('text', 'drag');
2527                 ev.target.style.opacity = 0.4;
2528         },
2529
2530         /** @private */
2531         handleDragOver: function(ev) {
2532                 var n = scope.dragState.targetNode,
2533                     r = scope.dragState.rect,
2534                     t = r.top + r.height / 2;
2535
2536                 if (ev.clientY <= t) {
2537                         n.classList.remove('drag-over-below');
2538                         n.classList.add('drag-over-above');
2539                 }
2540                 else {
2541                         n.classList.remove('drag-over-above');
2542                         n.classList.add('drag-over-below');
2543                 }
2544
2545                 ev.dataTransfer.dropEffect = 'move';
2546                 ev.preventDefault();
2547                 return false;
2548         },
2549
2550         /** @private */
2551         handleDragEnter: function(ev) {
2552                 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
2553                 scope.dragState.targetNode = ev.currentTarget;
2554         },
2555
2556         /** @private */
2557         handleDragLeave: function(ev) {
2558                 ev.currentTarget.classList.remove('drag-over-above');
2559                 ev.currentTarget.classList.remove('drag-over-below');
2560         },
2561
2562         /** @private */
2563         handleDragEnd: function(ev) {
2564                 var n = ev.target;
2565
2566                 n.style.opacity = '';
2567                 n.classList.add('flash');
2568                 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
2569                         .forEach(function(tr) {
2570                                 tr.classList.remove('drag-over-above');
2571                                 tr.classList.remove('drag-over-below');
2572                         });
2573         },
2574
2575         /** @private */
2576         handleDrop: function(ev) {
2577                 var s = scope.dragState;
2578
2579                 if (s.node && s.targetNode) {
2580                         var config_name = this.uciconfig || this.map.config,
2581                             ref_node = s.targetNode,
2582                             after = false;
2583
2584                     if (ref_node.classList.contains('drag-over-below')) {
2585                         ref_node = ref_node.nextElementSibling;
2586                         after = true;
2587                     }
2588
2589                     var sid1 = s.node.getAttribute('data-sid'),
2590                         sid2 = s.targetNode.getAttribute('data-sid');
2591
2592                     s.node.parentNode.insertBefore(s.node, ref_node);
2593                     this.map.data.move(config_name, sid1, sid2, after);
2594                 }
2595
2596                 scope.dragState = null;
2597                 ev.target.style.opacity = '';
2598                 ev.stopPropagation();
2599                 ev.preventDefault();
2600                 return false;
2601         },
2602
2603         /** @private */
2604         handleModalCancel: function(modalMap, ev) {
2605                 return Promise.resolve(ui.hideModal());
2606         },
2607
2608         /** @private */
2609         handleModalSave: function(modalMap, ev) {
2610                 return modalMap.save()
2611                         .then(L.bind(this.map.load, this.map))
2612                         .then(L.bind(this.map.reset, this.map))
2613                         .then(ui.hideModal)
2614                         .catch(function() {});
2615         },
2616
2617         /**
2618          * Add further options to the per-section instanced modal popup.
2619          *
2620          * This function may be overwritten by user code to perform additional
2621          * setup steps before displaying the more options modal which is useful to
2622          * e.g. query additional data or to inject further option elements.
2623          *
2624          * The default implementation of this function does nothing.
2625          *
2626          * @abstract
2627          * @param {LuCI.form.NamedSection} modalSection
2628          * The `NamedSection` instance about to be rendered in the modal popup.
2629          *
2630          * @param {string} section_id
2631          * The ID of the underlying UCI section the modal popup belongs to.
2632          *
2633          * @param {Event} ev
2634          * The DOM event emitted by clicking the `More…` button.
2635          *
2636          * @returns {*|Promise<*>}
2637          * Return values of this function are ignored but if a promise is returned,
2638          * it is run to completion before the rendering is continued, allowing
2639          * custom logic to perform asynchroneous work before the modal dialog
2640          * is shown.
2641          */
2642         addModalOptions: function(modalSection, section_id, ev) {
2643
2644         },
2645
2646         /** @private */
2647         renderMoreOptionsModal: function(section_id, ev) {
2648                 var parent = this.map,
2649                     title = parent.title,
2650                     name = null,
2651                     m = new CBIMap(this.map.config, null, null),
2652                     s = m.section(CBINamedSection, section_id, this.sectiontype);
2653
2654                 m.parent = parent;
2655                 m.readonly = parent.readonly;
2656
2657                 s.tabs = this.tabs;
2658                 s.tab_names = this.tab_names;
2659
2660                 if ((name = this.titleFn('modaltitle', section_id)) != null)
2661                         title = name;
2662                 else if ((name = this.titleFn('sectiontitle', section_id)) != null)
2663                         title = '%s - %s'.format(parent.title, name);
2664                 else if (!this.anonymous)
2665                         title = '%s - %s'.format(parent.title, section_id);
2666
2667                 for (var i = 0; i < this.children.length; i++) {
2668                         var o1 = this.children[i];
2669
2670                         if (o1.modalonly === false)
2671                                 continue;
2672
2673                         var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
2674
2675                         for (var k in o1) {
2676                                 if (!o1.hasOwnProperty(k))
2677                                         continue;
2678
2679                                 switch (k) {
2680                                 case 'map':
2681                                 case 'section':
2682                                 case 'option':
2683                                 case 'title':
2684                                 case 'description':
2685                                         continue;
2686
2687                                 default:
2688                                         o2[k] = o1[k];
2689                                 }
2690                         }
2691                 }
2692
2693                 return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
2694                         ui.showModal(title, [
2695                                 nodes,
2696                                 E('div', { 'class': 'right' }, [
2697                                         E('button', {
2698                                                 'class': 'btn',
2699                                                 'click': ui.createHandlerFn(this, 'handleModalCancel', m)
2700                                         }, [ _('Dismiss') ]), ' ',
2701                                         E('button', {
2702                                                 'class': 'cbi-button cbi-button-positive important',
2703                                                 'click': ui.createHandlerFn(this, 'handleModalSave', m),
2704                                                 'disabled': m.readonly || null
2705                                         }, [ _('Save') ])
2706                                 ])
2707                         ], 'cbi-modal');
2708                 }, this)).catch(L.error);
2709         }
2710 });
2711
2712 /**
2713  * @class GridSection
2714  * @memberof LuCI.form
2715  * @augments LuCI.form.TableSection
2716  * @hideconstructor
2717  * @classdesc
2718  *
2719  * The `GridSection` class maps all or - if `filter()` is overwritten - a
2720  * subset of the underlying UCI configuration sections of a given type.
2721  *
2722  * A grid section functions similar to a {@link LuCI.form.TableSection} but
2723  * supports tabbing in the modal overlay. Option elements added with
2724  * [option()]{@link LuCI.form.GridSection#option} are shown in the table while
2725  * elements added with [taboption()]{@link LuCI.form.GridSection#taboption}
2726  * are displayed in the modal popup.
2727  *
2728  * Another important difference is that the table cells show a readonly text
2729  * preview of the corresponding option elements by default, unless the child
2730  * option element is explicitely made writable by setting the `editable`
2731  * property to `true`.
2732  *
2733  * Additionally, the grid section honours a `modalonly` property of child
2734  * option elements. Refer to the [AbstractValue]{@link LuCI.form.AbstractValue}
2735  * documentation for details.
2736  *
2737  * Layout wise, a grid section looks mostly identical to table sections.
2738  *
2739  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
2740  * The configuration form this section is added to. It is automatically passed
2741  * by [section()]{@link LuCI.form.Map#section}.
2742  *
2743  * @param {string} section_type
2744  * The type of the UCI section to map.
2745  *
2746  * @param {string} [title]
2747  * The title caption of the form section element.
2748  *
2749  * @param {string} [description]
2750  * The description text of the form section element.
2751  */
2752 var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.prototype */ {
2753         /**
2754          * Add an option tab to the section.
2755          *
2756          * The modal option elements of a grid section may be divided into multiple
2757          * tabs to provide a better overview to the user.
2758          *
2759          * Before options can be moved into a tab pane, the corresponding tab
2760          * has to be defined first, which is done by calling this function.
2761          *
2762          * Note that tabs are only effective in modal popups, options added with
2763          * `option()` will not be assigned to a specific tab and are rendered in
2764          * the table view only.
2765          *
2766          * @param {string} name
2767          * The name of the tab to register. It may be freely chosen and just serves
2768          * as an identifier to differentiate tabs.
2769          *
2770          * @param {string} title
2771          * The human readable caption of the tab.
2772          *
2773          * @param {string} [description]
2774          * An additional description text for the corresponding tab pane. It is
2775          * displayed as text paragraph below the tab but before the tab pane
2776          * contents. If omitted, no description will be rendered.
2777          *
2778          * @throws {Error}
2779          * Throws an exeption if a tab with the same `name` already exists.
2780          */
2781         tab: function(name, title, description) {
2782                 CBIAbstractSection.prototype.tab.call(this, name, title, description);
2783         },
2784
2785         /** @private */
2786         handleAdd: function(ev, name) {
2787                 var config_name = this.uciconfig || this.map.config,
2788                     section_id = this.map.data.add(config_name, this.sectiontype, name);
2789
2790                 this.addedSection = section_id;
2791                 return this.renderMoreOptionsModal(section_id);
2792         },
2793
2794         /** @private */
2795         handleModalSave: function(/* ... */) {
2796                 return this.super('handleModalSave', arguments)
2797                         .then(L.bind(function() { this.addedSection = null }, this));
2798         },
2799
2800         /** @private */
2801         handleModalCancel: function(/* ... */) {
2802                 var config_name = this.uciconfig || this.map.config;
2803
2804                 if (this.addedSection != null) {
2805                         this.map.data.remove(config_name, this.addedSection);
2806                         this.addedSection = null;
2807                 }
2808
2809                 return this.super('handleModalCancel', arguments);
2810         },
2811
2812         /** @private */
2813         renderUCISection: function(section_id) {
2814                 return this.renderOptions(null, section_id);
2815         },
2816
2817         /** @private */
2818         renderChildren: function(tab_name, section_id, in_table) {
2819                 var tasks = [], index = 0;
2820
2821                 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
2822                         if (opt.disable || opt.modalonly)
2823                                 continue;
2824
2825                         if (opt.editable)
2826                                 tasks.push(opt.render(index++, section_id, in_table));
2827                         else
2828                                 tasks.push(this.renderTextValue(section_id, opt));
2829                 }
2830
2831                 return Promise.all(tasks);
2832         },
2833
2834         /** @private */
2835         renderTextValue: function(section_id, opt) {
2836                 var title = this.stripTags(opt.title).trim(),
2837                     descr = this.stripTags(opt.description).trim(),
2838                     value = opt.textvalue(section_id);
2839
2840                 return E('div', {
2841                         'class': 'td cbi-value-field',
2842                         'data-title': (title != '') ? title : null,
2843                         'data-description': (descr != '') ? descr : null,
2844                         'data-name': opt.option,
2845                         'data-widget': opt.typename || opt.__name__
2846                 }, (value != null) ? value : E('em', _('none')));
2847         },
2848
2849         /** @private */
2850         renderHeaderRows: function(section_id) {
2851                 return this.super('renderHeaderRows', [ NaN, true ]);
2852         },
2853
2854         /** @private */
2855         renderRowActions: function(section_id) {
2856                 return this.super('renderRowActions', [ section_id, _('Edit') ]);
2857         },
2858
2859         /** @override */
2860         parse: function() {
2861                 var section_ids = this.cfgsections(),
2862                     tasks = [];
2863
2864                 if (Array.isArray(this.children)) {
2865                         for (var i = 0; i < section_ids.length; i++) {
2866                                 for (var j = 0; j < this.children.length; j++) {
2867                                         if (!this.children[j].editable || this.children[j].modalonly)
2868                                                 continue;
2869
2870                                         tasks.push(this.children[j].parse(section_ids[i]));
2871                                 }
2872                         }
2873                 }
2874
2875                 return Promise.all(tasks);
2876         }
2877 });
2878
2879 /**
2880  * @class NamedSection
2881  * @memberof LuCI.form
2882  * @augments LuCI.form.AbstractSection
2883  * @hideconstructor
2884  * @classdesc
2885  *
2886  * The `NamedSection` class maps exactly one UCI section instance which is
2887  * specified when constructing the class instance.
2888  *
2889  * Layout and functionality wise, a named section is essentially a
2890  * `TypedSection` which allows exactly one section node.
2891  *
2892  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
2893  * The configuration form this section is added to. It is automatically passed
2894  * by [section()]{@link LuCI.form.Map#section}.
2895  *
2896  * @param {string} section_id
2897  * The name (ID) of the UCI section to map.
2898  *
2899  * @param {string} section_type
2900  * The type of the UCI section to map.
2901  *
2902  * @param {string} [title]
2903  * The title caption of the form section element.
2904  *
2905  * @param {string} [description]
2906  * The description text of the form section element.
2907  */
2908 var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSection.prototype */ {
2909         __name__: 'CBI.NamedSection',
2910         __init__: function(map, section_id /*, ... */) {
2911                 this.super('__init__', this.varargs(arguments, 2, map));
2912
2913                 this.section = section_id;
2914         },
2915
2916         /**
2917          * If set to `true`, the user may remove or recreate the sole mapped
2918          * configuration instance from the form section widget, otherwise only a
2919          * preexisting section may be edited. The default is `false`.
2920          *
2921          * @name LuCI.form.NamedSection.prototype#addremove
2922          * @type boolean
2923          * @default false
2924          */
2925
2926         /**
2927          * Override the UCI configuration name to read the section IDs from. By
2928          * default, the configuration name is inherited from the parent `Map`.
2929          * By setting this property, a deviating configuration may be specified.
2930          * The default is `null`, means inheriting from the parent form.
2931          *
2932          * @name LuCI.form.NamedSection.prototype#uciconfig
2933          * @type string
2934          * @default null
2935          */
2936
2937         /**
2938          * The `NamedSection` class overwrites the generic `cfgsections()`
2939          * implementation to return a one-element array containing the mapped
2940          * section ID as sole element. User code should not normally change this.
2941          *
2942          * @returns {string[]}
2943          * Returns a one-element array containing the mapped section ID.
2944          */
2945         cfgsections: function() {
2946                 return [ this.section ];
2947         },
2948
2949         /** @private */
2950         handleAdd: function(ev) {
2951                 var section_id = this.section,
2952                     config_name = this.uciconfig || this.map.config;
2953
2954                 this.map.data.add(config_name, this.sectiontype, section_id);
2955                 return this.map.save(null, true);
2956         },
2957
2958         /** @private */
2959         handleRemove: function(ev) {
2960                 var section_id = this.section,
2961                     config_name = this.uciconfig || this.map.config;
2962
2963                 this.map.data.remove(config_name, section_id);
2964                 return this.map.save(null, true);
2965         },
2966
2967         /** @private */
2968         renderContents: function(data) {
2969                 var ucidata = data[0], nodes = data[1],
2970                     section_id = this.section,
2971                     config_name = this.uciconfig || this.map.config,
2972                     sectionEl = E('div', {
2973                                 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
2974                                 'class': 'cbi-section',
2975                                 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
2976                                 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
2977                         });
2978
2979                 if (typeof(this.title) === 'string' && this.title !== '')
2980                         sectionEl.appendChild(E('legend', {}, this.title));
2981
2982                 if (typeof(this.description) === 'string' && this.description !== '')
2983                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
2984
2985                 if (ucidata) {
2986                         if (this.addremove) {
2987                                 sectionEl.appendChild(
2988                                         E('div', { 'class': 'cbi-section-remove right' },
2989                                                 E('button', {
2990                                                         'class': 'cbi-button',
2991                                                         'click': ui.createHandlerFn(this, 'handleRemove'),
2992                                                         'disabled': this.map.readonly || null
2993                                                 }, [ _('Delete') ])));
2994                         }
2995
2996                         sectionEl.appendChild(E('div', {
2997                                 'id': 'cbi-%s-%s'.format(config_name, section_id),
2998                                 'class': this.tabs
2999                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
3000                                 'data-section-id': section_id
3001                         }, nodes));
3002                 }
3003                 else if (this.addremove) {
3004                         sectionEl.appendChild(
3005                                 E('button', {
3006                                         'class': 'cbi-button cbi-button-add',
3007                                         'click': ui.createHandlerFn(this, 'handleAdd'),
3008                                         'disabled': this.map.readonly || null
3009                                 }, [ _('Add') ]));
3010                 }
3011
3012                 dom.bindClassInstance(sectionEl, this);
3013
3014                 return sectionEl;
3015         },
3016
3017         /** @override */
3018         render: function() {
3019                 var config_name = this.uciconfig || this.map.config,
3020                     section_id = this.section;
3021
3022                 return Promise.all([
3023                         this.map.data.get(config_name, section_id),
3024                         this.renderUCISection(section_id)
3025                 ]).then(this.renderContents.bind(this));
3026         }
3027 });
3028
3029 /**
3030  * @class Value
3031  * @memberof LuCI.form
3032  * @augments LuCI.form.AbstractValue
3033  * @hideconstructor
3034  * @classdesc
3035  *
3036  * The `Value` class represents a simple one-line form input using the
3037  * {@link LuCI.ui.Textfield} or - in case choices are added - the
3038  * {@link LuCI.ui.Combobox} class as underlying widget.
3039  *
3040  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3041  * The configuration form this section is added to. It is automatically passed
3042  * by [option()]{@link LuCI.form.AbstractSection#option} or
3043  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3044  * option to the section.
3045  *
3046  * @param {LuCI.form.AbstractSection} section
3047  * The configuration section this option is added to. It is automatically passed
3048  * by [option()]{@link LuCI.form.AbstractSection#option} or
3049  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3050  * option to the section.
3051  *
3052  * @param {string} option
3053  * The name of the UCI option to map.
3054  *
3055  * @param {string} [title]
3056  * The title caption of the option element.
3057  *
3058  * @param {string} [description]
3059  * The description text of the option element.
3060  */
3061 var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
3062         __name__: 'CBI.Value',
3063
3064         /**
3065          * If set to `true`, the field is rendered as password input, otherwise
3066          * as plain text input.
3067          *
3068          * @name LuCI.form.Value.prototype#password
3069          * @type boolean
3070          * @default false
3071          */
3072
3073         /**
3074          * Set a placeholder string to use when the input field is empty.
3075          *
3076          * @name LuCI.form.Value.prototype#placeholder
3077          * @type string
3078          * @default null
3079          */
3080
3081         /**
3082          * Add a predefined choice to the form option. By adding one or more
3083          * choices, the plain text input field is turned into a combobox widget
3084          * which prompts the user to select a predefined choice, or to enter a
3085          * custom value.
3086          *
3087          * @param {string} key
3088          * The choice value to add.
3089          *
3090          * @param {Node|string} value
3091          * The caption for the choice value. May be a DOM node, a document fragment
3092          * or a plain text string. If omitted, the `key` value is used as caption.
3093          */
3094         value: function(key, val) {
3095                 this.keylist = this.keylist || [];
3096                 this.keylist.push(String(key));
3097
3098                 this.vallist = this.vallist || [];
3099                 this.vallist.push(dom.elem(val) ? val : String(val != null ? val : key));
3100         },
3101
3102         /** @override */
3103         render: function(option_index, section_id, in_table) {
3104                 return Promise.resolve(this.cfgvalue(section_id))
3105                         .then(this.renderWidget.bind(this, section_id, option_index))
3106                         .then(this.renderFrame.bind(this, section_id, in_table, option_index));
3107         },
3108
3109         /** @private */
3110         renderFrame: function(section_id, in_table, option_index, nodes) {
3111                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
3112                     depend_list = this.transformDepList(section_id),
3113                     optionEl;
3114
3115                 if (in_table) {
3116                         var title = this.stripTags(this.title).trim();
3117                         optionEl = E('div', {
3118                                 'class': 'td cbi-value-field',
3119                                 'data-title': (title != '') ? title : null,
3120                                 'data-description': this.stripTags(this.description).trim(),
3121                                 'data-name': this.option,
3122                                 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
3123                         }, E('div', {
3124                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
3125                                 'data-index': option_index,
3126                                 'data-depends': depend_list,
3127                                 'data-field': this.cbid(section_id)
3128                         }));
3129                 }
3130                 else {
3131                         optionEl = E('div', {
3132                                 'class': 'cbi-value',
3133                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
3134                                 'data-index': option_index,
3135                                 'data-depends': depend_list,
3136                                 'data-field': this.cbid(section_id),
3137                                 'data-name': this.option,
3138                                 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
3139                         });
3140
3141                         if (this.last_child)
3142                                 optionEl.classList.add('cbi-value-last');
3143
3144                         if (typeof(this.title) === 'string' && this.title !== '') {
3145                                 optionEl.appendChild(E('label', {
3146                                         'class': 'cbi-value-title',
3147                                         'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option),
3148                                         'click': function(ev) {
3149                                                 var node = ev.currentTarget,
3150                                                     elem = node.nextElementSibling.querySelector('#' + node.getAttribute('for')) || node.nextElementSibling.querySelector('[data-widget-id="' + node.getAttribute('for') + '"]');
3151
3152                                                 if (elem) {
3153                                                         elem.click();
3154                                                         elem.focus();
3155                                                 }
3156                                         }
3157                                 },
3158                                 this.titleref ? E('a', {
3159                                         'class': 'cbi-title-ref',
3160                                         'href': this.titleref,
3161                                         'title': this.titledesc || _('Go to relevant configuration page')
3162                                 }, this.title) : this.title));
3163
3164                                 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
3165                         }
3166                 }
3167
3168                 if (nodes)
3169                         (optionEl.lastChild || optionEl).appendChild(nodes);
3170
3171                 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
3172                         dom.append(optionEl.lastChild || optionEl,
3173                                 E('div', { 'class': 'cbi-value-description' }, this.description));
3174
3175                 if (depend_list && depend_list.length)
3176                         optionEl.classList.add('hidden');
3177
3178                 optionEl.addEventListener('widget-change',
3179                         L.bind(this.map.checkDepends, this.map));
3180
3181                 dom.bindClassInstance(optionEl, this);
3182
3183                 return optionEl;
3184         },
3185
3186         /** @private */
3187         renderWidget: function(section_id, option_index, cfgvalue) {
3188                 var value = (cfgvalue != null) ? cfgvalue : this.default,
3189                     choices = this.transformChoices(),
3190                     widget;
3191
3192                 if (choices) {
3193                         var placeholder = (this.optional || this.rmempty)
3194                                 ? E('em', _('unspecified')) : _('-- Please choose --');
3195
3196                         widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
3197                                 id: this.cbid(section_id),
3198                                 sort: this.keylist,
3199                                 optional: this.optional || this.rmempty,
3200                                 datatype: this.datatype,
3201                                 select_placeholder: this.placeholder || placeholder,
3202                                 validate: L.bind(this.validate, this, section_id),
3203                                 disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3204                         });
3205                 }
3206                 else {
3207                         widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
3208                                 id: this.cbid(section_id),
3209                                 password: this.password,
3210                                 optional: this.optional || this.rmempty,
3211                                 datatype: this.datatype,
3212                                 placeholder: this.placeholder,
3213                                 validate: L.bind(this.validate, this, section_id),
3214                                 disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3215                         });
3216                 }
3217
3218                 return widget.render();
3219         }
3220 });
3221
3222 /**
3223  * @class DynamicList
3224  * @memberof LuCI.form
3225  * @augments LuCI.form.Value
3226  * @hideconstructor
3227  * @classdesc
3228  *
3229  * The `DynamicList` class represents a multi value widget allowing the user
3230  * to enter multiple unique values, optionally selected from a set of
3231  * predefined choices. It builds upon the {@link LuCI.ui.DynamicList} widget.
3232  *
3233  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3234  * The configuration form this section is added to. It is automatically passed
3235  * by [option()]{@link LuCI.form.AbstractSection#option} or
3236  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3237  * option to the section.
3238  *
3239  * @param {LuCI.form.AbstractSection} section
3240  * The configuration section this option is added to. It is automatically passed
3241  * by [option()]{@link LuCI.form.AbstractSection#option} or
3242  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3243  * option to the section.
3244  *
3245  * @param {string} option
3246  * The name of the UCI option to map.
3247  *
3248  * @param {string} [title]
3249  * The title caption of the option element.
3250  *
3251  * @param {string} [description]
3252  * The description text of the option element.
3253  */
3254 var CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototype */ {
3255         __name__: 'CBI.DynamicList',
3256
3257         /** @private */
3258         renderWidget: function(section_id, option_index, cfgvalue) {
3259                 var value = (cfgvalue != null) ? cfgvalue : this.default,
3260                     choices = this.transformChoices(),
3261                     items = L.toArray(value);
3262
3263                 var widget = new ui.DynamicList(items, choices, {
3264                         id: this.cbid(section_id),
3265                         sort: this.keylist,
3266                         optional: this.optional || this.rmempty,
3267                         datatype: this.datatype,
3268                         placeholder: this.placeholder,
3269                         validate: L.bind(this.validate, this, section_id),
3270                         disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3271                 });
3272
3273                 return widget.render();
3274         },
3275 });
3276
3277 /**
3278  * @class ListValue
3279  * @memberof LuCI.form
3280  * @augments LuCI.form.Value
3281  * @hideconstructor
3282  * @classdesc
3283  *
3284  * The `ListValue` class implements a simple static HTML select element
3285  * allowing the user to chose a single value from a set of predefined choices.
3286  * It builds upon the {@link LuCI.ui.Select} widget.
3287  *
3288  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3289  * The configuration form this section is added to. It is automatically passed
3290  * by [option()]{@link LuCI.form.AbstractSection#option} or
3291  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3292  * option to the section.
3293  *
3294  * @param {LuCI.form.AbstractSection} section
3295  * The configuration section this option is added to. It is automatically passed
3296  * by [option()]{@link LuCI.form.AbstractSection#option} or
3297  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3298  * option to the section.
3299  *
3300  * @param {string} option
3301  * The name of the UCI option to map.
3302  *
3303  * @param {string} [title]
3304  * The title caption of the option element.
3305  *
3306  * @param {string} [description]
3307  * The description text of the option element.
3308  */
3309 var CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ {
3310         __name__: 'CBI.ListValue',
3311
3312         __init__: function() {
3313                 this.super('__init__', arguments);
3314                 this.widget = 'select';
3315                 this.deplist = [];
3316         },
3317
3318         /**
3319          * Set the size attribute of the underlying HTML select element.
3320          *
3321          * @name LuCI.form.ListValue.prototype#size
3322          * @type number
3323          * @default null
3324          */
3325
3326          /** @private */
3327         renderWidget: function(section_id, option_index, cfgvalue) {
3328                 var choices = this.transformChoices();
3329                 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
3330                         id: this.cbid(section_id),
3331                         size: this.size,
3332                         sort: this.keylist,
3333                         optional: this.optional,
3334                         placeholder: this.placeholder,
3335                         validate: L.bind(this.validate, this, section_id),
3336                         disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3337                 });
3338
3339                 return widget.render();
3340         },
3341 });
3342
3343 /**
3344  * @class FlagValue
3345  * @memberof LuCI.form
3346  * @augments LuCI.form.Value
3347  * @hideconstructor
3348  * @classdesc
3349  *
3350  * The `FlagValue` element builds upon the {@link LuCI.ui.Checkbox} widget to
3351  * implement a simple checkbox element.
3352  *
3353  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3354  * The configuration form this section is added to. It is automatically passed
3355  * by [option()]{@link LuCI.form.AbstractSection#option} or
3356  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3357  * option to the section.
3358  *
3359  * @param {LuCI.form.AbstractSection} section
3360  * The configuration section this option is added to. It is automatically passed
3361  * by [option()]{@link LuCI.form.AbstractSection#option} or
3362  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3363  * option to the section.
3364  *
3365  * @param {string} option
3366  * The name of the UCI option to map.
3367  *
3368  * @param {string} [title]
3369  * The title caption of the option element.
3370  *
3371  * @param {string} [description]
3372  * The description text of the option element.
3373  */
3374 var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
3375         __name__: 'CBI.FlagValue',
3376
3377         __init__: function() {
3378                 this.super('__init__', arguments);
3379
3380                 this.enabled = '1';
3381                 this.disabled = '0';
3382                 this.default = this.disabled;
3383         },
3384
3385         /**
3386          * Sets the input value to use for the checkbox checked state.
3387          *
3388          * @name LuCI.form.FlagValue.prototype#enabled
3389          * @type number
3390          * @default 1
3391          */
3392
3393         /**
3394          * Sets the input value to use for the checkbox unchecked state.
3395          *
3396          * @name LuCI.form.FlagValue.prototype#disabled
3397          * @type number
3398          * @default 0
3399          */
3400
3401         /** @private */
3402         renderWidget: function(section_id, option_index, cfgvalue) {
3403                 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
3404                         id: this.cbid(section_id),
3405                         value_enabled: this.enabled,
3406                         value_disabled: this.disabled,
3407                         validate: L.bind(this.validate, this, section_id),
3408                         disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3409                 });
3410
3411                 return widget.render();
3412         },
3413
3414         /**
3415          * Query the checked state of the underlying checkbox widget and return
3416          * either the `enabled` or the `disabled` property value, depending on
3417          * the checked state.
3418          *
3419          * @override
3420          */
3421         formvalue: function(section_id) {
3422                 var elem = this.getUIElement(section_id),
3423                     checked = elem ? elem.isChecked() : false;
3424                 return checked ? this.enabled : this.disabled;
3425         },
3426
3427         /**
3428          * Query the checked state of the underlying checkbox widget and return
3429          * either a localized `Yes` or `No` string, depending on the checked state.
3430          *
3431          * @override
3432          */
3433         textvalue: function(section_id) {
3434                 var cval = this.cfgvalue(section_id);
3435
3436                 if (cval == null)
3437                         cval = this.default;
3438
3439                 return (cval == this.enabled) ? _('Yes') : _('No');
3440         },
3441
3442         /** @override */
3443         parse: function(section_id) {
3444                 if (this.isActive(section_id)) {
3445                         var fval = this.formvalue(section_id);
3446
3447                         if (!this.isValid(section_id)) {
3448                                 var title = this.stripTags(this.title).trim();
3449                                 return Promise.reject(new TypeError(_('Option "%s" contains an invalid input value.').format(title || this.option)));
3450                         }
3451
3452                         if (fval == this.default && (this.optional || this.rmempty))
3453                                 return Promise.resolve(this.remove(section_id));
3454                         else
3455                                 return Promise.resolve(this.write(section_id, fval));
3456                 }
3457                 else {
3458                         return Promise.resolve(this.remove(section_id));
3459                 }
3460         },
3461 });
3462
3463 /**
3464  * @class MultiValue
3465  * @memberof LuCI.form
3466  * @augments LuCI.form.DynamicList
3467  * @hideconstructor
3468  * @classdesc
3469  *
3470  * The `MultiValue` class is a modified variant of the `DynamicList` element
3471  * which leverages the {@link LuCI.ui.Dropdown} widget to implement a multi
3472  * select dropdown element.
3473  *
3474  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3475  * The configuration form this section is added to. It is automatically passed
3476  * by [option()]{@link LuCI.form.AbstractSection#option} or
3477  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3478  * option to the section.
3479  *
3480  * @param {LuCI.form.AbstractSection} section
3481  * The configuration section this option is added to. It is automatically passed
3482  * by [option()]{@link LuCI.form.AbstractSection#option} or
3483  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3484  * option to the section.
3485  *
3486  * @param {string} option
3487  * The name of the UCI option to map.
3488  *
3489  * @param {string} [title]
3490  * The title caption of the option element.
3491  *
3492  * @param {string} [description]
3493  * The description text of the option element.
3494  */
3495 var CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.prototype */ {
3496         __name__: 'CBI.MultiValue',
3497
3498         __init__: function() {
3499                 this.super('__init__', arguments);
3500                 this.placeholder = _('-- Please choose --');
3501         },
3502
3503         /**
3504          * Allows to specify the [display_items]{@link LuCI.ui.Dropdown.InitOptions}
3505          * property of the underlying dropdown widget. If omitted, the value of
3506          * the `size` property is used or `3` when `size` is unspecified as well.
3507          *
3508          * @name LuCI.form.MultiValue.prototype#display_size
3509          * @type number
3510          * @default null
3511          */
3512
3513         /**
3514          * Allows to specify the [dropdown_items]{@link LuCI.ui.Dropdown.InitOptions}
3515          * property of the underlying dropdown widget. If omitted, the value of
3516          * the `size` property is used or `-1` when `size` is unspecified as well.
3517          *
3518          * @name LuCI.form.MultiValue.prototype#dropdown_size
3519          * @type number
3520          * @default null
3521          */
3522
3523         /** @private */
3524         renderWidget: function(section_id, option_index, cfgvalue) {
3525                 var value = (cfgvalue != null) ? cfgvalue : this.default,
3526                     choices = this.transformChoices();
3527
3528                 var widget = new ui.Dropdown(L.toArray(value), choices, {
3529                         id: this.cbid(section_id),
3530                         sort: this.keylist,
3531                         multiple: true,
3532                         optional: this.optional || this.rmempty,
3533                         select_placeholder: this.placeholder,
3534                         display_items: this.display_size || this.size || 3,
3535                         dropdown_items: this.dropdown_size || this.size || -1,
3536                         validate: L.bind(this.validate, this, section_id),
3537                         disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3538                 });
3539
3540                 return widget.render();
3541         },
3542 });
3543
3544 /**
3545  * @class TextValue
3546  * @memberof LuCI.form
3547  * @augments LuCI.form.Value
3548  * @hideconstructor
3549  * @classdesc
3550  *
3551  * The `TextValue` class implements a multi-line textarea input using
3552  * {@link LuCI.ui.Textarea}.
3553  *
3554  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3555  * The configuration form this section is added to. It is automatically passed
3556  * by [option()]{@link LuCI.form.AbstractSection#option} or
3557  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3558  * option to the section.
3559  *
3560  * @param {LuCI.form.AbstractSection} section
3561  * The configuration section this option is added to. It is automatically passed
3562  * by [option()]{@link LuCI.form.AbstractSection#option} or
3563  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3564  * option to the section.
3565  *
3566  * @param {string} option
3567  * The name of the UCI option to map.
3568  *
3569  * @param {string} [title]
3570  * The title caption of the option element.
3571  *
3572  * @param {string} [description]
3573  * The description text of the option element.
3574  */
3575 var CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ {
3576         __name__: 'CBI.TextValue',
3577
3578         /** @ignore */
3579         value: null,
3580
3581         /**
3582          * Enforces the use of a monospace font for the textarea contents when set
3583          * to `true`.
3584          *
3585          * @name LuCI.form.TextValue.prototype#monospace
3586          * @type boolean
3587          * @default false
3588          */
3589
3590         /**
3591          * Allows to specify the [cols]{@link LuCI.ui.Textarea.InitOptions}
3592          * property of the underlying textarea widget.
3593          *
3594          * @name LuCI.form.TextValue.prototype#cols
3595          * @type number
3596          * @default null
3597          */
3598
3599         /**
3600          * Allows to specify the [rows]{@link LuCI.ui.Textarea.InitOptions}
3601          * property of the underlying textarea widget.
3602          *
3603          * @name LuCI.form.TextValue.prototype#rows
3604          * @type number
3605          * @default null
3606          */
3607
3608         /**
3609          * Allows to specify the [wrap]{@link LuCI.ui.Textarea.InitOptions}
3610          * property of the underlying textarea widget.
3611          *
3612          * @name LuCI.form.TextValue.prototype#wrap
3613          * @type number
3614          * @default null
3615          */
3616
3617         /** @private */
3618         renderWidget: function(section_id, option_index, cfgvalue) {
3619                 var value = (cfgvalue != null) ? cfgvalue : this.default;
3620
3621                 var widget = new ui.Textarea(value, {
3622                         id: this.cbid(section_id),
3623                         optional: this.optional || this.rmempty,
3624                         placeholder: this.placeholder,
3625                         monospace: this.monospace,
3626                         cols: this.cols,
3627                         rows: this.rows,
3628                         wrap: this.wrap,
3629                         validate: L.bind(this.validate, this, section_id),
3630                         disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3631                 });
3632
3633                 return widget.render();
3634         }
3635 });
3636
3637 /**
3638  * @class DummyValue
3639  * @memberof LuCI.form
3640  * @augments LuCI.form.Value
3641  * @hideconstructor
3642  * @classdesc
3643  *
3644  * The `DummyValue` element wraps an {@link LuCI.ui.Hiddenfield} widget and
3645  * renders the underlying UCI option or default value as readonly text.
3646  *
3647  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3648  * The configuration form this section is added to. It is automatically passed
3649  * by [option()]{@link LuCI.form.AbstractSection#option} or
3650  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3651  * option to the section.
3652  *
3653  * @param {LuCI.form.AbstractSection} section
3654  * The configuration section this option is added to. It is automatically passed
3655  * by [option()]{@link LuCI.form.AbstractSection#option} or
3656  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3657  * option to the section.
3658  *
3659  * @param {string} option
3660  * The name of the UCI option to map.
3661  *
3662  * @param {string} [title]
3663  * The title caption of the option element.
3664  *
3665  * @param {string} [description]
3666  * The description text of the option element.
3667  */
3668 var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */ {
3669         __name__: 'CBI.DummyValue',
3670
3671         /**
3672          * Set an URL which is opened when clicking on the dummy value text.
3673          *
3674          * By setting this property, the dummy value text is wrapped in an `<a>`
3675          * element with the property value used as `href` attribute.
3676          *
3677          * @name LuCI.form.DummyValue.prototype#href
3678          * @type string
3679          * @default null
3680          */
3681
3682         /**
3683          * Treat the UCI option value (or the `default` property value) as HTML.
3684          *
3685          * By default, the value text is HTML escaped before being rendered as
3686          * text. In some cases it may be needed to actually interpret and render
3687          * HTML contents as-is. When set to `true`, HTML escaping is disabled.
3688          *
3689          * @name LuCI.form.DummyValue.prototype#rawhtml
3690          * @type boolean
3691          * @default null
3692          */
3693
3694         /** @private */
3695         renderWidget: function(section_id, option_index, cfgvalue) {
3696                 var value = (cfgvalue != null) ? cfgvalue : this.default,
3697                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
3698                     outputEl = E('div');
3699
3700                 if (this.href && !((this.readonly != null) ? this.readonly : this.map.readonly))
3701                         outputEl.appendChild(E('a', { 'href': this.href }));
3702
3703                 dom.append(outputEl.lastChild || outputEl,
3704                         this.rawhtml ? value : [ value ]);
3705
3706                 return E([
3707                         outputEl,
3708                         hiddenEl.render()
3709                 ]);
3710         },
3711
3712         /** @override */
3713         remove: function() {},
3714
3715         /** @override */
3716         write: function() {}
3717 });
3718
3719 /**
3720  * @class ButtonValue
3721  * @memberof LuCI.form
3722  * @augments LuCI.form.Value
3723  * @hideconstructor
3724  * @classdesc
3725  *
3726  * The `DummyValue` element wraps an {@link LuCI.ui.Hiddenfield} widget and
3727  * renders the underlying UCI option or default value as readonly text.
3728  *
3729  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3730  * The configuration form this section is added to. It is automatically passed
3731  * by [option()]{@link LuCI.form.AbstractSection#option} or
3732  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3733  * option to the section.
3734  *
3735  * @param {LuCI.form.AbstractSection} section
3736  * The configuration section this option is added to. It is automatically passed
3737  * by [option()]{@link LuCI.form.AbstractSection#option} or
3738  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3739  * option to the section.
3740  *
3741  * @param {string} option
3742  * The name of the UCI option to map.
3743  *
3744  * @param {string} [title]
3745  * The title caption of the option element.
3746  *
3747  * @param {string} [description]
3748  * The description text of the option element.
3749  */
3750 var CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype */ {
3751         __name__: 'CBI.ButtonValue',
3752
3753         /**
3754          * Override the rendered button caption.
3755          *
3756          * By default, the option title - which is passed as fourth argument to the
3757          * constructor - is used as caption for the button element. When setting
3758          * this property to a string, it is used as `String.format()` pattern with
3759          * the underlying UCI section name passed as first format argument. When
3760          * set to a function, it is invoked passing the section ID as sole argument
3761          * and the resulting return value is converted to a string before being
3762          * used as button caption.
3763          *
3764          * The default is `null`, means the option title is used as caption.
3765          *
3766          * @name LuCI.form.ButtonValue.prototype#inputtitle
3767          * @type string|function
3768          * @default null
3769          */
3770
3771         /**
3772          * Override the button style class.
3773          *
3774          * By setting this property, a specific `cbi-button-*` CSS class can be
3775          * selected to influence the style of the resulting button.
3776          *
3777          * Suitable values which are implemented by most themes are `positive`,
3778          * `negative` and `primary`.
3779          *
3780          * The default is `null`, means a neutral button styling is used.
3781          *
3782          * @name LuCI.form.ButtonValue.prototype#inputstyle
3783          * @type string
3784          * @default null
3785          */
3786
3787         /**
3788          * Override the button click action.
3789          *
3790          * By default, the underlying UCI option (or default property) value is
3791          * copied into a hidden field tied to the button element and the save
3792          * action is triggered on the parent form element.
3793          *
3794          * When this property is set to a function, it is invoked instead of
3795          * performing the default actions. The handler function will receive the
3796          * DOM click element as first and the underlying configuration section ID
3797          * as second argument.
3798          *
3799          * @name LuCI.form.ButtonValue.prototype#onclick
3800          * @type function
3801          * @default null
3802          */
3803
3804         /** @private */
3805         renderWidget: function(section_id, option_index, cfgvalue) {
3806                 var value = (cfgvalue != null) ? cfgvalue : this.default,
3807                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
3808                     outputEl = E('div'),
3809                     btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
3810
3811                 if (value !== false)
3812                         dom.content(outputEl, [
3813                                 E('button', {
3814                                         'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
3815                                         'click': ui.createHandlerFn(this, function(section_id, ev) {
3816                                                 if (this.onclick)
3817                                                         return this.onclick(ev, section_id);
3818
3819                                                 ev.currentTarget.parentNode.nextElementSibling.value = value;
3820                                                 return this.map.save();
3821                                         }, section_id),
3822                                         'disabled': ((this.readonly != null) ? this.readonly : this.map.readonly) || null
3823                                 }, [ btn_title ])
3824                         ]);
3825                 else
3826                         dom.content(outputEl, ' - ');
3827
3828                 return E([
3829                         outputEl,
3830                         hiddenEl.render()
3831                 ]);
3832         }
3833 });
3834
3835 /**
3836  * @class HiddenValue
3837  * @memberof LuCI.form
3838  * @augments LuCI.form.Value
3839  * @hideconstructor
3840  * @classdesc
3841  *
3842  * The `HiddenValue` element wraps an {@link LuCI.ui.Hiddenfield} widget.
3843  *
3844  * Hidden value widgets used to be necessary in legacy code which actually
3845  * submitted the underlying HTML form the server. With client side handling of
3846  * forms, there are more efficient ways to store hidden state data.
3847  *
3848  * Since this widget has no visible content, the title and description values
3849  * of this form element should be set to `null` as well to avoid a broken or
3850  * distorted form layout when rendering the option element.
3851  *
3852  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3853  * The configuration form this section is added to. It is automatically passed
3854  * by [option()]{@link LuCI.form.AbstractSection#option} or
3855  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3856  * option to the section.
3857  *
3858  * @param {LuCI.form.AbstractSection} section
3859  * The configuration section this option is added to. It is automatically passed
3860  * by [option()]{@link LuCI.form.AbstractSection#option} or
3861  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3862  * option to the section.
3863  *
3864  * @param {string} option
3865  * The name of the UCI option to map.
3866  *
3867  * @param {string} [title]
3868  * The title caption of the option element.
3869  *
3870  * @param {string} [description]
3871  * The description text of the option element.
3872  */
3873 var CBIHiddenValue = CBIValue.extend(/** @lends LuCI.form.HiddenValue.prototype */ {
3874         __name__: 'CBI.HiddenValue',
3875
3876         /** @private */
3877         renderWidget: function(section_id, option_index, cfgvalue) {
3878                 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
3879                         id: this.cbid(section_id)
3880                 });
3881
3882                 return widget.render();
3883         }
3884 });
3885
3886 /**
3887  * @class FileUpload
3888  * @memberof LuCI.form
3889  * @augments LuCI.form.Value
3890  * @hideconstructor
3891  * @classdesc
3892  *
3893  * The `FileUpload` element wraps an {@link LuCI.ui.FileUpload} widget and
3894  * offers the ability to browse, upload and select remote files.
3895  *
3896  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
3897  * The configuration form this section is added to. It is automatically passed
3898  * by [option()]{@link LuCI.form.AbstractSection#option} or
3899  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3900  * option to the section.
3901  *
3902  * @param {LuCI.form.AbstractSection} section
3903  * The configuration section this option is added to. It is automatically passed
3904  * by [option()]{@link LuCI.form.AbstractSection#option} or
3905  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
3906  * option to the section.
3907  *
3908  * @param {string} option
3909  * The name of the UCI option to map.
3910  *
3911  * @param {string} [title]
3912  * The title caption of the option element.
3913  *
3914  * @param {string} [description]
3915  * The description text of the option element.
3916  */
3917 var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */ {
3918         __name__: 'CBI.FileSelect',
3919
3920         __init__: function(/* ... */) {
3921                 this.super('__init__', arguments);
3922
3923                 this.show_hidden = false;
3924                 this.enable_upload = true;
3925                 this.enable_remove = true;
3926                 this.root_directory = '/etc/luci-uploads';
3927         },
3928
3929         /**
3930          * Toggle display of hidden files.
3931          *
3932          * Display hidden files when rendering the remote directory listing.
3933          * Note that this is merely a cosmetic feature, hidden files are always
3934          * included in received remote file listings.
3935          *
3936          * The default is `false`, means hidden files are not displayed.
3937          *
3938          * @name LuCI.form.FileUpload.prototype#show_hidden
3939          * @type boolean
3940          * @default false
3941          */
3942
3943         /**
3944          * Toggle file upload functionality.
3945          *
3946          * When set to `true`, the underlying widget provides a button which lets
3947          * the user select and upload local files to the remote system.
3948          * Note that this is merely a cosmetic feature, remote upload access is
3949          * controlled by the session ACL rules.
3950          *
3951          * The default is `true`, means file upload functionality is displayed.
3952          *
3953          * @name LuCI.form.FileUpload.prototype#enable_upload
3954          * @type boolean
3955          * @default true
3956          */
3957
3958         /**
3959          * Toggle remote file delete functionality.
3960          *
3961          * When set to `true`, the underlying widget provides a buttons which let
3962          * the user delete files from remote directories. Note that this is merely
3963          * a cosmetic feature, remote delete permissions are controlled by the
3964          * session ACL rules.
3965          *
3966          * The default is `true`, means file removal buttons are displayed.
3967          *
3968          * @name LuCI.form.FileUpload.prototype#enable_remove
3969          * @type boolean
3970          * @default true
3971          */
3972
3973         /**
3974          * Specify the root directory for file browsing.
3975          *
3976          * This property defines the topmost directory the file browser widget may
3977          * navigate to, the UI will not allow browsing directories outside this
3978          * prefix. Note that this is merely a cosmetic feature, remote file access
3979          * and directory listing permissions are controlled by the session ACL
3980          * rules.
3981          *
3982          * The default is `/etc/luci-uploads`.
3983          *
3984          * @name LuCI.form.FileUpload.prototype#root_directory
3985          * @type string
3986          * @default /etc/luci-uploads
3987          */
3988
3989         /** @private */
3990         renderWidget: function(section_id, option_index, cfgvalue) {
3991                 var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
3992                         id: this.cbid(section_id),
3993                         name: this.cbid(section_id),
3994                         show_hidden: this.show_hidden,
3995                         enable_upload: this.enable_upload,
3996                         enable_remove: this.enable_remove,
3997                         root_directory: this.root_directory,
3998                         disabled: (this.readonly != null) ? this.readonly : this.map.readonly
3999                 });
4000
4001                 return browserEl.render();
4002         }
4003 });
4004
4005 /**
4006  * @class SectionValue
4007  * @memberof LuCI.form
4008  * @augments LuCI.form.Value
4009  * @hideconstructor
4010  * @classdesc
4011  *
4012  * The `SectionValue` widget embeds a form section element within an option
4013  * element container, allowing to nest form sections into other sections.
4014  *
4015  * @param {LuCI.form.Map|LuCI.form.JSONMap} form
4016  * The configuration form this section is added to. It is automatically passed
4017  * by [option()]{@link LuCI.form.AbstractSection#option} or
4018  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
4019  * option to the section.
4020  *
4021  * @param {LuCI.form.AbstractSection} section
4022  * The configuration section this option is added to. It is automatically passed
4023  * by [option()]{@link LuCI.form.AbstractSection#option} or
4024  * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
4025  * option to the section.
4026  *
4027  * @param {string} option
4028  * The internal name of the option element holding the section. Since a section
4029  * container element does not read or write any configuration itself, the name
4030  * is only used internally and does not need to relate to any underlying UCI
4031  * option name.
4032  *
4033  * @param {LuCI.form.AbstractSection} subsection_class
4034  * The class to use for instantiating the nested section element. Note that
4035  * the class value itself is expected here, not a class instance obtained by
4036  * calling `new`. The given class argument must be a subclass of the
4037  * `AbstractSection` class.
4038  *
4039  * @param {...*} [class_args]
4040  * All further arguments are passed as-is to the subclass constructor. Refer
4041  * to the corresponding class constructor documentations for details.
4042  */
4043 var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototype */ {
4044         __name__: 'CBI.ContainerValue',
4045         __init__: function(map, section, option, cbiClass /*, ... */) {
4046                 this.super('__init__', [map, section, option]);
4047
4048                 if (!CBIAbstractSection.isSubclass(cbiClass))
4049                         throw 'Sub section must be a descendent of CBIAbstractSection';
4050
4051                 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
4052                 this.subsection.parentoption = this;
4053         },
4054
4055         /**
4056          * Access the embedded section instance.
4057          *
4058          * This property holds a reference to the instantiated nested section.
4059          *
4060          * @name LuCI.form.SectionValue.prototype#subsection
4061          * @type LuCI.form.AbstractSection
4062          * @readonly
4063          */
4064
4065         /** @override */
4066         load: function(section_id) {
4067                 return this.subsection.load();
4068         },
4069
4070         /** @override */
4071         parse: function(section_id) {
4072                 return this.subsection.parse();
4073         },
4074
4075         /** @private */
4076         renderWidget: function(section_id, option_index, cfgvalue) {
4077                 return this.subsection.render();
4078         },
4079
4080         /** @private */
4081         checkDepends: function(section_id) {
4082                 this.subsection.checkDepends();
4083                 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
4084         },
4085
4086         /**
4087          * Since the section container is not rendering an own widget,
4088          * its `value()` implementation is a no-op.
4089          *
4090          * @override
4091          */
4092         value: function() {},
4093
4094         /**
4095          * Since the section container is not tied to any UCI configuration,
4096          * its `write()` implementation is a no-op.
4097          *
4098          * @override
4099          */
4100         write: function() {},
4101
4102         /**
4103          * Since the section container is not tied to any UCI configuration,
4104          * its `remove()` implementation is a no-op.
4105          *
4106          * @override
4107          */
4108         remove: function() {},
4109
4110         /**
4111          * Since the section container is not tied to any UCI configuration,
4112          * its `cfgvalue()` implementation will always return `null`.
4113          *
4114          * @override
4115          * @returns {null}
4116          */
4117         cfgvalue: function() { return null },
4118
4119         /**
4120          * Since the section container is not tied to any UCI configuration,
4121          * its `formvalue()` implementation will always return `null`.
4122          *
4123          * @override
4124          * @returns {null}
4125          */
4126         formvalue: function() { return null }
4127 });
4128
4129 /**
4130  * @class form
4131  * @memberof LuCI
4132  * @hideconstructor
4133  * @classdesc
4134  *
4135  * The LuCI form class provides high level abstractions for creating creating
4136  * UCI- or JSON backed configurations forms.
4137  *
4138  * To import the class in views, use `'require form'`, to import it in
4139  * external JavaScript, use `L.require("form").then(...)`.
4140  *
4141  * A typical form is created by first constructing a
4142  * {@link LuCI.form.Map} or {@link LuCI.form.JSONMap} instance using `new` and
4143  * by subsequently adding sections and options to it. Finally
4144  * [render()]{@link LuCI.form.Map#render} is invoked on the instance to
4145  * assemble the HTML markup and insert it into the DOM.
4146  *
4147  * Example:
4148  *
4149  * <pre>
4150  * 'use strict';
4151  * 'require form';
4152  *
4153  * var m, s, o;
4154  *
4155  * m = new form.Map('example', 'Example form',
4156  *      'This is an example form mapping the contents of /etc/config/example');
4157  *
4158  * s = m.section(form.NamedSection, 'first_section', 'example', 'The first section',
4159  *      'This sections maps "config example first_section" of /etc/config/example');
4160  *
4161  * o = s.option(form.Flag, 'some_bool', 'A checkbox option');
4162  *
4163  * o = s.option(form.ListValue, 'some_choice', 'A select element');
4164  * o.value('choice1', 'The first choice');
4165  * o.value('choice2', 'The second choice');
4166  *
4167  * m.render().then(function(node) {
4168  *      document.body.appendChild(node);
4169  * });
4170  * </pre>
4171  */
4172 return baseclass.extend(/** @lends LuCI.form.prototype */ {
4173         Map: CBIMap,
4174         JSONMap: CBIJSONMap,
4175         AbstractSection: CBIAbstractSection,
4176         AbstractValue: CBIAbstractValue,
4177
4178         TypedSection: CBITypedSection,
4179         TableSection: CBITableSection,
4180         GridSection: CBIGridSection,
4181         NamedSection: CBINamedSection,
4182
4183         Value: CBIValue,
4184         DynamicList: CBIDynamicList,
4185         ListValue: CBIListValue,
4186         Flag: CBIFlagValue,
4187         MultiValue: CBIMultiValue,
4188         TextValue: CBITextValue,
4189         DummyValue: CBIDummyValue,
4190         Button: CBIButtonValue,
4191         HiddenValue: CBIHiddenValue,
4192         FileUpload: CBIFileUpload,
4193         SectionValue: CBISectionValue
4194 });