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