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