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