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