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