7 var CBINode = Class.extend({
8 __init__: function(title, description) {
9 this.title = title || '';
10 this.description = description || '';
14 append: function(obj) {
15 this.children.push(obj);
20 this.children.forEach(function(child) {
21 child.parse.apply(child, args);
26 L.error('InternalError', 'Not implemented');
29 loadChildren: function(/* ... */) {
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));
37 return Promise.all(tasks);
40 renderChildren: function(tab_name /*, ... */) {
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++)));
51 return Promise.all(tasks);
54 stripTags: function(s) {
58 var x = E('div', {}, s);
59 return x.textContent || x.innerText || '';
62 titleFn: function(attr /*, ... */) {
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];
71 s = this.stripTags(String(s)).trim();
73 if (s == null || s == '')
80 var CBIMap = CBINode.extend({
81 __init__: function(config /*, ... */) {
82 this.super('__init__', this.varargs(arguments, 1));
85 this.parsechain = [ config ];
88 findElements: function(/* ... */) {
91 if (arguments.length == 1)
93 else if (arguments.length == 2)
94 q = '[%s="%s"]'.format(arguments[0], arguments[1]);
96 L.error('InternalError', 'Expecting one or two arguments to findElements()');
98 return this.root.querySelectorAll(q);
101 findElement: function(/* ... */) {
102 var res = this.findElements.apply(this, arguments);
103 return res.length ? res[0] : null;
106 chain: function(config) {
107 if (this.parsechain.indexOf(config) == -1)
108 this.parsechain.push(config);
111 section: function(cbiClass /*, ... */) {
112 if (!CBIAbstractSection.isSubclass(cbiClass))
113 L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
115 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
121 return uci.load(this.parsechain || [ this.config ])
122 .then(this.loadChildren.bind(this));
128 if (Array.isArray(this.children))
129 for (var i = 0; i < this.children.length; i++)
130 tasks.push(this.children[i].parse());
132 return Promise.all(tasks);
140 .then(uci.save.bind(uci))
141 .then(this.load.bind(this))
142 .then(this.renderContents.bind(this))
144 alert('Cannot save due to invalid values')
145 return Promise.reject();
150 return this.renderContents();
154 return this.load().then(this.renderContents.bind(this));
157 renderContents: function() {
158 var mapEl = this.root || (this.root = E('div', {
159 'id': 'cbi-%s'.format(this.config),
161 'cbi-dependency-check': L.bind(this.checkDepends, this)
164 L.dom.bindClassInstance(mapEl, this);
166 return this.renderChildren(null).then(L.bind(function(nodes) {
167 var initialRender = !mapEl.firstChild;
169 L.dom.content(mapEl, null);
171 if (this.title != null && this.title != '')
172 mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
174 if (this.description != null && this.description != '')
175 mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
178 L.dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
180 L.dom.append(mapEl, nodes);
182 if (!initialRender) {
183 mapEl.classList.remove('flash');
185 window.setTimeout(function() {
186 mapEl.classList.add('flash');
192 var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
194 for (var i = 0; i < tabGroups.length; i++)
195 ui.tabs.initTabGroup(tabGroups[i].childNodes);
201 lookupOption: function(name, section_id, config_name) {
202 var id, elem, sid, inst;
204 if (name.indexOf('.') > -1)
205 id = 'cbid.%s'.format(name);
207 id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
209 elem = this.findElement('data-field', id);
210 sid = elem ? id.split(/\./)[2] : null;
211 inst = elem ? L.dom.findClassInstance(elem) : null;
213 return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
216 checkDepends: function(ev, n) {
219 for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
220 if (s.checkDepends(ev, n))
223 if (changed && (n || 0) < 10)
224 this.checkDepends(ev, (n || 10) + 1);
226 ui.tabs.updateTabs(ev, this.root);
230 var CBIAbstractSection = CBINode.extend({
231 __init__: function(map, sectionType /*, ... */) {
232 this.super('__init__', this.varargs(arguments, 2));
234 this.sectiontype = sectionType;
236 this.config = map.config;
238 this.optional = true;
239 this.addremove = false;
240 this.dynamic = false;
243 cfgsections: function() {
244 L.error('InternalError', 'Not implemented');
247 filter: function(section_id) {
252 var section_ids = this.cfgsections(),
255 if (Array.isArray(this.children))
256 for (var i = 0; i < section_ids.length; i++)
257 tasks.push(this.loadChildren(section_ids[i])
258 .then(Function.prototype.bind.call(function(section_id, set_values) {
259 for (var i = 0; i < set_values.length; i++)
260 this.children[i].cfgvalue(section_id, set_values[i]);
261 }, this, section_ids[i])));
263 return Promise.all(tasks);
267 var section_ids = this.cfgsections(),
270 if (Array.isArray(this.children))
271 for (var i = 0; i < section_ids.length; i++)
272 for (var j = 0; j < this.children.length; j++)
273 tasks.push(this.children[j].parse(section_ids[i]));
275 return Promise.all(tasks);
278 tab: function(name, title, description) {
279 if (this.tabs && this.tabs[name])
280 throw 'Tab already declared';
285 description: description,
289 this.tabs = this.tabs || [];
290 this.tabs.push(entry);
291 this.tabs[name] = entry;
293 this.tab_names = this.tab_names || [];
294 this.tab_names.push(name);
297 option: function(cbiClass /*, ... */) {
298 if (!CBIAbstractValue.isSubclass(cbiClass))
299 throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
301 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
306 taboption: function(tabName /*, ... */) {
307 if (!this.tabs || !this.tabs[tabName])
308 throw L.error('ReferenceError', 'Associated tab not declared');
310 var obj = this.option.apply(this, this.varargs(arguments, 1));
312 this.tabs[tabName].children.push(obj);
316 renderUCISection: function(section_id) {
317 var renderTasks = [];
320 return this.renderOptions(null, section_id);
322 for (var i = 0; i < this.tab_names.length; i++)
323 renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
325 return Promise.all(renderTasks)
326 .then(this.renderTabContainers.bind(this, section_id));
329 renderTabContainers: function(section_id, nodes) {
330 var config_name = this.uciconfig || this.map.config,
331 containerEls = E([]);
333 for (var i = 0; i < nodes.length; i++) {
334 var tab_name = this.tab_names[i],
335 tab_data = this.tabs[tab_name],
336 containerEl = E('div', {
337 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
338 'data-tab': tab_name,
339 'data-tab-title': tab_data.title,
340 'data-tab-active': tab_name === this.selected_tab
343 if (tab_data.description != null && tab_data.description != '')
344 containerEl.appendChild(
345 E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
347 containerEl.appendChild(nodes[i]);
348 containerEls.appendChild(containerEl);
354 renderOptions: function(tab_name, section_id) {
355 var in_table = (this instanceof CBITableSection);
356 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
357 var optionEls = E([]);
358 for (var i = 0; i < nodes.length; i++)
359 optionEls.appendChild(nodes[i]);
364 checkDepends: function(ev, n) {
366 sids = this.cfgsections();
368 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
369 for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
370 var isActive = o.isActive(sid),
371 isSatisified = o.checkDepends(sid);
373 if (isActive != isSatisified) {
374 o.setActive(sid, !isActive);
379 o.triggerValidation(sid);
388 var isEqual = function(x, y) {
389 if (x != null && y != null && typeof(x) != typeof(y))
392 if ((x == null && y != null) || (x != null && y == null))
395 if (Array.isArray(x)) {
396 if (x.length != y.length)
399 for (var i = 0; i < x.length; i++)
400 if (!isEqual(x[i], y[i]))
403 else if (typeof(x) == 'object') {
405 if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
408 if (!isEqual(x[k], y[k]))
413 if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
423 var CBIAbstractValue = CBINode.extend({
424 __init__: function(map, section, option /*, ... */) {
425 this.super('__init__', this.varargs(arguments, 3));
427 this.section = section;
428 this.option = option;
430 this.config = map.config;
437 this.optional = false;
440 depends: function(field, value) {
443 if (typeof(field) === 'string')
444 deps = {}, deps[field] = value;
448 this.deps.push(deps);
451 transformDepList: function(section_id, deplist) {
452 var list = deplist || this.deps,
455 if (Array.isArray(list)) {
456 for (var i = 0; i < list.length; i++) {
459 for (var k in list[i]) {
460 if (list[i].hasOwnProperty(k)) {
461 if (k.charAt(0) === '!')
463 else if (k.indexOf('.') !== -1)
464 dep['cbid.%s'.format(k)] = list[i][k];
466 dep['cbid.%s.%s.%s'.format(
467 this.uciconfig || this.section.uciconfig || this.map.config,
468 this.ucisection || section_id,
475 if (dep.hasOwnProperty(k)) {
486 transformChoices: function() {
487 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
492 for (var i = 0; i < this.keylist.length; i++)
493 choices[this.keylist[i]] = this.vallist[i];
498 checkDepends: function(section_id) {
501 if (!Array.isArray(this.deps) || !this.deps.length)
504 for (var i = 0; i < this.deps.length; i++) {
508 for (var dep in this.deps[i]) {
509 if (dep == '!reverse') {
512 else if (dep == '!default') {
517 var conf = this.uciconfig || this.section.uciconfig || this.map.config,
518 res = this.map.lookupOption(dep, section_id, conf),
519 val = res ? res[0].formvalue(res[1]) : null;
521 istat = (istat && isEqual(val, this.deps[i][dep]));
532 cbid: function(section_id) {
533 if (section_id == null)
534 L.error('TypeError', 'Section ID required');
536 return 'cbid.%s.%s.%s'.format(
537 this.uciconfig || this.section.uciconfig || this.map.config,
538 section_id, this.option);
541 load: function(section_id) {
542 if (section_id == null)
543 L.error('TypeError', 'Section ID required');
546 this.uciconfig || this.section.uciconfig || this.map.config,
547 this.ucisection || section_id,
548 this.ucioption || this.option);
551 cfgvalue: function(section_id, set_value) {
552 if (section_id == null)
553 L.error('TypeError', 'Section ID required');
555 if (arguments.length == 2) {
556 this.data = this.data || {};
557 this.data[section_id] = set_value;
560 return this.data ? this.data[section_id] : null;
563 formvalue: function(section_id) {
564 var node = this.map.findElement('id', this.cbid(section_id));
565 return node ? L.dom.callClassMethod(node, 'getValue') : null;
568 textvalue: function(section_id) {
569 var cval = this.cfgvalue(section_id);
574 return (cval != null) ? '%h'.format(cval) : null;
577 validate: function(section_id, value) {
581 isValid: function(section_id) {
582 var node = this.map.findElement('id', this.cbid(section_id));
583 return node ? L.dom.callClassMethod(node, 'isValid') : true;
586 isActive: function(section_id) {
587 var field = this.map.findElement('data-field', this.cbid(section_id));
588 return (field != null && !field.classList.contains('hidden'));
591 setActive: function(section_id, active) {
592 var field = this.map.findElement('data-field', this.cbid(section_id));
594 if (field && field.classList.contains('hidden') == active) {
595 field.classList[active ? 'remove' : 'add']('hidden');
602 triggerValidation: function(section_id) {
603 var node = this.map.findElement('id', this.cbid(section_id));
604 return node ? L.dom.callClassMethod(node, 'triggerValidation') : true;
607 parse: function(section_id) {
608 var active = this.isActive(section_id),
609 cval = this.cfgvalue(section_id),
610 fval = active ? this.formvalue(section_id) : null;
612 if (active && !this.isValid(section_id))
613 return Promise.reject();
615 if (fval != '' && fval != null) {
616 if (this.forcewrite || !isEqual(cval, fval))
617 return Promise.resolve(this.write(section_id, fval));
620 if (this.rmempty || this.optional) {
621 return Promise.resolve(this.remove(section_id));
623 else if (!isEqual(cval, fval)) {
624 console.log('This should have been catched by isValid()');
625 return Promise.reject();
629 return Promise.resolve();
632 write: function(section_id, formvalue) {
634 this.uciconfig || this.section.uciconfig || this.map.config,
635 this.ucisection || section_id,
636 this.ucioption || this.option,
640 remove: function(section_id) {
642 this.uciconfig || this.section.uciconfig || this.map.config,
643 this.ucisection || section_id,
644 this.ucioption || this.option);
648 var CBITypedSection = CBIAbstractSection.extend({
649 __name__: 'CBI.TypedSection',
651 cfgsections: function() {
652 return uci.sections(this.uciconfig || this.map.config, this.sectiontype)
653 .map(function(s) { return s['.name'] })
654 .filter(L.bind(this.filter, this));
657 handleAdd: function(ev, name) {
658 var config_name = this.uciconfig || this.map.config;
660 uci.add(config_name, this.sectiontype, name);
664 handleRemove: function(section_id, ev) {
665 var config_name = this.uciconfig || this.map.config;
667 uci.remove(config_name, section_id);
671 renderSectionAdd: function(extra_class) {
675 var createEl = E('div', { 'class': 'cbi-section-create' }),
676 config_name = this.uciconfig || this.map.config,
677 btn_title = this.titleFn('addbtntitle');
679 if (extra_class != null)
680 createEl.classList.add(extra_class);
682 if (this.anonymous) {
683 createEl.appendChild(E('input', {
685 'class': 'cbi-button cbi-button-add',
686 'value': btn_title || _('Add'),
687 'title': btn_title || _('Add'),
688 'click': L.bind(this.handleAdd, this)
692 var nameEl = E('input', {
694 'class': 'cbi-section-create-name'
697 L.dom.append(createEl, [
698 E('div', {}, nameEl),
700 'class': 'cbi-button cbi-button-add',
702 'value': btn_title || _('Add'),
703 'title': btn_title || _('Add'),
704 'click': L.bind(function(ev) {
705 if (nameEl.classList.contains('cbi-input-invalid'))
708 this.handleAdd(ev, nameEl.value);
713 ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
719 renderSectionPlaceholder: function() {
721 E('em', _('This section contains no values yet')),
726 renderContents: function(cfgsections, nodes) {
727 var section_id = null,
728 config_name = this.uciconfig || this.map.config,
729 sectionEl = E('div', {
730 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
731 'class': 'cbi-section',
732 'data-tab': this.map.tabbed ? this.sectiontype : null,
733 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
736 if (this.title != null && this.title != '')
737 sectionEl.appendChild(E('legend', {}, this.title));
739 if (this.description != null && this.description != '')
740 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
742 for (var i = 0; i < nodes.length; i++) {
743 if (this.addremove) {
744 sectionEl.appendChild(
745 E('div', { 'class': 'cbi-section-remove right' },
748 'class': 'cbi-button',
749 'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
750 'value': _('Delete'),
751 'data-section-id': cfgsections[i],
752 'click': L.bind(this.handleRemove, this, cfgsections[i])
757 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
759 sectionEl.appendChild(E('div', {
760 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
762 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
763 'data-section-id': cfgsections[i]
767 if (nodes.length == 0)
768 sectionEl.appendChild(this.renderSectionPlaceholder());
770 sectionEl.appendChild(this.renderSectionAdd());
772 L.dom.bindClassInstance(sectionEl, this);
778 var cfgsections = this.cfgsections(),
781 for (var i = 0; i < cfgsections.length; i++)
782 renderTasks.push(this.renderUCISection(cfgsections[i]));
784 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
788 var CBITableSection = CBITypedSection.extend({
789 __name__: 'CBI.TableSection',
792 throw 'Tabs are not supported by TableSection';
795 renderContents: function(cfgsections, nodes) {
796 var section_id = null,
797 config_name = this.uciconfig || this.map.config,
798 max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
799 has_more = max_cols < this.children.length,
800 sectionEl = E('div', {
801 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
802 'class': 'cbi-section cbi-tblsection',
803 'data-tab': this.map.tabbed ? this.sectiontype : null,
804 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
807 'class': 'table cbi-section-table'
810 if (this.title != null && this.title != '')
811 sectionEl.appendChild(E('h3', {}, this.title));
813 if (this.description != null && this.description != '')
814 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
816 tableEl.appendChild(this.renderHeaderRows(max_cols));
818 for (var i = 0; i < nodes.length; i++) {
819 var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
821 var trEl = E('div', {
822 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
823 'class': 'tr cbi-section-table-row',
824 'data-sid': cfgsections[i],
825 'draggable': this.sortable ? true : null,
826 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
827 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
828 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
829 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
830 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
831 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
832 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
833 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
834 'data-section-id': cfgsections[i]
837 if (this.extedit || this.rowcolors)
838 trEl.classList.add(!(tableEl.childNodes.length % 2)
839 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
841 for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
842 trEl.appendChild(nodes[i].firstChild);
844 trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
845 tableEl.appendChild(trEl);
848 if (nodes.length == 0)
849 tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
850 E('div', { 'class': 'td' },
851 E('em', {}, _('This section contains no values yet')))));
853 sectionEl.appendChild(tableEl);
855 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
857 L.dom.bindClassInstance(sectionEl, this);
862 renderHeaderRows: function(max_cols) {
863 var has_titles = false,
864 has_descriptions = false,
865 anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
868 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
869 if (opt.optional || opt.modalonly)
872 has_titles = has_titles || !!opt.title;
873 has_descriptions = has_descriptions || !!opt.description;
877 var trEl = E('div', {
878 'class': 'tr cbi-section-table-titles ' + anon_class,
879 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
882 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
883 if (opt.optional || opt.modalonly)
886 trEl.appendChild(E('div', {
887 'class': 'th cbi-section-table-cell',
888 'data-type': opt.__name__
891 if (opt.width != null)
892 trEl.lastElementChild.style.width =
893 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
896 trEl.lastElementChild.appendChild(E('a', {
897 'href': opt.titleref,
898 'class': 'cbi-title-ref',
899 'title': this.titledesc || _('Go to relevant configuration page')
902 L.dom.content(trEl.lastElementChild, opt.title);
905 if (this.sortable || this.extedit || this.addremove || has_more)
906 trEl.appendChild(E('div', {
907 'class': 'th cbi-section-table-cell cbi-section-actions'
910 trEls.appendChild(trEl);
913 if (has_descriptions) {
914 var trEl = E('div', {
915 'class': 'tr cbi-section-table-descr ' + anon_class
918 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
919 if (opt.optional || opt.modalonly)
922 trEl.appendChild(E('div', {
923 'class': 'th cbi-section-table-cell',
924 'data-type': opt.__name__
925 }, opt.description));
927 if (opt.width != null)
928 trEl.lastElementChild.style.width =
929 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
932 if (this.sortable || this.extedit || this.addremove || has_more)
933 trEl.appendChild(E('div', {
934 'class': 'th cbi-section-table-cell cbi-section-actions'
937 trEls.appendChild(trEl);
943 renderRowActions: function(section_id, more_label) {
944 var config_name = this.uciconfig || this.map.config;
946 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
949 var tdEl = E('div', {
950 'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
954 L.dom.append(tdEl.lastElementChild, [
956 'title': _('Drag to reorder'),
957 'class': 'cbi-button drag-handle center',
958 'style': 'cursor:move'
966 if (typeof(this.extedit) == 'function')
967 evFn = L.bind(this.extedit, this);
968 else if (typeof(this.extedit) == 'string')
969 evFn = L.bind(function(sid, ev) {
970 location.href = this.extedit.format(sid);
971 }, this, section_id);
973 L.dom.append(tdEl.lastElementChild,
978 'class': 'cbi-button cbi-button-edit',
985 L.dom.append(tdEl.lastElementChild,
990 'class': 'cbi-button cbi-button-edit',
991 'click': L.bind(this.renderMoreOptionsModal, this, section_id)
996 if (this.addremove) {
997 var btn_title = this.titleFn('removebtntitle', section_id);
999 L.dom.append(tdEl.lastElementChild,
1002 'value': btn_title || _('Delete'),
1003 'title': btn_title || _('Delete'),
1004 'class': 'cbi-button cbi-button-remove',
1005 'click': L.bind(function(sid, ev) {
1006 uci.remove(config_name, sid);
1008 }, this, section_id)
1016 handleDragInit: function(ev) {
1017 scope.dragState = { node: ev.target };
1020 handleDragStart: function(ev) {
1021 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
1022 scope.dragState = null;
1023 ev.preventDefault();
1027 scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
1028 ev.dataTransfer.setData('text', 'drag');
1029 ev.target.style.opacity = 0.4;
1032 handleDragOver: function(ev) {
1033 var n = scope.dragState.targetNode,
1034 r = scope.dragState.rect,
1035 t = r.top + r.height / 2;
1037 if (ev.clientY <= t) {
1038 n.classList.remove('drag-over-below');
1039 n.classList.add('drag-over-above');
1042 n.classList.remove('drag-over-above');
1043 n.classList.add('drag-over-below');
1046 ev.dataTransfer.dropEffect = 'move';
1047 ev.preventDefault();
1051 handleDragEnter: function(ev) {
1052 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
1053 scope.dragState.targetNode = ev.currentTarget;
1056 handleDragLeave: function(ev) {
1057 ev.currentTarget.classList.remove('drag-over-above');
1058 ev.currentTarget.classList.remove('drag-over-below');
1061 handleDragEnd: function(ev) {
1064 n.style.opacity = '';
1065 n.classList.add('flash');
1066 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
1067 .forEach(function(tr) {
1068 tr.classList.remove('drag-over-above');
1069 tr.classList.remove('drag-over-below');
1073 handleDrop: function(ev) {
1074 var s = scope.dragState;
1076 if (s.node && s.targetNode) {
1077 var config_name = this.uciconfig || this.map.config,
1078 ref_node = s.targetNode,
1081 if (ref_node.classList.contains('drag-over-below')) {
1082 ref_node = ref_node.nextElementSibling;
1086 var sid1 = s.node.getAttribute('data-sid'),
1087 sid2 = s.targetNode.getAttribute('data-sid');
1089 s.node.parentNode.insertBefore(s.node, ref_node);
1090 uci.move(config_name, sid1, sid2, after);
1093 scope.dragState = null;
1094 ev.target.style.opacity = '';
1095 ev.stopPropagation();
1096 ev.preventDefault();
1100 handleModalCancel: function(modalMap, ev) {
1101 return Promise.resolve(L.ui.hideModal());
1104 handleModalSave: function(modalMap, ev) {
1105 return modalMap.save()
1106 .then(L.bind(this.map.load, this.map))
1107 .then(L.bind(this.map.reset, this.map))
1108 .then(L.ui.hideModal)
1109 .catch(function() {});
1112 addModalOptions: function(modalSection, section_id, ev) {
1116 renderMoreOptionsModal: function(section_id, ev) {
1117 var parent = this.map,
1118 title = parent.title,
1120 m = new CBIMap(this.map.config, null, null),
1121 s = m.section(CBINamedSection, section_id, this.sectiontype);
1124 s.tab_names = this.tab_names;
1126 if ((name = this.titleFn('modaltitle', section_id)) != null)
1128 else if ((name = this.titleFn('sectiontitle', section_id)) != null)
1129 title = '%s - %s'.format(parent.title, name);
1130 else if (!this.anonymous)
1131 title = '%s - %s'.format(parent.title, section_id);
1133 for (var i = 0; i < this.children.length; i++) {
1134 var o1 = this.children[i];
1136 if (o1.modalonly === false)
1139 var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
1142 if (!o1.hasOwnProperty(k))
1159 //ev.target.classList.add('spinning');
1160 Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
1161 //ev.target.classList.remove('spinning');
1162 L.ui.showModal(title, [
1164 E('div', { 'class': 'right' }, [
1168 'click': L.bind(this.handleModalCancel, this, m),
1169 'value': _('Dismiss')
1173 'class': 'cbi-button cbi-button-positive important',
1174 'click': L.bind(this.handleModalSave, this, m),
1179 }, this)).catch(L.error);
1183 var CBIGridSection = CBITableSection.extend({
1184 tab: function(name, title, description) {
1185 CBIAbstractSection.prototype.tab.call(this, name, title, description);
1188 handleAdd: function(ev) {
1189 var config_name = this.uciconfig || this.map.config,
1190 section_id = uci.add(config_name, this.sectiontype);
1192 this.addedSection = section_id;
1193 this.renderMoreOptionsModal(section_id);
1196 handleModalSave: function(/* ... */) {
1197 return this.super('handleModalSave', arguments)
1198 .then(L.bind(function() { this.addedSection = null }, this));
1201 handleModalCancel: function(/* ... */) {
1202 var config_name = this.uciconfig || this.map.config;
1204 if (this.addedSection != null) {
1205 uci.remove(config_name, this.addedSection);
1206 this.addedSection = null;
1209 return this.super('handleModalCancel', arguments);
1212 renderUCISection: function(section_id) {
1213 return this.renderOptions(null, section_id);
1216 renderChildren: function(tab_name, section_id, in_table) {
1217 var tasks = [], index = 0;
1219 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
1220 if (opt.disable || opt.modalonly)
1224 tasks.push(opt.render(index++, section_id, in_table));
1226 tasks.push(this.renderTextValue(section_id, opt));
1229 return Promise.all(tasks);
1232 renderTextValue: function(section_id, opt) {
1233 var title = this.stripTags(opt.title).trim(),
1234 descr = this.stripTags(opt.description).trim(),
1235 value = opt.textvalue(section_id);
1238 'class': 'td cbi-value-field',
1239 'data-title': (title != '') ? title : opt.option,
1240 'data-description': (descr != '') ? descr : null,
1241 'data-name': opt.option,
1242 'data-type': opt.typename || opt.__name__
1243 }, (value != null) ? value : E('em', _('none')));
1246 renderRowActions: function(section_id) {
1247 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1251 var section_ids = this.cfgsections(),
1254 if (Array.isArray(this.children)) {
1255 for (var i = 0; i < section_ids.length; i++) {
1256 for (var j = 0; j < this.children.length; j++) {
1257 if (!this.children[j].editable || this.children[j].modalonly)
1260 tasks.push(this.children[j].parse(section_ids[i]));
1265 return Promise.all(tasks);
1269 var CBINamedSection = CBIAbstractSection.extend({
1270 __name__: 'CBI.NamedSection',
1271 __init__: function(map, section_id /*, ... */) {
1272 this.super('__init__', this.varargs(arguments, 2, map));
1274 this.section = section_id;
1277 cfgsections: function() {
1278 return [ this.section ];
1281 handleAdd: function(ev) {
1282 var section_id = this.section,
1283 config_name = this.uciconfig || this.map.config;
1285 uci.add(config_name, this.sectiontype, section_id);
1289 handleRemove: function(ev) {
1290 var section_id = this.section,
1291 config_name = this.uciconfig || this.map.config;
1293 uci.remove(config_name, section_id);
1297 renderContents: function(data) {
1298 var ucidata = data[0], nodes = data[1],
1299 section_id = this.section,
1300 config_name = this.uciconfig || this.map.config,
1301 sectionEl = E('div', {
1302 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1303 'class': 'cbi-section',
1304 'data-tab': this.map.tabbed ? this.sectiontype : null,
1305 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
1308 if (typeof(this.title) === 'string' && this.title !== '')
1309 sectionEl.appendChild(E('legend', {}, this.title));
1311 if (typeof(this.description) === 'string' && this.description !== '')
1312 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1315 if (this.addremove) {
1316 sectionEl.appendChild(
1317 E('div', { 'class': 'cbi-section-remove right' },
1320 'class': 'cbi-button',
1321 'value': _('Delete'),
1322 'click': L.bind(this.handleRemove, this)
1326 sectionEl.appendChild(E('div', {
1327 'id': 'cbi-%s-%s'.format(config_name, section_id),
1329 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1330 'data-section-id': section_id
1333 else if (this.addremove) {
1334 sectionEl.appendChild(
1337 'class': 'cbi-button cbi-button-add',
1339 'click': L.bind(this.handleAdd, this)
1343 L.dom.bindClassInstance(sectionEl, this);
1348 render: function() {
1349 var config_name = this.uciconfig || this.map.config,
1350 section_id = this.section;
1352 return Promise.all([
1353 uci.get(config_name, section_id),
1354 this.renderUCISection(section_id)
1355 ]).then(this.renderContents.bind(this));
1359 var CBIValue = CBIAbstractValue.extend({
1360 __name__: 'CBI.Value',
1362 value: function(key, val) {
1363 this.keylist = this.keylist || [];
1364 this.keylist.push(String(key));
1366 this.vallist = this.vallist || [];
1367 this.vallist.push(String(val != null ? val : key));
1370 render: function(option_index, section_id, in_table) {
1371 return Promise.resolve(this.cfgvalue(section_id))
1372 .then(this.renderWidget.bind(this, section_id, option_index))
1373 .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1376 renderFrame: function(section_id, in_table, option_index, nodes) {
1377 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1378 depend_list = this.transformDepList(section_id),
1382 optionEl = E('div', {
1383 'class': 'td cbi-value-field',
1384 'data-title': this.stripTags(this.title).trim(),
1385 'data-description': this.stripTags(this.description).trim(),
1386 'data-name': this.option,
1387 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1389 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1390 'data-index': option_index,
1391 'data-depends': depend_list,
1392 'data-field': this.cbid(section_id)
1396 optionEl = E('div', {
1397 'class': 'cbi-value',
1398 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1399 'data-index': option_index,
1400 'data-depends': depend_list,
1401 'data-field': this.cbid(section_id),
1402 'data-name': this.option,
1403 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1406 if (this.last_child)
1407 optionEl.classList.add('cbi-value-last');
1409 if (typeof(this.title) === 'string' && this.title !== '') {
1410 optionEl.appendChild(E('label', {
1411 'class': 'cbi-value-title',
1412 'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option)
1414 this.titleref ? E('a', {
1415 'class': 'cbi-title-ref',
1416 'href': this.titleref,
1417 'title': this.titledesc || _('Go to relevant configuration page')
1418 }, this.title) : this.title));
1420 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1425 (optionEl.lastChild || optionEl).appendChild(nodes);
1427 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1428 L.dom.append(optionEl.lastChild || optionEl,
1429 E('div', { 'class': 'cbi-value-description' }, this.description));
1431 if (depend_list && depend_list.length)
1432 optionEl.classList.add('hidden');
1434 optionEl.addEventListener('widget-change',
1435 L.bind(this.map.checkDepends, this.map));
1437 L.dom.bindClassInstance(optionEl, this);
1442 renderWidget: function(section_id, option_index, cfgvalue) {
1443 var value = (cfgvalue != null) ? cfgvalue : this.default,
1444 choices = this.transformChoices(),
1448 var placeholder = (this.optional || this.rmempty)
1449 ? E('em', _('unspecified')) : _('-- Please choose --');
1451 widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1452 id: this.cbid(section_id),
1454 optional: this.optional || this.rmempty,
1455 datatype: this.datatype,
1456 select_placeholder: this.placeholder || placeholder,
1457 validate: L.bind(this.validate, this, section_id)
1461 widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1462 id: this.cbid(section_id),
1463 password: this.password,
1464 optional: this.optional || this.rmempty,
1465 datatype: this.datatype,
1466 placeholder: this.placeholder,
1467 validate: L.bind(this.validate, this, section_id)
1471 return widget.render();
1475 var CBIDynamicList = CBIValue.extend({
1476 __name__: 'CBI.DynamicList',
1478 renderWidget: function(section_id, option_index, cfgvalue) {
1479 var value = (cfgvalue != null) ? cfgvalue : this.default,
1480 choices = this.transformChoices(),
1481 items = L.toArray(value);
1483 var widget = new ui.DynamicList(items, choices, {
1484 id: this.cbid(section_id),
1486 optional: this.optional || this.rmempty,
1487 datatype: this.datatype,
1488 placeholder: this.placeholder,
1489 validate: L.bind(this.validate, this, section_id)
1492 return widget.render();
1496 var CBIListValue = CBIValue.extend({
1497 __name__: 'CBI.ListValue',
1499 __init__: function() {
1500 this.super('__init__', arguments);
1501 this.widget = 'select';
1505 renderWidget: function(section_id, option_index, cfgvalue) {
1506 var choices = this.transformChoices();
1507 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1508 id: this.cbid(section_id),
1511 optional: this.optional,
1512 placeholder: this.placeholder,
1513 validate: L.bind(this.validate, this, section_id)
1516 return widget.render();
1520 var CBIFlagValue = CBIValue.extend({
1521 __name__: 'CBI.FlagValue',
1523 __init__: function() {
1524 this.super('__init__', arguments);
1527 this.disabled = '0';
1528 this.default = this.disabled;
1531 renderWidget: function(section_id, option_index, cfgvalue) {
1532 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1533 id: this.cbid(section_id),
1534 value_enabled: this.enabled,
1535 value_disabled: this.disabled,
1536 validate: L.bind(this.validate, this, section_id)
1539 return widget.render();
1542 formvalue: function(section_id) {
1543 var node = this.map.findElement('id', this.cbid(section_id)),
1544 checked = node ? L.dom.callClassMethod(node, 'isChecked') : false;
1546 return checked ? this.enabled : this.disabled;
1549 textvalue: function(section_id) {
1550 var cval = this.cfgvalue(section_id);
1553 cval = this.default;
1555 return (cval == this.enabled) ? _('Yes') : _('No');
1558 parse: function(section_id) {
1559 if (this.isActive(section_id)) {
1560 var fval = this.formvalue(section_id);
1562 if (!this.isValid(section_id))
1563 return Promise.reject();
1565 if (fval == this.default && (this.optional || this.rmempty))
1566 return Promise.resolve(this.remove(section_id));
1568 return Promise.resolve(this.write(section_id, fval));
1571 return Promise.resolve(this.remove(section_id));
1576 var CBIMultiValue = CBIDynamicList.extend({
1577 __name__: 'CBI.MultiValue',
1579 __init__: function() {
1580 this.super('__init__', arguments);
1581 this.placeholder = _('-- Please choose --');
1584 renderWidget: function(section_id, option_index, cfgvalue) {
1585 var value = (cfgvalue != null) ? cfgvalue : this.default,
1586 choices = this.transformChoices();
1588 var widget = new ui.Dropdown(L.toArray(value), choices, {
1589 id: this.cbid(section_id),
1592 optional: this.optional || this.rmempty,
1593 select_placeholder: this.placeholder,
1594 display_items: this.display_size || this.size || 3,
1595 dropdown_items: this.dropdown_size || this.size || -1,
1596 validate: L.bind(this.validate, this, section_id)
1599 return widget.render();
1603 var CBITextValue = CBIValue.extend({
1604 __name__: 'CBI.TextValue',
1608 renderWidget: function(section_id, option_index, cfgvalue) {
1609 var value = (cfgvalue != null) ? cfgvalue : this.default;
1611 var widget = new ui.Textarea(value, {
1612 id: this.cbid(section_id),
1613 optional: this.optional || this.rmempty,
1614 placeholder: this.placeholder,
1615 monospace: this.monospace,
1619 validate: L.bind(this.validate, this, section_id)
1622 return widget.render();
1626 var CBIDummyValue = CBIValue.extend({
1627 __name__: 'CBI.DummyValue',
1629 renderWidget: function(section_id, option_index, cfgvalue) {
1630 var value = (cfgvalue != null) ? cfgvalue : this.default,
1631 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1632 outputEl = E('div');
1635 outputEl.appendChild(E('a', { 'href': this.href }));
1637 L.dom.append(outputEl.lastChild || outputEl,
1638 this.rawhtml ? value : [ value ]);
1647 var CBIButtonValue = CBIValue.extend({
1648 __name__: 'CBI.ButtonValue',
1650 renderWidget: function(section_id, option_index, cfgvalue) {
1651 var value = (cfgvalue != null) ? cfgvalue : this.default,
1652 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1653 outputEl = E('div'),
1654 btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
1656 if (value !== false)
1657 L.dom.content(outputEl, [
1659 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1662 'click': L.bind(this.onclick || function(ev) {
1663 ev.target.previousElementSibling.value = ev.target.value;
1669 L.dom.content(outputEl, ' - ');
1678 var CBIHiddenValue = CBIValue.extend({
1679 __name__: 'CBI.HiddenValue',
1681 renderWidget: function(section_id, option_index, cfgvalue) {
1682 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1683 id: this.cbid(section_id)
1686 return widget.render();
1690 var CBISectionValue = CBIValue.extend({
1691 __name__: 'CBI.ContainerValue',
1692 __init__: function(map, section, option, cbiClass /*, ... */) {
1693 this.super('__init__', [map, section, option]);
1695 if (!CBIAbstractSection.isSubclass(cbiClass))
1696 throw 'Sub section must be a descendent of CBIAbstractSection';
1698 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1701 load: function(section_id) {
1702 return this.subsection.load();
1705 parse: function(section_id) {
1706 return this.subsection.parse();
1709 renderWidget: function(section_id, option_index, cfgvalue) {
1710 return this.subsection.render();
1713 checkDepends: function(section_id) {
1714 this.subsection.checkDepends();
1715 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
1718 write: function() {},
1719 remove: function() {},
1720 cfgvalue: function() { return null },
1721 formvalue: function() { return null }
1724 return L.Class.extend({
1726 AbstractSection: CBIAbstractSection,
1727 AbstractValue: CBIAbstractValue,
1729 TypedSection: CBITypedSection,
1730 TableSection: CBITableSection,
1731 GridSection: CBIGridSection,
1732 NamedSection: CBINamedSection,
1735 DynamicList: CBIDynamicList,
1736 ListValue: CBIListValue,
1738 MultiValue: CBIMultiValue,
1739 TextValue: CBITextValue,
1740 DummyValue: CBIDummyValue,
1741 Button: CBIButtonValue,
1742 HiddenValue: CBIHiddenValue,
1743 SectionValue: CBISectionValue