9c5cb9365044d890f26d5923f4ba318537cc54ec
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / form.js
1 'use strict';
2 'require ui';
3 'require uci';
4
5 var scope = this;
6
7 var CBINode = Class.extend({
8         __init__: function(title, description) {
9                 this.title = title || '';
10                 this.description = description || '';
11                 this.children = [];
12         },
13
14         append: function(obj) {
15                 this.children.push(obj);
16         },
17
18         parse: function() {
19                 var args = arguments;
20                 this.children.forEach(function(child) {
21                         child.parse.apply(child, args);
22                 });
23         },
24
25         render: function() {
26                 L.error('InternalError', 'Not implemented');
27         },
28
29         loadChildren: function(/* ... */) {
30                 var tasks = [];
31
32                 if (Array.isArray(this.children))
33                         for (var i = 0; i < this.children.length; i++)
34                                 if (!this.children[i].disable)
35                                         tasks.push(this.children[i].load.apply(this.children[i], arguments));
36
37                 return Promise.all(tasks);
38         },
39
40         renderChildren: function(tab_name /*, ... */) {
41                 var tasks = [],
42                     index = 0;
43
44                 if (Array.isArray(this.children))
45                         for (var i = 0; i < this.children.length; i++)
46                                 if (tab_name === null || this.children[i].tab === tab_name)
47                                         if (!this.children[i].disable)
48                                                 tasks.push(this.children[i].render.apply(
49                                                         this.children[i], this.varargs(arguments, 1, index++)));
50
51                 return Promise.all(tasks);
52         },
53
54         stripTags: function(s) {
55                 if (!s.match(/[<>]/))
56                         return s;
57
58                 var x = E('div', {}, s);
59                 return x.textContent || x.innerText || '';
60         },
61
62         titleFn: function(attr /*, ... */) {
63                 var s = null;
64
65                 if (typeof(this[attr]) == 'function')
66                         s = this[attr].apply(this, this.varargs(arguments, 1));
67                 else if (typeof(this[attr]) == 'string')
68                         s = (arguments.length > 1) ? ''.format.apply(this[attr], this.varargs(arguments, 1)) : this[attr];
69
70                 if (s != null)
71                         s = this.stripTags(String(s)).trim();
72
73                 if (s == null || s == '')
74                         return null;
75
76                 return s;
77         }
78 });
79
80 var CBIMap = CBINode.extend({
81         __init__: function(config /*, ... */) {
82                 this.super('__init__', this.varargs(arguments, 1));
83
84                 this.config = config;
85                 this.parsechain = [ config ];
86         },
87
88         findElements: function(/* ... */) {
89                 var q = null;
90
91                 if (arguments.length == 1)
92                         q = arguments[0];
93                 else if (arguments.length == 2)
94                         q = '[%s="%s"]'.format(arguments[0], arguments[1]);
95                 else
96                         L.error('InternalError', 'Expecting one or two arguments to findElements()');
97
98                 return this.root.querySelectorAll(q);
99         },
100
101         findElement: function(/* ... */) {
102                 var res = this.findElements.apply(this, arguments);
103                 return res.length ? res[0] : null;
104         },
105
106         chain: function(config) {
107                 if (this.parsechain.indexOf(config) == -1)
108                         this.parsechain.push(config);
109         },
110
111         section: function(cbiClass /*, ... */) {
112                 if (!CBIAbstractSection.isSubclass(cbiClass))
113                         L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
114
115                 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
116                 this.append(obj);
117                 return obj;
118         },
119
120         load: function() {
121                 return uci.load(this.parsechain || [ this.config ])
122                         .then(this.loadChildren.bind(this));
123         },
124
125         parse: function() {
126                 var tasks = [];
127
128                 if (Array.isArray(this.children))
129                         for (var i = 0; i < this.children.length; i++)
130                                 tasks.push(this.children[i].parse());
131
132                 return Promise.all(tasks);
133         },
134
135         save: function() {
136                 this.checkDepends();
137
138                 return this.parse()
139                         .then(uci.save.bind(uci))
140                         .then(this.load.bind(this))
141                         .then(this.renderContents.bind(this))
142                         .catch(function(e) {
143                                 alert('Cannot save due to invalid values')
144                                 return Promise.reject();
145                         });
146         },
147
148         reset: function() {
149                 return this.renderContents();
150         },
151
152         render: function() {
153                 return this.load().then(this.renderContents.bind(this));
154         },
155
156         renderContents: function() {
157                 var mapEl = this.root || (this.root = E('div', {
158                         'id': 'cbi-%s'.format(this.config),
159                         'class': 'cbi-map',
160                         'cbi-dependency-check': L.bind(this.checkDepends, this)
161                 }));
162
163                 L.dom.bindClassInstance(mapEl, this);
164
165                 return this.renderChildren(null).then(L.bind(function(nodes) {
166                         var initialRender = !mapEl.firstChild;
167
168                         L.dom.content(mapEl, null);
169
170                         if (this.title != null && this.title != '')
171                                 mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
172
173                         if (this.description != null && this.description != '')
174                                 mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
175
176                         if (this.tabbed)
177                                 L.dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
178                         else
179                                 L.dom.append(mapEl, nodes);
180
181                         if (!initialRender) {
182                                 mapEl.classList.remove('flash');
183
184                                 window.setTimeout(function() {
185                                         mapEl.classList.add('flash');
186                                 }, 1);
187                         }
188
189                         this.checkDepends();
190
191                         var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
192
193                         for (var i = 0; i < tabGroups.length; i++)
194                                 ui.tabs.initTabGroup(tabGroups[i].childNodes);
195
196                         return mapEl;
197                 }, this));
198         },
199
200         lookupOption: function(name, section_id, config_name) {
201                 var id, elem, sid, inst;
202
203                 if (name.indexOf('.') > -1)
204                         id = 'cbid.%s'.format(name);
205                 else
206                         id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
207
208                 elem = this.findElement('data-field', id);
209                 sid  = elem ? id.split(/\./)[2] : null;
210                 inst = elem ? L.dom.findClassInstance(elem) : null;
211
212                 return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
213         },
214
215         checkDepends: function(ev, n) {
216                 var changed = false;
217
218                 for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
219                         if (s.checkDepends(ev, n))
220                                 changed = true;
221
222                 if (changed && (n || 0) < 10)
223                         this.checkDepends(ev, (n || 10) + 1);
224
225                 ui.tabs.updateTabs(ev, this.root);
226         }
227 });
228
229 var CBIAbstractSection = CBINode.extend({
230         __init__: function(map, sectionType /*, ... */) {
231                 this.super('__init__', this.varargs(arguments, 2));
232
233                 this.sectiontype = sectionType;
234                 this.map = map;
235                 this.config = map.config;
236
237                 this.optional = true;
238                 this.addremove = false;
239                 this.dynamic = false;
240         },
241
242         cfgsections: function() {
243                 L.error('InternalError', 'Not implemented');
244         },
245
246         filter: function(section_id) {
247                 return true;
248         },
249
250         load: function() {
251                 var section_ids = this.cfgsections(),
252                     tasks = [];
253
254                 if (Array.isArray(this.children))
255                         for (var i = 0; i < section_ids.length; i++)
256                                 tasks.push(this.loadChildren(section_ids[i])
257                                         .then(Function.prototype.bind.call(function(section_id, set_values) {
258                                                 for (var i = 0; i < set_values.length; i++)
259                                                         this.children[i].cfgvalue(section_id, set_values[i]);
260                                         }, this, section_ids[i])));
261
262                 return Promise.all(tasks);
263         },
264
265         parse: function() {
266                 var section_ids = this.cfgsections(),
267                     tasks = [];
268
269                 if (Array.isArray(this.children))
270                         for (var i = 0; i < section_ids.length; i++)
271                                 for (var j = 0; j < this.children.length; j++)
272                                         tasks.push(this.children[j].parse(section_ids[i]));
273
274                 return Promise.all(tasks);
275         },
276
277         tab: function(name, title, description) {
278                 if (this.tabs && this.tabs[name])
279                         throw 'Tab already declared';
280
281                 var entry = {
282                         name: name,
283                         title: title,
284                         description: description,
285                         children: []
286                 };
287
288                 this.tabs = this.tabs || [];
289                 this.tabs.push(entry);
290                 this.tabs[name] = entry;
291
292                 this.tab_names = this.tab_names || [];
293                 this.tab_names.push(name);
294         },
295
296         option: function(cbiClass /*, ... */) {
297                 if (!CBIAbstractValue.isSubclass(cbiClass))
298                         throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
299
300                 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
301                 this.append(obj);
302                 return obj;
303         },
304
305         taboption: function(tabName /*, ... */) {
306                 if (!this.tabs || !this.tabs[tabName])
307                         throw L.error('ReferenceError', 'Associated tab not declared');
308
309                 var obj = this.option.apply(this, this.varargs(arguments, 1));
310                 obj.tab = tabName;
311                 this.tabs[tabName].children.push(obj);
312                 return obj;
313         },
314
315         renderUCISection: function(section_id) {
316                 var renderTasks = [];
317
318                 if (!this.tabs)
319                         return this.renderOptions(null, section_id);
320
321                 for (var i = 0; i < this.tab_names.length; i++)
322                         renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
323
324                 return Promise.all(renderTasks)
325                         .then(this.renderTabContainers.bind(this, section_id));
326         },
327
328         renderTabContainers: function(section_id, nodes) {
329                 var config_name = this.uciconfig || this.map.config,
330                     containerEls = E([]);
331
332                 for (var i = 0; i < nodes.length; i++) {
333                         var tab_name = this.tab_names[i],
334                             tab_data = this.tabs[tab_name],
335                             containerEl = E('div', {
336                                 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
337                                 'data-tab': tab_name,
338                                 'data-tab-title': tab_data.title,
339                                 'data-tab-active': tab_name === this.selected_tab
340                             });
341
342                         if (tab_data.description != null && tab_data.description != '')
343                                 containerEl.appendChild(
344                                         E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
345
346                         containerEl.appendChild(nodes[i]);
347                         containerEls.appendChild(containerEl);
348                 }
349
350                 return containerEls;
351         },
352
353         renderOptions: function(tab_name, section_id) {
354                 var in_table = (this instanceof CBITableSection);
355                 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
356                         var optionEls = E([]);
357                         for (var i = 0; i < nodes.length; i++)
358                                 optionEls.appendChild(nodes[i]);
359                         return optionEls;
360                 });
361         },
362
363         checkDepends: function(ev, n) {
364                 var changed = false,
365                     sids = this.cfgsections();
366
367                 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
368                         for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
369                                 var isActive = o.isActive(sid),
370                                     isSatisified = o.checkDepends(sid);
371
372                                 if (isActive != isSatisified) {
373                                         o.setActive(sid, !isActive);
374                                         changed = true;
375                                 }
376
377                                 if (!n && isActive)
378                                         o.triggerValidation(sid);
379                         }
380                 }
381
382                 return changed;
383         }
384 });
385
386
387 var isEqual = function(x, y) {
388         if (x != null && y != null && typeof(x) != typeof(y))
389                 return false;
390
391         if ((x == null && y != null) || (x != null && y == null))
392                 return false;
393
394         if (Array.isArray(x)) {
395                 if (x.length != y.length)
396                         return false;
397
398                 for (var i = 0; i < x.length; i++)
399                         if (!isEqual(x[i], y[i]))
400                                 return false;
401         }
402         else if (typeof(x) == 'object') {
403                 for (var k in x) {
404                         if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
405                                 return false;
406
407                         if (!isEqual(x[k], y[k]))
408                                 return false;
409                 }
410
411                 for (var k in y)
412                         if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
413                                 return false;
414         }
415         else if (x != y) {
416                 return false;
417         }
418
419         return true;
420 };
421
422 var CBIAbstractValue = CBINode.extend({
423         __init__: function(map, section, option /*, ... */) {
424                 this.super('__init__', this.varargs(arguments, 3));
425
426                 this.section = section;
427                 this.option = option;
428                 this.map = map;
429                 this.config = map.config;
430
431                 this.deps = [];
432                 this.initial = {};
433                 this.rmempty = true;
434                 this.default = null;
435                 this.size = null;
436                 this.optional = false;
437         },
438
439         depends: function(field, value) {
440                 var deps;
441
442                 if (typeof(field) === 'string')
443                         deps = {}, deps[field] = value;
444                 else
445                         deps = field;
446
447                 this.deps.push(deps);
448         },
449
450         transformDepList: function(section_id, deplist) {
451                 var list = deplist || this.deps,
452                     deps = [];
453
454                 if (Array.isArray(list)) {
455                         for (var i = 0; i < list.length; i++) {
456                                 var dep = {};
457
458                                 for (var k in list[i]) {
459                                         if (list[i].hasOwnProperty(k)) {
460                                                 if (k.charAt(0) === '!')
461                                                         dep[k] = list[i][k];
462                                                 else if (k.indexOf('.') !== -1)
463                                                         dep['cbid.%s'.format(k)] = list[i][k];
464                                                 else
465                                                         dep['cbid.%s.%s.%s'.format(
466                                                                 this.uciconfig || this.section.uciconfig || this.map.config,
467                                                                 this.ucisection || section_id,
468                                                                 k
469                                                         )] = list[i][k];
470                                         }
471                                 }
472
473                                 for (var k in dep) {
474                                         if (dep.hasOwnProperty(k)) {
475                                                 deps.push(dep);
476                                                 break;
477                                         }
478                                 }
479                         }
480                 }
481
482                 return deps;
483         },
484
485         transformChoices: function() {
486                 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
487                         return null;
488
489                 var choices = {};
490
491                 for (var i = 0; i < this.keylist.length; i++)
492                         choices[this.keylist[i]] = this.vallist[i];
493
494                 return choices;
495         },
496
497         checkDepends: function(section_id) {
498                 var def = false;
499
500                 if (!Array.isArray(this.deps) || !this.deps.length)
501                         return true;
502
503                 for (var i = 0; i < this.deps.length; i++) {
504                         var istat = true,
505                             reverse = false;
506
507                         for (var dep in this.deps[i]) {
508                                 if (dep == '!reverse') {
509                                         reverse = true;
510                                 }
511                                 else if (dep == '!default') {
512                                         def = true;
513                                         istat = false;
514                                 }
515                                 else {
516                                         var conf = this.uciconfig || this.section.uciconfig || this.map.config,
517                                             res = this.map.lookupOption(dep, section_id, conf),
518                                             val = res ? res[0].formvalue(res[1]) : null;
519
520                                         istat = (istat && isEqual(val, this.deps[i][dep]));
521                                 }
522                         }
523
524                         if (istat ^ reverse)
525                                 return true;
526                 }
527
528                 return def;
529         },
530
531         cbid: function(section_id) {
532                 if (section_id == null)
533                         L.error('TypeError', 'Section ID required');
534
535                 return 'cbid.%s.%s.%s'.format(
536                         this.uciconfig || this.section.uciconfig || this.map.config,
537                         section_id, this.option);
538         },
539
540         load: function(section_id) {
541                 if (section_id == null)
542                         L.error('TypeError', 'Section ID required');
543
544                 return uci.get(
545                         this.uciconfig || this.section.uciconfig || this.map.config,
546                         this.ucisection || section_id,
547                         this.ucioption || this.option);
548         },
549
550         cfgvalue: function(section_id, set_value) {
551                 if (section_id == null)
552                         L.error('TypeError', 'Section ID required');
553
554                 if (arguments.length == 2) {
555                         this.data = this.data || {};
556                         this.data[section_id] = set_value;
557                 }
558
559                 return this.data ? this.data[section_id] : null;
560         },
561
562         formvalue: function(section_id) {
563                 var node = this.map.findElement('id', this.cbid(section_id));
564                 return node ? L.dom.callClassMethod(node, 'getValue') : null;
565         },
566
567         textvalue: function(section_id) {
568                 var cval = this.cfgvalue(section_id);
569
570                 if (cval == null)
571                         cval = this.default;
572
573                 return (cval != null) ? '%h'.format(cval) : null;
574         },
575
576         validate: function(section_id, value) {
577                 return true;
578         },
579
580         isValid: function(section_id) {
581                 var node = this.map.findElement('id', this.cbid(section_id));
582                 return node ? L.dom.callClassMethod(node, 'isValid') : true;
583         },
584
585         isActive: function(section_id) {
586                 var field = this.map.findElement('data-field', this.cbid(section_id));
587                 return (field != null && !field.classList.contains('hidden'));
588         },
589
590         setActive: function(section_id, active) {
591                 var field = this.map.findElement('data-field', this.cbid(section_id));
592
593                 if (field && field.classList.contains('hidden') == active) {
594                         field.classList[active ? 'remove' : 'add']('hidden');
595                         return true;
596                 }
597
598                 return false;
599         },
600
601         triggerValidation: function(section_id) {
602                 var node = this.map.findElement('id', this.cbid(section_id));
603                 return node ? L.dom.callClassMethod(node, 'triggerValidation') : true;
604         },
605
606         parse: function(section_id) {
607                 var active = this.isActive(section_id),
608                     cval = this.cfgvalue(section_id),
609                     fval = active ? this.formvalue(section_id) : null;
610
611                 if (active && !this.isValid(section_id))
612                         return Promise.reject();
613
614                 if (fval != '' && fval != null) {
615                         if (this.forcewrite || !isEqual(cval, fval))
616                                 return Promise.resolve(this.write(section_id, fval));
617                 }
618                 else {
619                         if (this.rmempty || this.optional) {
620                                 return Promise.resolve(this.remove(section_id));
621                         }
622                         else if (!isEqual(cval, fval)) {
623                                 console.log('This should have been catched by isValid()');
624                                 return Promise.reject();
625                         }
626                 }
627
628                 return Promise.resolve();
629         },
630
631         write: function(section_id, formvalue) {
632                 return uci.set(
633                         this.uciconfig || this.section.uciconfig || this.map.config,
634                         this.ucisection || section_id,
635                         this.ucioption || this.option,
636                         formvalue);
637         },
638
639         remove: function(section_id) {
640                 return uci.unset(
641                         this.uciconfig || this.section.uciconfig || this.map.config,
642                         this.ucisection || section_id,
643                         this.ucioption || this.option);
644         }
645 });
646
647 var CBITypedSection = CBIAbstractSection.extend({
648         __name__: 'CBI.TypedSection',
649
650         cfgsections: function() {
651                 return uci.sections(this.uciconfig || this.map.config, this.sectiontype)
652                         .map(function(s) { return s['.name'] })
653                         .filter(L.bind(this.filter, this));
654         },
655
656         handleAdd: function(ev, name) {
657                 var config_name = this.uciconfig || this.map.config;
658
659                 uci.add(config_name, this.sectiontype, name);
660                 this.map.save();
661         },
662
663         handleRemove: function(section_id, ev) {
664                 var config_name = this.uciconfig || this.map.config;
665
666                 uci.remove(config_name, section_id);
667                 this.map.save();
668         },
669
670         renderSectionAdd: function(extra_class) {
671                 if (!this.addremove)
672                         return E([]);
673
674                 var createEl = E('div', { 'class': 'cbi-section-create' }),
675                     config_name = this.uciconfig || this.map.config,
676                     btn_title = this.titleFn('addbtntitle');
677
678                 if (extra_class != null)
679                         createEl.classList.add(extra_class);
680
681                 if (this.anonymous) {
682                         createEl.appendChild(E('input', {
683                                 'type': 'submit',
684                                 'class': 'cbi-button cbi-button-add',
685                                 'value': btn_title || _('Add'),
686                                 'title': btn_title || _('Add'),
687                                 'click': L.bind(this.handleAdd, this)
688                         }));
689                 }
690                 else {
691                         var nameEl = E('input', {
692                                 'type': 'text',
693                                 'class': 'cbi-section-create-name'
694                         });
695
696                         L.dom.append(createEl, [
697                                 E('div', {}, nameEl),
698                                 E('input', {
699                                         'class': 'cbi-button cbi-button-add',
700                                         'type': 'submit',
701                                         'value': btn_title || _('Add'),
702                                         'title': btn_title || _('Add'),
703                                         'click': L.bind(function(ev) {
704                                                 if (nameEl.classList.contains('cbi-input-invalid'))
705                                                         return;
706
707                                                 this.handleAdd(ev, nameEl.value);
708                                         }, this)
709                                 })
710                         ]);
711
712                         ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
713                 }
714
715                 return createEl;
716         },
717
718         renderSectionPlaceholder: function() {
719                 return E([
720                         E('em', _('This section contains no values yet')),
721                         E('br'), E('br')
722                 ]);
723         },
724
725         renderContents: function(cfgsections, nodes) {
726                 var section_id = null,
727                     config_name = this.uciconfig || this.map.config,
728                     sectionEl = E('div', {
729                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
730                                 'class': 'cbi-section',
731                                 'data-tab': this.map.tabbed ? this.sectiontype : null,
732                                 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
733                         });
734
735                 if (this.title != null && this.title != '')
736                         sectionEl.appendChild(E('legend', {}, this.title));
737
738                 if (this.description != null && this.description != '')
739                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
740
741                 for (var i = 0; i < nodes.length; i++) {
742                         if (this.addremove) {
743                                 sectionEl.appendChild(
744                                         E('div', { 'class': 'cbi-section-remove right' },
745                                                 E('input', {
746                                                         'type': 'submit',
747                                                         'class': 'cbi-button',
748                                                         'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
749                                                         'value': _('Delete'),
750                                                         'data-section-id': cfgsections[i],
751                                                         'click': L.bind(this.handleRemove, this, cfgsections[i])
752                                                 })));
753                         }
754
755                         if (!this.anonymous)
756                                 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
757
758                         sectionEl.appendChild(E('div', {
759                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
760                                 'class': this.tabs
761                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
762                                 'data-section-id': cfgsections[i]
763                         }, nodes[i]));
764                 }
765
766                 if (nodes.length == 0)
767                         sectionEl.appendChild(this.renderSectionPlaceholder());
768
769                 sectionEl.appendChild(this.renderSectionAdd());
770
771                 L.dom.bindClassInstance(sectionEl, this);
772
773                 return sectionEl;
774         },
775
776         render: function() {
777                 var cfgsections = this.cfgsections(),
778                     renderTasks = [];
779
780                 for (var i = 0; i < cfgsections.length; i++)
781                         renderTasks.push(this.renderUCISection(cfgsections[i]));
782
783                 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
784         }
785 });
786
787 var CBITableSection = CBITypedSection.extend({
788         __name__: 'CBI.TableSection',
789
790         tab: function() {
791                 throw 'Tabs are not supported by TableSection';
792         },
793
794         renderContents: function(cfgsections, nodes) {
795                 var section_id = null,
796                     config_name = this.uciconfig || this.map.config,
797                     max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
798                     has_more = max_cols < this.children.length,
799                     sectionEl = E('div', {
800                                 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
801                                 'class': 'cbi-section cbi-tblsection',
802                                 'data-tab': this.map.tabbed ? this.sectiontype : null,
803                                 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
804                         }),
805                         tableEl = E('div', {
806                                 'class': 'table cbi-section-table'
807                         });
808
809                 if (this.title != null && this.title != '')
810                         sectionEl.appendChild(E('h3', {}, this.title));
811
812                 if (this.description != null && this.description != '')
813                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
814
815                 tableEl.appendChild(this.renderHeaderRows(max_cols));
816
817                 for (var i = 0; i < nodes.length; i++) {
818                         var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
819
820                         var trEl = E('div', {
821                                 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
822                                 'class': 'tr cbi-section-table-row',
823                                 'data-sid': cfgsections[i],
824                                 'draggable': this.sortable ? true : null,
825                                 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
826                                 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
827                                 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
828                                 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
829                                 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
830                                 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
831                                 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
832                                 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
833                                 'data-section-id': cfgsections[i]
834                         });
835
836                         if (this.extedit || this.rowcolors)
837                                 trEl.classList.add(!(tableEl.childNodes.length % 2)
838                                         ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
839
840                         for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
841                                 trEl.appendChild(nodes[i].firstChild);
842
843                         trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
844                         tableEl.appendChild(trEl);
845                 }
846
847                 if (nodes.length == 0)
848                         tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
849                                 E('div', { 'class': 'td' },
850                                         E('em', {}, _('This section contains no values yet')))));
851
852                 sectionEl.appendChild(tableEl);
853
854                 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
855
856                 L.dom.bindClassInstance(sectionEl, this);
857
858                 return sectionEl;
859         },
860
861         renderHeaderRows: function(max_cols) {
862                 var has_titles = false,
863                     has_descriptions = false,
864                     anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
865                     trEls = E([]);
866
867                 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
868                         if (opt.optional || opt.modalonly)
869                                 continue;
870
871                         has_titles = has_titles || !!opt.title;
872                         has_descriptions = has_descriptions || !!opt.description;
873                 }
874
875                 if (has_titles) {
876                         var trEl = E('div', {
877                                 'class': 'tr cbi-section-table-titles ' + anon_class,
878                                 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
879                         });
880
881                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
882                                 if (opt.optional || opt.modalonly)
883                                         continue;
884
885                                 trEl.appendChild(E('div', {
886                                         'class': 'th cbi-section-table-cell',
887                                         'data-type': opt.__name__
888                                 }));
889
890                                 if (opt.width != null)
891                                         trEl.lastElementChild.style.width =
892                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
893
894                                 if (opt.titleref)
895                                         trEl.lastElementChild.appendChild(E('a', {
896                                                 'href': opt.titleref,
897                                                 'class': 'cbi-title-ref',
898                                                 'title': this.titledesc || _('Go to relevant configuration page')
899                                         }, opt.title));
900                                 else
901                                         L.dom.content(trEl.lastElementChild, opt.title);
902                         }
903
904                         if (this.sortable || this.extedit || this.addremove || has_more)
905                                 trEl.appendChild(E('div', {
906                                         'class': 'th cbi-section-table-cell cbi-section-actions'
907                                 }));
908
909                         trEls.appendChild(trEl);
910                 }
911
912                 if (has_descriptions) {
913                         var trEl = E('div', {
914                                 'class': 'tr cbi-section-table-descr ' + anon_class
915                         });
916
917                         for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
918                                 if (opt.optional || opt.modalonly)
919                                         continue;
920
921                                 trEl.appendChild(E('div', {
922                                         'class': 'th cbi-section-table-cell',
923                                         'data-type': opt.__name__
924                                 }, opt.description));
925
926                                 if (opt.width != null)
927                                         trEl.lastElementChild.style.width =
928                                                 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
929                         }
930
931                         if (this.sortable || this.extedit || this.addremove || has_more)
932                                 trEl.appendChild(E('div', {
933                                         'class': 'th cbi-section-table-cell cbi-section-actions'
934                                 }));
935
936                         trEls.appendChild(trEl);
937                 }
938
939                 return trEls;
940         },
941
942         renderRowActions: function(section_id, more_label) {
943                 var config_name = this.uciconfig || this.map.config;
944
945                 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
946                         return E([]);
947
948                 var tdEl = E('div', {
949                         'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
950                 }, E('div'));
951
952                 if (this.sortable) {
953                         L.dom.append(tdEl.lastElementChild, [
954                                 E('div', {
955                                         'title': _('Drag to reorder'),
956                                         'class': 'cbi-button drag-handle center',
957                                         'style': 'cursor:move'
958                                 }, '☰')
959                         ]);
960                 }
961
962                 if (this.extedit) {
963                         var evFn = null;
964
965                         if (typeof(this.extedit) == 'function')
966                                 evFn = L.bind(this.extedit, this);
967                         else if (typeof(this.extedit) == 'string')
968                                 evFn = L.bind(function(sid, ev) {
969                                         location.href = this.extedit.format(sid);
970                                 }, this, section_id);
971
972                         L.dom.append(tdEl.lastElementChild,
973                                 E('input', {
974                                         'type': 'button',
975                                         'value': _('Edit'),
976                                         'title': _('Edit'),
977                                         'class': 'cbi-button cbi-button-edit',
978                                         'click': evFn
979                                 })
980                         );
981                 }
982
983                 if (more_label) {
984                         L.dom.append(tdEl.lastElementChild,
985                                 E('input', {
986                                         'type': 'button',
987                                         'value': more_label,
988                                         'title': more_label,
989                                         'class': 'cbi-button cbi-button-edit',
990                                         'click': L.bind(this.renderMoreOptionsModal, this, section_id)
991                                 })
992                         );
993                 }
994
995                 if (this.addremove) {
996                         var btn_title = this.titleFn('removebtntitle', section_id);
997
998                         L.dom.append(tdEl.lastElementChild,
999                                 E('input', {
1000                                         'type': 'submit',
1001                                         'value': btn_title || _('Delete'),
1002                                         'title': btn_title || _('Delete'),
1003                                         'class': 'cbi-button cbi-button-remove',
1004                                         'click': L.bind(function(sid, ev) {
1005                                                 uci.remove(config_name, sid);
1006                                                 this.map.save();
1007                                         }, this, section_id)
1008                                 })
1009                         );
1010                 }
1011
1012                 return tdEl;
1013         },
1014
1015         handleDragInit: function(ev) {
1016                 scope.dragState = { node: ev.target };
1017         },
1018
1019         handleDragStart: function(ev) {
1020                 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
1021                         scope.dragState = null;
1022                         ev.preventDefault();
1023                         return false;
1024                 }
1025
1026                 scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
1027                 ev.dataTransfer.setData('text', 'drag');
1028                 ev.target.style.opacity = 0.4;
1029         },
1030
1031         handleDragOver: function(ev) {
1032                 var n = scope.dragState.targetNode,
1033                     r = scope.dragState.rect,
1034                     t = r.top + r.height / 2;
1035
1036                 if (ev.clientY <= t) {
1037                         n.classList.remove('drag-over-below');
1038                         n.classList.add('drag-over-above');
1039                 }
1040                 else {
1041                         n.classList.remove('drag-over-above');
1042                         n.classList.add('drag-over-below');
1043                 }
1044
1045                 ev.dataTransfer.dropEffect = 'move';
1046                 ev.preventDefault();
1047                 return false;
1048         },
1049
1050         handleDragEnter: function(ev) {
1051                 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
1052                 scope.dragState.targetNode = ev.currentTarget;
1053         },
1054
1055         handleDragLeave: function(ev) {
1056                 ev.currentTarget.classList.remove('drag-over-above');
1057                 ev.currentTarget.classList.remove('drag-over-below');
1058         },
1059
1060         handleDragEnd: function(ev) {
1061                 var n = ev.target;
1062
1063                 n.style.opacity = '';
1064                 n.classList.add('flash');
1065                 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
1066                         .forEach(function(tr) {
1067                                 tr.classList.remove('drag-over-above');
1068                                 tr.classList.remove('drag-over-below');
1069                         });
1070         },
1071
1072         handleDrop: function(ev) {
1073                 var s = scope.dragState;
1074
1075                 if (s.node && s.targetNode) {
1076                         var config_name = this.uciconfig || this.map.config,
1077                             ref_node = s.targetNode,
1078                             after = false;
1079
1080                     if (ref_node.classList.contains('drag-over-below')) {
1081                         ref_node = ref_node.nextElementSibling;
1082                         after = true;
1083                     }
1084
1085                     var sid1 = s.node.getAttribute('data-sid'),
1086                         sid2 = s.targetNode.getAttribute('data-sid');
1087
1088                     s.node.parentNode.insertBefore(s.node, ref_node);
1089                     uci.move(config_name, sid1, sid2, after);
1090                 }
1091
1092                 scope.dragState = null;
1093                 ev.target.style.opacity = '';
1094                 ev.stopPropagation();
1095                 ev.preventDefault();
1096                 return false;
1097         },
1098
1099         handleModalCancel: function(modalMap, ev) {
1100                 return Promise.resolve(L.ui.hideModal());
1101         },
1102
1103         handleModalSave: function(modalMap, ev) {
1104                 return modalMap.save()
1105                         .then(L.bind(this.map.load, this.map))
1106                         .then(L.bind(this.map.reset, this.map))
1107                         .then(L.ui.hideModal)
1108                         .catch(function() {});
1109         },
1110
1111         addModalOptions: function(modalSection, section_id, ev) {
1112
1113         },
1114
1115         renderMoreOptionsModal: function(section_id, ev) {
1116                 var parent = this.map,
1117                     title = parent.title,
1118                     name = null,
1119                     m = new CBIMap(this.map.config, null, null),
1120                     s = m.section(CBINamedSection, section_id, this.sectiontype);
1121
1122                 s.tabs = this.tabs;
1123                 s.tab_names = this.tab_names;
1124
1125                 if ((name = this.titleFn('modaltitle', section_id)) != null)
1126                         title = name;
1127                 else if ((name = this.titleFn('sectiontitle', section_id)) != null)
1128                         title = '%s - %s'.format(parent.title, name);
1129                 else if (!this.anonymous)
1130                         title = '%s - %s'.format(parent.title, section_id);
1131
1132                 for (var i = 0; i < this.children.length; i++) {
1133                         var o1 = this.children[i];
1134
1135                         if (o1.modalonly === false)
1136                                 continue;
1137
1138                         var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
1139
1140                         for (var k in o1) {
1141                                 if (!o1.hasOwnProperty(k))
1142                                         continue;
1143
1144                                 switch (k) {
1145                                 case 'map':
1146                                 case 'section':
1147                                 case 'option':
1148                                 case 'title':
1149                                 case 'description':
1150                                         continue;
1151
1152                                 default:
1153                                         o2[k] = o1[k];
1154                                 }
1155                         }
1156                 }
1157
1158                 //ev.target.classList.add('spinning');
1159                 Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
1160                         //ev.target.classList.remove('spinning');
1161                         L.ui.showModal(title, [
1162                                 nodes,
1163                                 E('div', { 'class': 'right' }, [
1164                                         E('input', {
1165                                                 'type': 'button',
1166                                                 'class': 'btn',
1167                                                 'click': L.bind(this.handleModalCancel, this, m),
1168                                                 'value': _('Dismiss')
1169                                         }), ' ',
1170                                         E('input', {
1171                                                 'type': 'button',
1172                                                 'class': 'cbi-button cbi-button-positive important',
1173                                                 'click': L.bind(this.handleModalSave, this, m),
1174                                                 'value': _('Save')
1175                                         })
1176                                 ])
1177                         ], 'cbi-modal');
1178                 }, this)).catch(L.error);
1179         }
1180 });
1181
1182 var CBIGridSection = CBITableSection.extend({
1183         tab: function(name, title, description) {
1184                 CBIAbstractSection.prototype.tab.call(this, name, title, description);
1185         },
1186
1187         handleAdd: function(ev) {
1188                 var config_name = this.uciconfig || this.map.config,
1189                     section_id = uci.add(config_name, this.sectiontype);
1190
1191             this.addedSection = section_id;
1192                 this.renderMoreOptionsModal(section_id);
1193         },
1194
1195         handleModalSave: function(/* ... */) {
1196                 return this.super('handleModalSave', arguments)
1197                         .then(L.bind(function() { this.addedSection = null }, this));
1198         },
1199
1200         handleModalCancel: function(/* ... */) {
1201                 var config_name = this.uciconfig || this.map.config;
1202
1203                 if (this.addedSection != null) {
1204                         uci.remove(config_name, this.addedSection);
1205                         this.addedSection = null;
1206                 }
1207
1208                 return this.super('handleModalCancel', arguments);
1209         },
1210
1211         renderUCISection: function(section_id) {
1212                 return this.renderOptions(null, section_id);
1213         },
1214
1215         renderChildren: function(tab_name, section_id, in_table) {
1216                 var tasks = [], index = 0;
1217
1218                 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
1219                         if (opt.disable || opt.modalonly)
1220                                 continue;
1221
1222                         if (opt.editable)
1223                                 tasks.push(opt.render(index++, section_id, in_table));
1224                         else
1225                                 tasks.push(this.renderTextValue(section_id, opt));
1226                 }
1227
1228                 return Promise.all(tasks);
1229         },
1230
1231         renderTextValue: function(section_id, opt) {
1232                 var title = this.stripTags(opt.title).trim(),
1233                     descr = this.stripTags(opt.description).trim(),
1234                     value = opt.textvalue(section_id);
1235
1236                 return E('div', {
1237                         'class': 'td cbi-value-field',
1238                         'data-title': (title != '') ? title : opt.option,
1239                         'data-description': (descr != '') ? descr : null,
1240                         'data-name': opt.option,
1241                         'data-type': opt.typename || opt.__name__
1242                 }, (value != null) ? value : E('em', _('none')));
1243         },
1244
1245         renderRowActions: function(section_id) {
1246                 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1247         },
1248
1249         parse: function() {
1250                 var section_ids = this.cfgsections(),
1251                     tasks = [];
1252
1253                 if (Array.isArray(this.children)) {
1254                         for (var i = 0; i < section_ids.length; i++) {
1255                                 for (var j = 0; j < this.children.length; j++) {
1256                                         if (!this.children[j].editable || this.children[j].modalonly)
1257                                                 continue;
1258
1259                                         tasks.push(this.children[j].parse(section_ids[i]));
1260                                 }
1261                         }
1262                 }
1263
1264                 return Promise.all(tasks);
1265         }
1266 });
1267
1268 var CBINamedSection = CBIAbstractSection.extend({
1269         __name__: 'CBI.NamedSection',
1270         __init__: function(map, section_id /*, ... */) {
1271                 this.super('__init__', this.varargs(arguments, 2, map));
1272
1273                 this.section = section_id;
1274         },
1275
1276         cfgsections: function() {
1277                 return [ this.section ];
1278         },
1279
1280         handleAdd: function(ev) {
1281                 var section_id = this.section,
1282                     config_name = this.uciconfig || this.map.config;
1283
1284                 uci.add(config_name, this.sectiontype, section_id);
1285                 this.map.save();
1286         },
1287
1288         handleRemove: function(ev) {
1289                 var section_id = this.section,
1290                     config_name = this.uciconfig || this.map.config;
1291
1292                 uci.remove(config_name, section_id);
1293                 this.map.save();
1294         },
1295
1296         renderContents: function(data) {
1297                 var ucidata = data[0], nodes = data[1],
1298                     section_id = this.section,
1299                     config_name = this.uciconfig || this.map.config,
1300                     sectionEl = E('div', {
1301                                 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1302                                 'class': 'cbi-section',
1303                                 'data-tab': this.map.tabbed ? this.sectiontype : null,
1304                                 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
1305                         });
1306
1307                 if (typeof(this.title) === 'string' && this.title !== '')
1308                         sectionEl.appendChild(E('legend', {}, this.title));
1309
1310                 if (typeof(this.description) === 'string' && this.description !== '')
1311                         sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1312
1313                 if (ucidata) {
1314                         if (this.addremove) {
1315                                 sectionEl.appendChild(
1316                                         E('div', { 'class': 'cbi-section-remove right' },
1317                                                 E('input', {
1318                                                         'type': 'submit',
1319                                                         'class': 'cbi-button',
1320                                                         'value': _('Delete'),
1321                                                         'click': L.bind(this.handleRemove, this)
1322                                                 })));
1323                         }
1324
1325                         sectionEl.appendChild(E('div', {
1326                                 'id': 'cbi-%s-%s'.format(config_name, section_id),
1327                                 'class': this.tabs
1328                                         ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1329                                 'data-section-id': section_id
1330                         }, nodes));
1331                 }
1332                 else if (this.addremove) {
1333                         sectionEl.appendChild(
1334                                 E('input', {
1335                                         'type': 'submit',
1336                                         'class': 'cbi-button cbi-button-add',
1337                                         'value': _('Add'),
1338                                         'click': L.bind(this.handleAdd, this)
1339                                 }));
1340                 }
1341
1342                 L.dom.bindClassInstance(sectionEl, this);
1343
1344                 return sectionEl;
1345         },
1346
1347         render: function() {
1348                 var config_name = this.uciconfig || this.map.config,
1349                     section_id = this.section;
1350
1351                 return Promise.all([
1352                         uci.get(config_name, section_id),
1353                         this.renderUCISection(section_id)
1354                 ]).then(this.renderContents.bind(this));
1355         }
1356 });
1357
1358 var CBIValue = CBIAbstractValue.extend({
1359         __name__: 'CBI.Value',
1360
1361         value: function(key, val) {
1362                 this.keylist = this.keylist || [];
1363                 this.keylist.push(String(key));
1364
1365                 this.vallist = this.vallist || [];
1366                 this.vallist.push(String(val != null ? val : key));
1367         },
1368
1369         render: function(option_index, section_id, in_table) {
1370                 return Promise.resolve(this.cfgvalue(section_id))
1371                         .then(this.renderWidget.bind(this, section_id, option_index))
1372                         .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1373         },
1374
1375         renderFrame: function(section_id, in_table, option_index, nodes) {
1376                 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1377                     depend_list = this.transformDepList(section_id),
1378                     optionEl;
1379
1380                 if (in_table) {
1381                         optionEl = E('div', {
1382                                 'class': 'td cbi-value-field',
1383                                 'data-title': this.stripTags(this.title).trim(),
1384                                 'data-description': this.stripTags(this.description).trim(),
1385                                 'data-name': this.option,
1386                                 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1387                         }, E('div', {
1388                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1389                                 'data-index': option_index,
1390                                 'data-depends': depend_list,
1391                                 'data-field': this.cbid(section_id)
1392                         }));
1393                 }
1394                 else {
1395                         optionEl = E('div', {
1396                                 'class': 'cbi-value',
1397                                 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1398                                 'data-index': option_index,
1399                                 'data-depends': depend_list,
1400                                 'data-field': this.cbid(section_id),
1401                                 'data-name': this.option,
1402                                 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1403                         });
1404
1405                         if (this.last_child)
1406                                 optionEl.classList.add('cbi-value-last');
1407
1408                         if (typeof(this.title) === 'string' && this.title !== '') {
1409                                 optionEl.appendChild(E('label', {
1410                                         'class': 'cbi-value-title',
1411                                         'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option)
1412                                 },
1413                                 this.titleref ? E('a', {
1414                                         'class': 'cbi-title-ref',
1415                                         'href': this.titleref,
1416                                         'title': this.titledesc || _('Go to relevant configuration page')
1417                                 }, this.title) : this.title));
1418
1419                                 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1420                         }
1421                 }
1422
1423                 if (nodes)
1424                         (optionEl.lastChild || optionEl).appendChild(nodes);
1425
1426                 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1427                         L.dom.append(optionEl.lastChild || optionEl,
1428                                 E('div', { 'class': 'cbi-value-description' }, this.description));
1429
1430                 if (depend_list && depend_list.length)
1431                         optionEl.classList.add('hidden');
1432
1433                 optionEl.addEventListener('widget-change',
1434                         L.bind(this.map.checkDepends, this.map));
1435
1436                 L.dom.bindClassInstance(optionEl, this);
1437
1438                 return optionEl;
1439         },
1440
1441         renderWidget: function(section_id, option_index, cfgvalue) {
1442                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1443                     choices = this.transformChoices(),
1444                     widget;
1445
1446                 if (choices) {
1447                         var placeholder = (this.optional || this.rmempty)
1448                                 ? E('em', _('unspecified')) : _('-- Please choose --');
1449
1450                         widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1451                                 id: this.cbid(section_id),
1452                                 sort: this.keylist,
1453                                 optional: this.optional || this.rmempty,
1454                                 datatype: this.datatype,
1455                                 select_placeholder: this.placeholder || placeholder,
1456                                 validate: L.bind(this.validate, this, section_id)
1457                         });
1458                 }
1459                 else {
1460                         widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1461                                 id: this.cbid(section_id),
1462                                 password: this.password,
1463                                 optional: this.optional || this.rmempty,
1464                                 datatype: this.datatype,
1465                                 placeholder: this.placeholder,
1466                                 validate: L.bind(this.validate, this, section_id)
1467                         });
1468                 }
1469
1470                 return widget.render();
1471         }
1472 });
1473
1474 var CBIDynamicList = CBIValue.extend({
1475         __name__: 'CBI.DynamicList',
1476
1477         renderWidget: function(section_id, option_index, cfgvalue) {
1478                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1479                     choices = this.transformChoices(),
1480                     items = L.toArray(value);
1481
1482                 var widget = new ui.DynamicList(items, choices, {
1483                         id: this.cbid(section_id),
1484                         sort: this.keylist,
1485                         optional: this.optional || this.rmempty,
1486                         datatype: this.datatype,
1487                         placeholder: this.placeholder,
1488                         validate: L.bind(this.validate, this, section_id)
1489                 });
1490
1491                 return widget.render();
1492         },
1493 });
1494
1495 var CBIListValue = CBIValue.extend({
1496         __name__: 'CBI.ListValue',
1497
1498         __init__: function() {
1499                 this.super('__init__', arguments);
1500                 this.widget = 'select';
1501                 this.deplist = [];
1502         },
1503
1504         renderWidget: function(section_id, option_index, cfgvalue) {
1505                 var choices = this.transformChoices();
1506                 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1507                         id: this.cbid(section_id),
1508                         size: this.size,
1509                         sort: this.keylist,
1510                         optional: this.optional,
1511                         placeholder: this.placeholder,
1512                         validate: L.bind(this.validate, this, section_id)
1513                 });
1514
1515                 return widget.render();
1516         },
1517 });
1518
1519 var CBIFlagValue = CBIValue.extend({
1520         __name__: 'CBI.FlagValue',
1521
1522         __init__: function() {
1523                 this.super('__init__', arguments);
1524
1525                 this.enabled = '1';
1526                 this.disabled = '0';
1527                 this.default = this.disabled;
1528         },
1529
1530         renderWidget: function(section_id, option_index, cfgvalue) {
1531                 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1532                         id: this.cbid(section_id),
1533                         value_enabled: this.enabled,
1534                         value_disabled: this.disabled,
1535                         validate: L.bind(this.validate, this, section_id)
1536                 });
1537
1538                 return widget.render();
1539         },
1540
1541         formvalue: function(section_id) {
1542                 var node = this.map.findElement('id', this.cbid(section_id)),
1543                     checked = node ? L.dom.callClassMethod(node, 'isChecked') : false;
1544
1545                 return checked ? this.enabled : this.disabled;
1546         },
1547
1548         textvalue: function(section_id) {
1549                 var cval = this.cfgvalue(section_id);
1550
1551                 if (cval == null)
1552                         cval = this.default;
1553
1554                 return (cval == this.enabled) ? _('Yes') : _('No');
1555         },
1556
1557         parse: function(section_id) {
1558                 if (this.isActive(section_id)) {
1559                         var fval = this.formvalue(section_id);
1560
1561                         if (!this.isValid(section_id))
1562                                 return Promise.reject();
1563
1564                         if (fval == this.default && (this.optional || this.rmempty))
1565                                 return Promise.resolve(this.remove(section_id));
1566                         else
1567                                 return Promise.resolve(this.write(section_id, fval));
1568                 }
1569                 else {
1570                         return Promise.resolve(this.remove(section_id));
1571                 }
1572         },
1573 });
1574
1575 var CBIMultiValue = CBIDynamicList.extend({
1576         __name__: 'CBI.MultiValue',
1577
1578         __init__: function() {
1579                 this.super('__init__', arguments);
1580                 this.placeholder = _('-- Please choose --');
1581         },
1582
1583         renderWidget: function(section_id, option_index, cfgvalue) {
1584                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1585                     choices = this.transformChoices();
1586
1587                 var widget = new ui.Dropdown(L.toArray(value), choices, {
1588                         id: this.cbid(section_id),
1589                         sort: this.keylist,
1590                         multiple: true,
1591                         optional: this.optional || this.rmempty,
1592                         select_placeholder: this.placeholder,
1593                         display_items: this.display_size || this.size || 3,
1594                         dropdown_items: this.dropdown_size || this.size || -1,
1595                         validate: L.bind(this.validate, this, section_id)
1596                 });
1597
1598                 return widget.render();
1599         },
1600 });
1601
1602 var CBIDummyValue = CBIValue.extend({
1603         __name__: 'CBI.DummyValue',
1604
1605         renderWidget: function(section_id, option_index, cfgvalue) {
1606                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1607                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1608                     outputEl = E('div');
1609
1610                 if (this.href)
1611                         outputEl.appendChild(E('a', { 'href': this.href }));
1612
1613                 L.dom.append(outputEl.lastChild || outputEl,
1614                         this.rawhtml ? value : [ value ]);
1615
1616                 return E([
1617                         outputEl,
1618                         hiddenEl.render()
1619                 ]);
1620         },
1621 });
1622
1623 var CBIButtonValue = CBIValue.extend({
1624         __name__: 'CBI.ButtonValue',
1625
1626         renderWidget: function(section_id, option_index, cfgvalue) {
1627                 var value = (cfgvalue != null) ? cfgvalue : this.default,
1628                     hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1629                     outputEl = E('div'),
1630                     btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
1631
1632                 if (value !== false)
1633                         L.dom.content(outputEl, [
1634                                 E('input', {
1635                                         'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1636                                         'type': 'button',
1637                                         'value': btn_title,
1638                                         'click': L.bind(this.onclick || function(ev) {
1639                                                 ev.target.previousElementSibling.value = ev.target.value;
1640                                                 this.map.save();
1641                                         }, this)
1642                                 })
1643                         ]);
1644                 else
1645                         L.dom.content(outputEl, ' - ');
1646
1647                 return E([
1648                         outputEl,
1649                         hiddenEl.render()
1650                 ]);
1651         }
1652 });
1653
1654 var CBIHiddenValue = CBIValue.extend({
1655         __name__: 'CBI.HiddenValue',
1656
1657         renderWidget: function(section_id, option_index, cfgvalue) {
1658                 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1659                         id: this.cbid(section_id)
1660                 });
1661
1662                 return widget.render();
1663         }
1664 });
1665
1666 var CBISectionValue = CBIValue.extend({
1667         __name__: 'CBI.ContainerValue',
1668         __init__: function(map, section, option, cbiClass /*, ... */) {
1669                 this.super('__init__', [map, section, option]);
1670
1671                 if (!CBIAbstractSection.isSubclass(cbiClass))
1672                         throw 'Sub section must be a descendent of CBIAbstractSection';
1673
1674                 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1675         },
1676
1677         load: function(section_id) {
1678                 return this.subsection.load();
1679         },
1680
1681         parse: function(section_id) {
1682                 return this.subsection.parse();
1683         },
1684
1685         renderWidget: function(section_id, option_index, cfgvalue) {
1686                 return this.subsection.render();
1687         },
1688
1689         checkDepends: function(section_id) {
1690                 this.subsection.checkDepends();
1691                 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
1692         },
1693
1694         write: function() {},
1695         remove: function() {},
1696         cfgvalue: function() { return null },
1697         formvalue: function() { return null }
1698 });
1699
1700 return L.Class.extend({
1701         Map: CBIMap,
1702         AbstractSection: CBIAbstractSection,
1703         AbstractValue: CBIAbstractValue,
1704
1705         TypedSection: CBITypedSection,
1706         TableSection: CBITableSection,
1707         GridSection: CBIGridSection,
1708         NamedSection: CBINamedSection,
1709
1710         Value: CBIValue,
1711         DynamicList: CBIDynamicList,
1712         ListValue: CBIListValue,
1713         Flag: CBIFlagValue,
1714         MultiValue: CBIMultiValue,
1715         DummyValue: CBIDummyValue,
1716         Button: CBIButtonValue,
1717         HiddenValue: CBIHiddenValue,
1718         SectionValue: CBISectionValue
1719 });