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);
139 .then(uci.save.bind(uci))
140 .then(this.load.bind(this))
141 .then(this.renderContents.bind(this))
143 alert('Cannot save due to invalid values')
144 return Promise.reject();
149 return this.renderContents();
153 return this.load().then(this.renderContents.bind(this));
156 renderContents: function() {
157 var mapEl = this.root || (this.root = E('div', {
158 'id': 'cbi-%s'.format(this.config),
160 'cbi-dependency-check': L.bind(this.checkDepends, this)
163 L.dom.bindClassInstance(mapEl, this);
165 return this.renderChildren(null).then(L.bind(function(nodes) {
166 var initialRender = !mapEl.firstChild;
168 L.dom.content(mapEl, null);
170 if (this.title != null && this.title != '')
171 mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
173 if (this.description != null && this.description != '')
174 mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
177 L.dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
179 L.dom.append(mapEl, nodes);
181 if (!initialRender) {
182 mapEl.classList.remove('flash');
184 window.setTimeout(function() {
185 mapEl.classList.add('flash');
191 var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
193 for (var i = 0; i < tabGroups.length; i++)
194 ui.tabs.initTabGroup(tabGroups[i].childNodes);
200 lookupOption: function(name, section_id, config_name) {
201 var id, elem, sid, inst;
203 if (name.indexOf('.') > -1)
204 id = 'cbid.%s'.format(name);
206 id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
208 elem = this.findElement('data-field', id);
209 sid = elem ? id.split(/\./)[2] : null;
210 inst = elem ? L.dom.findClassInstance(elem) : null;
212 return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
215 checkDepends: function(ev, n) {
218 for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
219 if (s.checkDepends(ev, n))
222 if (changed && (n || 0) < 10)
223 this.checkDepends(ev, (n || 10) + 1);
225 ui.tabs.updateTabs(ev, this.root);
229 var CBIAbstractSection = CBINode.extend({
230 __init__: function(map, sectionType /*, ... */) {
231 this.super('__init__', this.varargs(arguments, 2));
233 this.sectiontype = sectionType;
235 this.config = map.config;
237 this.optional = true;
238 this.addremove = false;
239 this.dynamic = false;
242 cfgsections: function() {
243 L.error('InternalError', 'Not implemented');
246 filter: function(section_id) {
251 var section_ids = this.cfgsections(),
254 if (Array.isArray(this.children))
255 for (var i = 0; i < section_ids.length; i++)
256 tasks.push(this.loadChildren(section_ids[i])
257 .then(Function.prototype.bind.call(function(section_id, set_values) {
258 for (var i = 0; i < set_values.length; i++)
259 this.children[i].cfgvalue(section_id, set_values[i]);
260 }, this, section_ids[i])));
262 return Promise.all(tasks);
266 var section_ids = this.cfgsections(),
269 if (Array.isArray(this.children))
270 for (var i = 0; i < section_ids.length; i++)
271 for (var j = 0; j < this.children.length; j++)
272 tasks.push(this.children[j].parse(section_ids[i]));
274 return Promise.all(tasks);
277 tab: function(name, title, description) {
278 if (this.tabs && this.tabs[name])
279 throw 'Tab already declared';
284 description: description,
288 this.tabs = this.tabs || [];
289 this.tabs.push(entry);
290 this.tabs[name] = entry;
292 this.tab_names = this.tab_names || [];
293 this.tab_names.push(name);
296 option: function(cbiClass /*, ... */) {
297 if (!CBIAbstractValue.isSubclass(cbiClass))
298 throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
300 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
305 taboption: function(tabName /*, ... */) {
306 if (!this.tabs || !this.tabs[tabName])
307 throw L.error('ReferenceError', 'Associated tab not declared');
309 var obj = this.option.apply(this, this.varargs(arguments, 1));
311 this.tabs[tabName].children.push(obj);
315 renderUCISection: function(section_id) {
316 var renderTasks = [];
319 return this.renderOptions(null, section_id);
321 for (var i = 0; i < this.tab_names.length; i++)
322 renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
324 return Promise.all(renderTasks)
325 .then(this.renderTabContainers.bind(this, section_id));
328 renderTabContainers: function(section_id, nodes) {
329 var config_name = this.uciconfig || this.map.config,
330 containerEls = E([]);
332 for (var i = 0; i < nodes.length; i++) {
333 var tab_name = this.tab_names[i],
334 tab_data = this.tabs[tab_name],
335 containerEl = E('div', {
336 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
337 'data-tab': tab_name,
338 'data-tab-title': tab_data.title,
339 'data-tab-active': tab_name === this.selected_tab
342 if (tab_data.description != null && tab_data.description != '')
343 containerEl.appendChild(
344 E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
346 containerEl.appendChild(nodes[i]);
347 containerEls.appendChild(containerEl);
353 renderOptions: function(tab_name, section_id) {
354 var in_table = (this instanceof CBITableSection);
355 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
356 var optionEls = E([]);
357 for (var i = 0; i < nodes.length; i++)
358 optionEls.appendChild(nodes[i]);
363 checkDepends: function(ev, n) {
365 sids = this.cfgsections();
367 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
368 for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
369 var isActive = o.isActive(sid),
370 isSatisified = o.checkDepends(sid);
372 if (isActive != isSatisified) {
373 o.setActive(sid, !isActive);
378 o.triggerValidation(sid);
387 var isEqual = function(x, y) {
388 if (x != null && y != null && typeof(x) != typeof(y))
391 if ((x == null && y != null) || (x != null && y == null))
394 if (Array.isArray(x)) {
395 if (x.length != y.length)
398 for (var i = 0; i < x.length; i++)
399 if (!isEqual(x[i], y[i]))
402 else if (typeof(x) == 'object') {
404 if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
407 if (!isEqual(x[k], y[k]))
412 if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
422 var CBIAbstractValue = CBINode.extend({
423 __init__: function(map, section, option /*, ... */) {
424 this.super('__init__', this.varargs(arguments, 3));
426 this.section = section;
427 this.option = option;
429 this.config = map.config;
436 this.optional = false;
439 depends: function(field, value) {
442 if (typeof(field) === 'string')
443 deps = {}, deps[field] = value;
447 this.deps.push(deps);
450 transformDepList: function(section_id, deplist) {
451 var list = deplist || this.deps,
454 if (Array.isArray(list)) {
455 for (var i = 0; i < list.length; i++) {
458 for (var k in list[i]) {
459 if (list[i].hasOwnProperty(k)) {
460 if (k.charAt(0) === '!')
462 else if (k.indexOf('.') !== -1)
463 dep['cbid.%s'.format(k)] = list[i][k];
465 dep['cbid.%s.%s.%s'.format(
466 this.uciconfig || this.section.uciconfig || this.map.config,
467 this.ucisection || section_id,
474 if (dep.hasOwnProperty(k)) {
485 transformChoices: function() {
486 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
491 for (var i = 0; i < this.keylist.length; i++)
492 choices[this.keylist[i]] = this.vallist[i];
497 checkDepends: function(section_id) {
500 if (!Array.isArray(this.deps) || !this.deps.length)
503 for (var i = 0; i < this.deps.length; i++) {
507 for (var dep in this.deps[i]) {
508 if (dep == '!reverse') {
511 else if (dep == '!default') {
516 var conf = this.uciconfig || this.section.uciconfig || this.map.config,
517 res = this.map.lookupOption(dep, section_id, conf),
518 val = res ? res[0].formvalue(res[1]) : null;
520 istat = (istat && isEqual(val, this.deps[i][dep]));
531 cbid: function(section_id) {
532 if (section_id == null)
533 L.error('TypeError', 'Section ID required');
535 return 'cbid.%s.%s.%s'.format(
536 this.uciconfig || this.section.uciconfig || this.map.config,
537 section_id, this.option);
540 load: function(section_id) {
541 if (section_id == null)
542 L.error('TypeError', 'Section ID required');
545 this.uciconfig || this.section.uciconfig || this.map.config,
546 this.ucisection || section_id,
547 this.ucioption || this.option);
550 cfgvalue: function(section_id, set_value) {
551 if (section_id == null)
552 L.error('TypeError', 'Section ID required');
554 if (arguments.length == 2) {
555 this.data = this.data || {};
556 this.data[section_id] = set_value;
559 return this.data ? this.data[section_id] : null;
562 formvalue: function(section_id) {
563 var node = this.map.findElement('id', this.cbid(section_id));
564 return node ? L.dom.callClassMethod(node, 'getValue') : null;
567 textvalue: function(section_id) {
568 var cval = this.cfgvalue(section_id);
573 return (cval != null) ? '%h'.format(cval) : null;
576 validate: function(section_id, value) {
580 isValid: function(section_id) {
581 var node = this.map.findElement('id', this.cbid(section_id));
582 return node ? L.dom.callClassMethod(node, 'isValid') : true;
585 isActive: function(section_id) {
586 var field = this.map.findElement('data-field', this.cbid(section_id));
587 return (field != null && !field.classList.contains('hidden'));
590 setActive: function(section_id, active) {
591 var field = this.map.findElement('data-field', this.cbid(section_id));
593 if (field && field.classList.contains('hidden') == active) {
594 field.classList[active ? 'remove' : 'add']('hidden');
601 triggerValidation: function(section_id) {
602 var node = this.map.findElement('id', this.cbid(section_id));
603 return node ? L.dom.callClassMethod(node, 'triggerValidation') : true;
606 parse: function(section_id) {
607 var active = this.isActive(section_id),
608 cval = this.cfgvalue(section_id),
609 fval = active ? this.formvalue(section_id) : null;
611 if (active && !this.isValid(section_id))
612 return Promise.reject();
614 if (fval != '' && fval != null) {
615 if (this.forcewrite || !isEqual(cval, fval))
616 return Promise.resolve(this.write(section_id, fval));
619 if (this.rmempty || this.optional) {
620 return Promise.resolve(this.remove(section_id));
622 else if (!isEqual(cval, fval)) {
623 console.log('This should have been catched by isValid()');
624 return Promise.reject();
628 return Promise.resolve();
631 write: function(section_id, formvalue) {
633 this.uciconfig || this.section.uciconfig || this.map.config,
634 this.ucisection || section_id,
635 this.ucioption || this.option,
639 remove: function(section_id) {
641 this.uciconfig || this.section.uciconfig || this.map.config,
642 this.ucisection || section_id,
643 this.ucioption || this.option);
647 var CBITypedSection = CBIAbstractSection.extend({
648 __name__: 'CBI.TypedSection',
650 cfgsections: function() {
651 return uci.sections(this.uciconfig || this.map.config, this.sectiontype)
652 .map(function(s) { return s['.name'] })
653 .filter(L.bind(this.filter, this));
656 handleAdd: function(ev, name) {
657 var config_name = this.uciconfig || this.map.config;
659 uci.add(config_name, this.sectiontype, name);
663 handleRemove: function(section_id, ev) {
664 var config_name = this.uciconfig || this.map.config;
666 uci.remove(config_name, section_id);
670 renderSectionAdd: function(extra_class) {
674 var createEl = E('div', { 'class': 'cbi-section-create' }),
675 config_name = this.uciconfig || this.map.config,
676 btn_title = this.titleFn('addbtntitle');
678 if (extra_class != null)
679 createEl.classList.add(extra_class);
681 if (this.anonymous) {
682 createEl.appendChild(E('input', {
684 'class': 'cbi-button cbi-button-add',
685 'value': btn_title || _('Add'),
686 'title': btn_title || _('Add'),
687 'click': L.bind(this.handleAdd, this)
691 var nameEl = E('input', {
693 'class': 'cbi-section-create-name'
696 L.dom.append(createEl, [
697 E('div', {}, nameEl),
699 'class': 'cbi-button cbi-button-add',
701 'value': btn_title || _('Add'),
702 'title': btn_title || _('Add'),
703 'click': L.bind(function(ev) {
704 if (nameEl.classList.contains('cbi-input-invalid'))
707 this.handleAdd(ev, nameEl.value);
712 ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
718 renderSectionPlaceholder: function() {
720 E('em', _('This section contains no values yet')),
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
735 if (this.title != null && this.title != '')
736 sectionEl.appendChild(E('legend', {}, this.title));
738 if (this.description != null && this.description != '')
739 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
741 for (var i = 0; i < nodes.length; i++) {
742 if (this.addremove) {
743 sectionEl.appendChild(
744 E('div', { 'class': 'cbi-section-remove right' },
747 'class': 'cbi-button',
748 'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
749 'value': _('Delete'),
750 'data-section-id': cfgsections[i],
751 'click': L.bind(this.handleRemove, this, cfgsections[i])
756 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
758 sectionEl.appendChild(E('div', {
759 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
761 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
762 'data-section-id': cfgsections[i]
766 if (nodes.length == 0)
767 sectionEl.appendChild(this.renderSectionPlaceholder());
769 sectionEl.appendChild(this.renderSectionAdd());
771 L.dom.bindClassInstance(sectionEl, this);
777 var cfgsections = this.cfgsections(),
780 for (var i = 0; i < cfgsections.length; i++)
781 renderTasks.push(this.renderUCISection(cfgsections[i]));
783 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
787 var CBITableSection = CBITypedSection.extend({
788 __name__: 'CBI.TableSection',
791 throw 'Tabs are not supported by TableSection';
794 renderContents: function(cfgsections, nodes) {
795 var section_id = null,
796 config_name = this.uciconfig || this.map.config,
797 max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
798 has_more = max_cols < this.children.length,
799 sectionEl = E('div', {
800 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
801 'class': 'cbi-section cbi-tblsection',
802 'data-tab': this.map.tabbed ? this.sectiontype : null,
803 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
806 'class': 'table cbi-section-table'
809 if (this.title != null && this.title != '')
810 sectionEl.appendChild(E('h3', {}, this.title));
812 if (this.description != null && this.description != '')
813 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
815 tableEl.appendChild(this.renderHeaderRows(max_cols));
817 for (var i = 0; i < nodes.length; i++) {
818 var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
820 var trEl = E('div', {
821 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
822 'class': 'tr cbi-section-table-row',
823 'data-sid': cfgsections[i],
824 'draggable': this.sortable ? true : null,
825 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
826 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
827 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
828 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
829 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
830 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
831 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
832 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
833 'data-section-id': cfgsections[i]
836 if (this.extedit || this.rowcolors)
837 trEl.classList.add(!(tableEl.childNodes.length % 2)
838 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
840 for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
841 trEl.appendChild(nodes[i].firstChild);
843 trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
844 tableEl.appendChild(trEl);
847 if (nodes.length == 0)
848 tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
849 E('div', { 'class': 'td' },
850 E('em', {}, _('This section contains no values yet')))));
852 sectionEl.appendChild(tableEl);
854 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
856 L.dom.bindClassInstance(sectionEl, this);
861 renderHeaderRows: function(max_cols) {
862 var has_titles = false,
863 has_descriptions = false,
864 anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
867 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
868 if (opt.optional || opt.modalonly)
871 has_titles = has_titles || !!opt.title;
872 has_descriptions = has_descriptions || !!opt.description;
876 var trEl = E('div', {
877 'class': 'tr cbi-section-table-titles ' + anon_class,
878 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
881 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
882 if (opt.optional || opt.modalonly)
885 trEl.appendChild(E('div', {
886 'class': 'th cbi-section-table-cell',
887 'data-type': opt.__name__
890 if (opt.width != null)
891 trEl.lastElementChild.style.width =
892 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
895 trEl.lastElementChild.appendChild(E('a', {
896 'href': opt.titleref,
897 'class': 'cbi-title-ref',
898 'title': this.titledesc || _('Go to relevant configuration page')
901 L.dom.content(trEl.lastElementChild, opt.title);
904 if (this.sortable || this.extedit || this.addremove || has_more)
905 trEl.appendChild(E('div', {
906 'class': 'th cbi-section-table-cell cbi-section-actions'
909 trEls.appendChild(trEl);
912 if (has_descriptions) {
913 var trEl = E('div', {
914 'class': 'tr cbi-section-table-descr ' + anon_class
917 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
918 if (opt.optional || opt.modalonly)
921 trEl.appendChild(E('div', {
922 'class': 'th cbi-section-table-cell',
923 'data-type': opt.__name__
924 }, opt.description));
926 if (opt.width != null)
927 trEl.lastElementChild.style.width =
928 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
931 if (this.sortable || this.extedit || this.addremove || has_more)
932 trEl.appendChild(E('div', {
933 'class': 'th cbi-section-table-cell cbi-section-actions'
936 trEls.appendChild(trEl);
942 renderRowActions: function(section_id, more_label) {
943 var config_name = this.uciconfig || this.map.config;
945 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
948 var tdEl = E('div', {
949 'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
953 L.dom.append(tdEl.lastElementChild, [
955 'title': _('Drag to reorder'),
956 'class': 'cbi-button drag-handle center',
957 'style': 'cursor:move'
965 if (typeof(this.extedit) == 'function')
966 evFn = L.bind(this.extedit, this);
967 else if (typeof(this.extedit) == 'string')
968 evFn = L.bind(function(sid, ev) {
969 location.href = this.extedit.format(sid);
970 }, this, section_id);
972 L.dom.append(tdEl.lastElementChild,
977 'class': 'cbi-button cbi-button-edit',
984 L.dom.append(tdEl.lastElementChild,
989 'class': 'cbi-button cbi-button-edit',
990 'click': L.bind(this.renderMoreOptionsModal, this, section_id)
995 if (this.addremove) {
996 var btn_title = this.titleFn('removebtntitle', section_id);
998 L.dom.append(tdEl.lastElementChild,
1001 'value': btn_title || _('Delete'),
1002 'title': btn_title || _('Delete'),
1003 'class': 'cbi-button cbi-button-remove',
1004 'click': L.bind(function(sid, ev) {
1005 uci.remove(config_name, sid);
1007 }, this, section_id)
1015 handleDragInit: function(ev) {
1016 scope.dragState = { node: ev.target };
1019 handleDragStart: function(ev) {
1020 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
1021 scope.dragState = null;
1022 ev.preventDefault();
1026 scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
1027 ev.dataTransfer.setData('text', 'drag');
1028 ev.target.style.opacity = 0.4;
1031 handleDragOver: function(ev) {
1032 var n = scope.dragState.targetNode,
1033 r = scope.dragState.rect,
1034 t = r.top + r.height / 2;
1036 if (ev.clientY <= t) {
1037 n.classList.remove('drag-over-below');
1038 n.classList.add('drag-over-above');
1041 n.classList.remove('drag-over-above');
1042 n.classList.add('drag-over-below');
1045 ev.dataTransfer.dropEffect = 'move';
1046 ev.preventDefault();
1050 handleDragEnter: function(ev) {
1051 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
1052 scope.dragState.targetNode = ev.currentTarget;
1055 handleDragLeave: function(ev) {
1056 ev.currentTarget.classList.remove('drag-over-above');
1057 ev.currentTarget.classList.remove('drag-over-below');
1060 handleDragEnd: function(ev) {
1063 n.style.opacity = '';
1064 n.classList.add('flash');
1065 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
1066 .forEach(function(tr) {
1067 tr.classList.remove('drag-over-above');
1068 tr.classList.remove('drag-over-below');
1072 handleDrop: function(ev) {
1073 var s = scope.dragState;
1075 if (s.node && s.targetNode) {
1076 var config_name = this.uciconfig || this.map.config,
1077 ref_node = s.targetNode,
1080 if (ref_node.classList.contains('drag-over-below')) {
1081 ref_node = ref_node.nextElementSibling;
1085 var sid1 = s.node.getAttribute('data-sid'),
1086 sid2 = s.targetNode.getAttribute('data-sid');
1088 s.node.parentNode.insertBefore(s.node, ref_node);
1089 uci.move(config_name, sid1, sid2, after);
1092 scope.dragState = null;
1093 ev.target.style.opacity = '';
1094 ev.stopPropagation();
1095 ev.preventDefault();
1099 handleModalCancel: function(modalMap, ev) {
1100 return Promise.resolve(L.ui.hideModal());
1103 handleModalSave: function(modalMap, ev) {
1104 return modalMap.save()
1105 .then(L.bind(this.map.load, this.map))
1106 .then(L.bind(this.map.reset, this.map))
1107 .then(L.ui.hideModal)
1108 .catch(function() {});
1111 addModalOptions: function(modalSection, section_id, ev) {
1115 renderMoreOptionsModal: function(section_id, ev) {
1116 var parent = this.map,
1117 title = parent.title,
1119 m = new CBIMap(this.map.config, null, null),
1120 s = m.section(CBINamedSection, section_id, this.sectiontype);
1123 s.tab_names = this.tab_names;
1125 if ((name = this.titleFn('modaltitle', section_id)) != null)
1127 else if ((name = this.titleFn('sectiontitle', section_id)) != null)
1128 title = '%s - %s'.format(parent.title, name);
1129 else if (!this.anonymous)
1130 title = '%s - %s'.format(parent.title, section_id);
1132 for (var i = 0; i < this.children.length; i++) {
1133 var o1 = this.children[i];
1135 if (o1.modalonly === false)
1138 var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
1141 if (!o1.hasOwnProperty(k))
1158 //ev.target.classList.add('spinning');
1159 Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
1160 //ev.target.classList.remove('spinning');
1161 L.ui.showModal(title, [
1163 E('div', { 'class': 'right' }, [
1167 'click': L.bind(this.handleModalCancel, this, m),
1168 'value': _('Dismiss')
1172 'class': 'cbi-button cbi-button-positive important',
1173 'click': L.bind(this.handleModalSave, this, m),
1178 }, this)).catch(L.error);
1182 var CBIGridSection = CBITableSection.extend({
1183 tab: function(name, title, description) {
1184 CBIAbstractSection.prototype.tab.call(this, name, title, description);
1187 handleAdd: function(ev) {
1188 var config_name = this.uciconfig || this.map.config,
1189 section_id = uci.add(config_name, this.sectiontype);
1191 this.addedSection = section_id;
1192 this.renderMoreOptionsModal(section_id);
1195 handleModalSave: function(/* ... */) {
1196 return this.super('handleModalSave', arguments)
1197 .then(L.bind(function() { this.addedSection = null }, this));
1200 handleModalCancel: function(/* ... */) {
1201 var config_name = this.uciconfig || this.map.config;
1203 if (this.addedSection != null) {
1204 uci.remove(config_name, this.addedSection);
1205 this.addedSection = null;
1208 return this.super('handleModalCancel', arguments);
1211 renderUCISection: function(section_id) {
1212 return this.renderOptions(null, section_id);
1215 renderChildren: function(tab_name, section_id, in_table) {
1216 var tasks = [], index = 0;
1218 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
1219 if (opt.disable || opt.modalonly)
1223 tasks.push(opt.render(index++, section_id, in_table));
1225 tasks.push(this.renderTextValue(section_id, opt));
1228 return Promise.all(tasks);
1231 renderTextValue: function(section_id, opt) {
1232 var title = this.stripTags(opt.title).trim(),
1233 descr = this.stripTags(opt.description).trim(),
1234 value = opt.textvalue(section_id);
1237 'class': 'td cbi-value-field',
1238 'data-title': (title != '') ? title : opt.option,
1239 'data-description': (descr != '') ? descr : null,
1240 'data-name': opt.option,
1241 'data-type': opt.typename || opt.__name__
1242 }, (value != null) ? value : E('em', _('none')));
1245 renderRowActions: function(section_id) {
1246 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1250 var section_ids = this.cfgsections(),
1253 if (Array.isArray(this.children)) {
1254 for (var i = 0; i < section_ids.length; i++) {
1255 for (var j = 0; j < this.children.length; j++) {
1256 if (!this.children[j].editable || this.children[j].modalonly)
1259 tasks.push(this.children[j].parse(section_ids[i]));
1264 return Promise.all(tasks);
1268 var CBINamedSection = CBIAbstractSection.extend({
1269 __name__: 'CBI.NamedSection',
1270 __init__: function(map, section_id /*, ... */) {
1271 this.super('__init__', this.varargs(arguments, 2, map));
1273 this.section = section_id;
1276 cfgsections: function() {
1277 return [ this.section ];
1280 handleAdd: function(ev) {
1281 var section_id = this.section,
1282 config_name = this.uciconfig || this.map.config;
1284 uci.add(config_name, this.sectiontype, section_id);
1288 handleRemove: function(ev) {
1289 var section_id = this.section,
1290 config_name = this.uciconfig || this.map.config;
1292 uci.remove(config_name, section_id);
1296 renderContents: function(data) {
1297 var ucidata = data[0], nodes = data[1],
1298 section_id = this.section,
1299 config_name = this.uciconfig || this.map.config,
1300 sectionEl = E('div', {
1301 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1302 'class': 'cbi-section',
1303 'data-tab': this.map.tabbed ? this.sectiontype : null,
1304 'data-tab-title': this.map.tabbed ? this.title || this.sectiontype : null
1307 if (typeof(this.title) === 'string' && this.title !== '')
1308 sectionEl.appendChild(E('legend', {}, this.title));
1310 if (typeof(this.description) === 'string' && this.description !== '')
1311 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1314 if (this.addremove) {
1315 sectionEl.appendChild(
1316 E('div', { 'class': 'cbi-section-remove right' },
1319 'class': 'cbi-button',
1320 'value': _('Delete'),
1321 'click': L.bind(this.handleRemove, this)
1325 sectionEl.appendChild(E('div', {
1326 'id': 'cbi-%s-%s'.format(config_name, section_id),
1328 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
1329 'data-section-id': section_id
1332 else if (this.addremove) {
1333 sectionEl.appendChild(
1336 'class': 'cbi-button cbi-button-add',
1338 'click': L.bind(this.handleAdd, this)
1342 L.dom.bindClassInstance(sectionEl, this);
1347 render: function() {
1348 var config_name = this.uciconfig || this.map.config,
1349 section_id = this.section;
1351 return Promise.all([
1352 uci.get(config_name, section_id),
1353 this.renderUCISection(section_id)
1354 ]).then(this.renderContents.bind(this));
1358 var CBIValue = CBIAbstractValue.extend({
1359 __name__: 'CBI.Value',
1361 value: function(key, val) {
1362 this.keylist = this.keylist || [];
1363 this.keylist.push(String(key));
1365 this.vallist = this.vallist || [];
1366 this.vallist.push(String(val != null ? val : key));
1369 render: function(option_index, section_id, in_table) {
1370 return Promise.resolve(this.cfgvalue(section_id))
1371 .then(this.renderWidget.bind(this, section_id, option_index))
1372 .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1375 renderFrame: function(section_id, in_table, option_index, nodes) {
1376 var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
1377 depend_list = this.transformDepList(section_id),
1381 optionEl = E('div', {
1382 'class': 'td cbi-value-field',
1383 'data-title': this.stripTags(this.title).trim(),
1384 'data-description': this.stripTags(this.description).trim(),
1385 'data-name': this.option,
1386 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1388 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1389 'data-index': option_index,
1390 'data-depends': depend_list,
1391 'data-field': this.cbid(section_id)
1395 optionEl = E('div', {
1396 'class': 'cbi-value',
1397 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1398 'data-index': option_index,
1399 'data-depends': depend_list,
1400 'data-field': this.cbid(section_id),
1401 'data-name': this.option,
1402 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1405 if (this.last_child)
1406 optionEl.classList.add('cbi-value-last');
1408 if (typeof(this.title) === 'string' && this.title !== '') {
1409 optionEl.appendChild(E('label', {
1410 'class': 'cbi-value-title',
1411 'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option)
1413 this.titleref ? E('a', {
1414 'class': 'cbi-title-ref',
1415 'href': this.titleref,
1416 'title': this.titledesc || _('Go to relevant configuration page')
1417 }, this.title) : this.title));
1419 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1424 (optionEl.lastChild || optionEl).appendChild(nodes);
1426 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1427 L.dom.append(optionEl.lastChild || optionEl,
1428 E('div', { 'class': 'cbi-value-description' }, this.description));
1430 if (depend_list && depend_list.length)
1431 optionEl.classList.add('hidden');
1433 optionEl.addEventListener('widget-change',
1434 L.bind(this.map.checkDepends, this.map));
1436 L.dom.bindClassInstance(optionEl, this);
1441 renderWidget: function(section_id, option_index, cfgvalue) {
1442 var value = (cfgvalue != null) ? cfgvalue : this.default,
1443 choices = this.transformChoices(),
1447 var placeholder = (this.optional || this.rmempty)
1448 ? E('em', _('unspecified')) : _('-- Please choose --');
1450 widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1451 id: this.cbid(section_id),
1453 optional: this.optional || this.rmempty,
1454 datatype: this.datatype,
1455 select_placeholder: this.placeholder || placeholder,
1456 validate: L.bind(this.validate, this, section_id)
1460 widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1461 id: this.cbid(section_id),
1462 password: this.password,
1463 optional: this.optional || this.rmempty,
1464 datatype: this.datatype,
1465 placeholder: this.placeholder,
1466 validate: L.bind(this.validate, this, section_id)
1470 return widget.render();
1474 var CBIDynamicList = CBIValue.extend({
1475 __name__: 'CBI.DynamicList',
1477 renderWidget: function(section_id, option_index, cfgvalue) {
1478 var value = (cfgvalue != null) ? cfgvalue : this.default,
1479 choices = this.transformChoices(),
1480 items = L.toArray(value);
1482 var widget = new ui.DynamicList(items, choices, {
1483 id: this.cbid(section_id),
1485 optional: this.optional || this.rmempty,
1486 datatype: this.datatype,
1487 placeholder: this.placeholder,
1488 validate: L.bind(this.validate, this, section_id)
1491 return widget.render();
1495 var CBIListValue = CBIValue.extend({
1496 __name__: 'CBI.ListValue',
1498 __init__: function() {
1499 this.super('__init__', arguments);
1500 this.widget = 'select';
1504 renderWidget: function(section_id, option_index, cfgvalue) {
1505 var choices = this.transformChoices();
1506 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1507 id: this.cbid(section_id),
1510 optional: this.optional,
1511 placeholder: this.placeholder,
1512 validate: L.bind(this.validate, this, section_id)
1515 return widget.render();
1519 var CBIFlagValue = CBIValue.extend({
1520 __name__: 'CBI.FlagValue',
1522 __init__: function() {
1523 this.super('__init__', arguments);
1526 this.disabled = '0';
1527 this.default = this.disabled;
1530 renderWidget: function(section_id, option_index, cfgvalue) {
1531 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1532 id: this.cbid(section_id),
1533 value_enabled: this.enabled,
1534 value_disabled: this.disabled,
1535 validate: L.bind(this.validate, this, section_id)
1538 return widget.render();
1541 formvalue: function(section_id) {
1542 var node = this.map.findElement('id', this.cbid(section_id)),
1543 checked = node ? L.dom.callClassMethod(node, 'isChecked') : false;
1545 return checked ? this.enabled : this.disabled;
1548 textvalue: function(section_id) {
1549 var cval = this.cfgvalue(section_id);
1552 cval = this.default;
1554 return (cval == this.enabled) ? _('Yes') : _('No');
1557 parse: function(section_id) {
1558 if (this.isActive(section_id)) {
1559 var fval = this.formvalue(section_id);
1561 if (!this.isValid(section_id))
1562 return Promise.reject();
1564 if (fval == this.default && (this.optional || this.rmempty))
1565 return Promise.resolve(this.remove(section_id));
1567 return Promise.resolve(this.write(section_id, fval));
1570 return Promise.resolve(this.remove(section_id));
1575 var CBIMultiValue = CBIDynamicList.extend({
1576 __name__: 'CBI.MultiValue',
1578 __init__: function() {
1579 this.super('__init__', arguments);
1580 this.placeholder = _('-- Please choose --');
1583 renderWidget: function(section_id, option_index, cfgvalue) {
1584 var value = (cfgvalue != null) ? cfgvalue : this.default,
1585 choices = this.transformChoices();
1587 var widget = new ui.Dropdown(L.toArray(value), choices, {
1588 id: this.cbid(section_id),
1591 optional: this.optional || this.rmempty,
1592 select_placeholder: this.placeholder,
1593 display_items: this.display_size || this.size || 3,
1594 dropdown_items: this.dropdown_size || this.size || -1,
1595 validate: L.bind(this.validate, this, section_id)
1598 return widget.render();
1602 var CBIDummyValue = CBIValue.extend({
1603 __name__: 'CBI.DummyValue',
1605 renderWidget: function(section_id, option_index, cfgvalue) {
1606 var value = (cfgvalue != null) ? cfgvalue : this.default,
1607 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1608 outputEl = E('div');
1611 outputEl.appendChild(E('a', { 'href': this.href }));
1613 L.dom.append(outputEl.lastChild || outputEl,
1614 this.rawhtml ? value : [ value ]);
1623 var CBIButtonValue = CBIValue.extend({
1624 __name__: 'CBI.ButtonValue',
1626 renderWidget: function(section_id, option_index, cfgvalue) {
1627 var value = (cfgvalue != null) ? cfgvalue : this.default,
1628 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1629 outputEl = E('div'),
1630 btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
1632 if (value !== false)
1633 L.dom.content(outputEl, [
1635 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1638 'click': L.bind(this.onclick || function(ev) {
1639 ev.target.previousElementSibling.value = ev.target.value;
1645 L.dom.content(outputEl, ' - ');
1654 var CBIHiddenValue = CBIValue.extend({
1655 __name__: 'CBI.HiddenValue',
1657 renderWidget: function(section_id, option_index, cfgvalue) {
1658 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1659 id: this.cbid(section_id)
1662 return widget.render();
1666 var CBISectionValue = CBIValue.extend({
1667 __name__: 'CBI.ContainerValue',
1668 __init__: function(map, section, option, cbiClass /*, ... */) {
1669 this.super('__init__', [map, section, option]);
1671 if (!CBIAbstractSection.isSubclass(cbiClass))
1672 throw 'Sub section must be a descendent of CBIAbstractSection';
1674 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1677 load: function(section_id) {
1678 return this.subsection.load();
1681 parse: function(section_id) {
1682 return this.subsection.parse();
1685 renderWidget: function(section_id, option_index, cfgvalue) {
1686 return this.subsection.render();
1689 checkDepends: function(section_id) {
1690 this.subsection.checkDepends();
1691 return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
1694 write: function() {},
1695 remove: function() {},
1696 cfgvalue: function() { return null },
1697 formvalue: function() { return null }
1700 return L.Class.extend({
1702 AbstractSection: CBIAbstractSection,
1703 AbstractValue: CBIAbstractValue,
1705 TypedSection: CBITypedSection,
1706 TableSection: CBITableSection,
1707 GridSection: CBIGridSection,
1708 NamedSection: CBINamedSection,
1711 DynamicList: CBIDynamicList,
1712 ListValue: CBIListValue,
1714 MultiValue: CBIMultiValue,
1715 DummyValue: CBIDummyValue,
1716 Button: CBIButtonValue,
1717 HiddenValue: CBIHiddenValue,
1718 SectionValue: CBISectionValue