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