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