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