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 || '';
63 var CBIMap = CBINode.extend({
64 __init__: function(config /*, ... */) {
65 this.super('__init__', this.varargs(arguments, 1));
68 this.parsechain = [ config ];
71 findElements: function(/* ... */) {
74 if (arguments.length == 1)
76 else if (arguments.length == 2)
77 q = '[%s="%s"]'.format(arguments[0], arguments[1]);
79 L.error('InternalError', 'Expecting one or two arguments to findElements()');
81 return this.root.querySelectorAll(q);
84 findElement: function(/* ... */) {
85 var res = this.findElements.apply(this, arguments);
86 return res.length ? res[0] : null;
89 chain: function(config) {
90 if (this.parsechain.indexOf(config) == -1)
91 this.parsechain.push(config);
94 section: function(cbiClass /*, ... */) {
95 if (!CBIAbstractSection.isSubclass(cbiClass))
96 L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
98 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
104 return uci.load(this.parsechain || [ this.config ])
105 .then(this.loadChildren.bind(this));
111 if (Array.isArray(this.children))
112 for (var i = 0; i < this.children.length; i++)
113 tasks.push(this.children[i].parse());
115 return Promise.all(tasks);
122 .then(uci.save.bind(uci))
123 .then(this.load.bind(this))
124 .then(this.renderContents.bind(this))
126 alert('Cannot save due to invalid values')
127 return Promise.reject();
132 return this.renderContents();
136 return this.load().then(this.renderContents.bind(this));
139 renderContents: function() {
140 var mapEl = this.root || (this.root = E('div', {
141 'id': 'cbi-%s'.format(this.config),
143 'cbi-dependency-check': L.bind(this.checkDepends, this)
146 L.dom.bindClassInstance(mapEl, this);
148 return this.renderChildren(null).then(L.bind(function(nodes) {
149 var initialRender = !mapEl.firstChild;
151 L.dom.content(mapEl, null);
153 if (this.title != null && this.title != '')
154 mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
156 if (this.description != null && this.description != '')
157 mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
159 L.dom.append(mapEl, nodes);
161 if (!initialRender) {
162 mapEl.classList.remove('flash');
164 window.setTimeout(function() {
165 mapEl.classList.add('flash');
175 lookupOption: function(name, section_id) {
176 var id, elem, sid, inst;
178 if (name.indexOf('.') > -1)
179 id = 'cbid.%s'.format(name);
181 id = 'cbid.%s.%s.%s'.format(this.config, section_id, name);
183 elem = this.findElement('data-field', id);
184 sid = elem ? id.split(/\./)[2] : null;
185 inst = elem ? L.dom.findClassInstance(elem) : null;
187 return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
190 checkDepends: function(ev, n) {
193 for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
194 if (s.checkDepends(ev, n))
197 if (changed && (n || 0) < 10)
198 this.checkDepends(ev, (n || 10) + 1);
200 ui.tabs.updateTabs(ev, this.root);
204 var CBIAbstractSection = CBINode.extend({
205 __init__: function(map, sectionType /*, ... */) {
206 this.super('__init__', this.varargs(arguments, 2));
208 this.sectiontype = sectionType;
210 this.config = map.config;
212 this.optional = true;
213 this.addremove = false;
214 this.dynamic = false;
217 cfgsections: function() {
218 L.error('InternalError', 'Not implemented');
221 filter: function(section_id) {
226 var section_ids = this.cfgsections(),
229 if (Array.isArray(this.children))
230 for (var i = 0; i < section_ids.length; i++)
231 tasks.push(this.loadChildren(section_ids[i])
232 .then(Function.prototype.bind.call(function(section_id, set_values) {
233 for (var i = 0; i < set_values.length; i++)
234 this.children[i].cfgvalue(section_id, set_values[i]);
235 }, this, section_ids[i])));
237 return Promise.all(tasks);
241 var section_ids = this.cfgsections(),
244 if (Array.isArray(this.children))
245 for (var i = 0; i < section_ids.length; i++)
246 for (var j = 0; j < this.children.length; j++)
247 tasks.push(this.children[j].parse(section_ids[i]));
249 return Promise.all(tasks);
252 tab: function(name, title, description) {
253 if (this.tabs && this.tabs[name])
254 throw 'Tab already declared';
259 description: description,
263 this.tabs = this.tabs || [];
264 this.tabs.push(entry);
265 this.tabs[name] = entry;
267 this.tab_names = this.tab_names || [];
268 this.tab_names.push(name);
271 option: function(cbiClass /*, ... */) {
272 if (!CBIAbstractValue.isSubclass(cbiClass))
273 throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
275 var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
280 taboption: function(tabName /*, ... */) {
281 if (!this.tabs || !this.tabs[tabName])
282 throw L.error('ReferenceError', 'Associated tab not declared');
284 var obj = this.option.apply(this, this.varargs(arguments, 1));
286 this.tabs[tabName].children.push(obj);
290 renderUCISection: function(section_id) {
291 var renderTasks = [];
294 return this.renderOptions(null, section_id);
296 for (var i = 0; i < this.tab_names.length; i++)
297 renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
299 return Promise.all(renderTasks)
300 .then(this.renderTabContainers.bind(this, section_id));
303 renderTabContainers: function(section_id, nodes) {
304 var config_name = this.uciconfig || this.map.config,
305 containerEls = E([]);
307 for (var i = 0; i < nodes.length; i++) {
308 var tab_name = this.tab_names[i],
309 tab_data = this.tabs[tab_name],
310 containerEl = E('div', {
311 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
312 'data-tab': tab_name,
313 'data-tab-title': tab_data.title,
314 'data-tab-active': tab_name === this.selected_tab
317 if (tab_data.description != null && tab_data.description != '')
318 containerEl.appendChild(
319 E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
321 containerEl.appendChild(nodes[i]);
322 containerEls.appendChild(containerEl);
328 renderOptions: function(tab_name, section_id) {
329 var in_table = (this instanceof CBITableSection);
330 return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
331 var optionEls = E([]);
332 for (var i = 0; i < nodes.length; i++)
333 optionEls.appendChild(nodes[i]);
338 checkDepends: function(ev, n) {
340 sids = this.cfgsections();
342 for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
343 for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
344 var isActive = o.isActive(sid),
345 isSatisified = o.checkDepends(sid);
347 if (isActive != isSatisified) {
348 o.setActive(sid, !isActive);
353 o.triggerValidation(sid);
362 var isEqual = function(x, y) {
363 if (x != null && y != null && typeof(x) != typeof(y))
366 if ((x == null && y != null) || (x != null && y == null))
369 if (Array.isArray(x)) {
370 if (x.length != y.length)
373 for (var i = 0; i < x.length; i++)
374 if (!isEqual(x[i], y[i]))
377 else if (typeof(x) == 'object') {
379 if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
382 if (!isEqual(x[k], y[k]))
387 if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
397 var CBIAbstractValue = CBINode.extend({
398 __init__: function(map, section, option /*, ... */) {
399 this.super('__init__', this.varargs(arguments, 3));
401 this.section = section;
402 this.option = option;
404 this.config = map.config;
411 this.optional = false;
414 depends: function(field, value) {
417 if (typeof(field) === 'string')
418 deps = {}, deps[field] = value;
422 this.deps.push(deps);
425 transformDepList: function(section_id, deplist) {
426 var list = deplist || this.deps,
429 if (Array.isArray(list)) {
430 for (var i = 0; i < list.length; i++) {
433 for (var k in list[i]) {
434 if (list[i].hasOwnProperty(k)) {
435 if (k.charAt(0) === '!')
437 else if (k.indexOf('.') !== -1)
438 dep['cbid.%s'.format(k)] = list[i][k];
440 dep['cbid.%s.%s.%s'.format(this.config, this.ucisection || section_id, k)] = list[i][k];
445 if (dep.hasOwnProperty(k)) {
456 transformChoices: function() {
457 if (!Array.isArray(this.keylist) || this.keylist.length == 0)
462 for (var i = 0; i < this.keylist.length; i++)
463 choices[this.keylist[i]] = this.vallist[i];
468 checkDepends: function(section_id) {
471 if (!Array.isArray(this.deps) || !this.deps.length)
474 for (var i = 0; i < this.deps.length; i++) {
478 for (var dep in this.deps[i]) {
479 if (dep == '!reverse') {
482 else if (dep == '!default') {
487 var res = this.map.lookupOption(dep, section_id),
488 val = res ? res[0].formvalue(res[1]) : null;
490 istat = (istat && isEqual(val, this.deps[i][dep]));
501 cbid: function(section_id) {
502 if (section_id == null)
503 L.error('TypeError', 'Section ID required');
505 return 'cbid.%s.%s.%s'.format(this.map.config, section_id, this.option);
508 load: function(section_id) {
509 if (section_id == null)
510 L.error('TypeError', 'Section ID required');
513 this.uciconfig || this.map.config,
514 this.ucisection || section_id,
515 this.ucioption || this.option);
518 cfgvalue: function(section_id, set_value) {
519 if (section_id == null)
520 L.error('TypeError', 'Section ID required');
522 if (arguments.length == 2) {
523 this.data = this.data || {};
524 this.data[section_id] = set_value;
527 return this.data ? this.data[section_id] : null;
530 formvalue: function(section_id) {
531 var node = this.map.findElement('id', this.cbid(section_id));
532 return node ? L.dom.callClassMethod(node, 'getValue') : null;
535 textvalue: function(section_id) {
536 var cval = this.cfgvalue(section_id);
541 return (cval != null) ? '%h'.format(cval) : null;
544 validate: function(section_id, value) {
548 isValid: function(section_id) {
549 var node = this.map.findElement('id', this.cbid(section_id));
550 return node ? L.dom.callClassMethod(node, 'isValid') : true;
553 isActive: function(section_id) {
554 var field = this.map.findElement('data-field', this.cbid(section_id));
555 return (field != null && !field.classList.contains('hidden'));
558 setActive: function(section_id, active) {
559 var field = this.map.findElement('data-field', this.cbid(section_id));
561 if (field && field.classList.contains('hidden') == active) {
562 field.classList[active ? 'remove' : 'add']('hidden');
569 triggerValidation: function(section_id) {
570 var node = this.map.findElement('id', this.cbid(section_id));
571 return node ? L.dom.callClassMethod(node, 'triggerValidation') : true;
574 parse: function(section_id) {
575 var active = this.isActive(section_id),
576 cval = this.cfgvalue(section_id),
577 fval = active ? this.formvalue(section_id) : null;
579 if (active && !this.isValid(section_id))
580 return Promise.reject();
582 if (fval != '' && fval != null) {
583 if (this.forcewrite || !isEqual(cval, fval))
584 return Promise.resolve(this.write(section_id, fval));
587 if (this.rmempty || this.optional) {
588 return Promise.resolve(this.remove(section_id));
590 else if (!isEqual(cval, fval)) {
591 console.log('This should have been catched by isValid()');
592 return Promise.reject();
596 return Promise.resolve();
599 write: function(section_id, formvalue) {
601 this.uciconfig || this.map.config,
602 this.ucisection || section_id,
603 this.ucioption || this.option,
607 remove: function(section_id) {
609 this.uciconfig || this.map.config,
610 this.ucisection || section_id,
611 this.ucioption || this.option);
615 var CBITypedSection = CBIAbstractSection.extend({
616 __name__: 'CBI.TypedSection',
618 cfgsections: function() {
619 return uci.sections(this.uciconfig || this.map.config, this.sectiontype)
620 .map(function(s) { return s['.name'] })
621 .filter(L.bind(this.filter, this));
624 handleAdd: function(ev, name) {
625 var config_name = this.uciconfig || this.map.config;
627 uci.add(config_name, this.sectiontype, name);
631 handleRemove: function(section_id, ev) {
632 var config_name = this.uciconfig || this.map.config;
634 uci.remove(config_name, section_id);
638 renderSectionAdd: function(extra_class) {
642 var createEl = E('div', { 'class': 'cbi-section-create' }),
643 config_name = this.uciconfig || this.map.config;
645 if (extra_class != null)
646 createEl.classList.add(extra_class);
648 if (this.anonymous) {
649 createEl.appendChild(E('input', {
651 'class': 'cbi-button cbi-button-add',
654 'click': L.bind(this.handleAdd, this)
658 var nameEl = E('input', {
660 'class': 'cbi-section-create-name'
663 L.dom.append(createEl, [
664 E('div', {}, nameEl),
666 'class': 'cbi-button cbi-button-add',
670 'click': L.bind(function(ev) {
671 if (nameEl.classList.contains('cbi-input-invalid'))
674 this.handleAdd(ev, nameEl.value);
679 ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
685 renderContents: function(cfgsections, nodes) {
686 var section_id = null,
687 config_name = this.uciconfig || this.map.config,
688 sectionEl = E('div', {
689 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
690 'class': 'cbi-section'
693 if (this.title != null && this.title != '')
694 sectionEl.appendChild(E('legend', {}, this.title));
696 if (this.description != null && this.description != '')
697 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
699 for (var i = 0; i < nodes.length; i++) {
700 if (this.addremove) {
701 sectionEl.appendChild(
702 E('div', { 'class': 'cbi-section-remove right' },
705 'class': 'cbi-button',
706 'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
707 'value': _('Delete'),
708 'data-section-id': cfgsections[i],
709 'click': L.bind(this.handleRemove, this, cfgsections[i])
714 sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
716 sectionEl.appendChild(E('div', {
717 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
719 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node'
723 ui.tabs.initTabGroup(sectionEl.lastChild.childNodes);
726 if (nodes.length == 0)
727 L.dom.append(sectionEl, [
728 E('em', _('This section contains no values yet')),
732 sectionEl.appendChild(this.renderSectionAdd());
734 L.dom.bindClassInstance(sectionEl, this);
740 var cfgsections = this.cfgsections(),
743 for (var i = 0; i < cfgsections.length; i++)
744 renderTasks.push(this.renderUCISection(cfgsections[i]));
746 return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
750 var CBITableSection = CBITypedSection.extend({
751 __name__: 'CBI.TableSection',
754 throw 'Tabs are not supported by TableSection';
757 renderContents: function(cfgsections, nodes) {
758 var section_id = null,
759 config_name = this.uciconfig || this.map.config,
760 max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
761 has_more = max_cols < this.children.length,
762 sectionEl = E('div', {
763 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
764 'class': 'cbi-section cbi-tblsection'
767 'class': 'table cbi-section-table'
770 if (this.title != null && this.title != '')
771 sectionEl.appendChild(E('h3', {}, this.title));
773 if (this.description != null && this.description != '')
774 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
776 tableEl.appendChild(this.renderHeaderRows(max_cols));
778 for (var i = 0; i < nodes.length; i++) {
779 var sectionname = this.stripTags((typeof(this.sectiontitle) == 'function')
780 ? String(this.sectiontitle(cfgsections[i]) || '') : cfgsections[i]).trim();
782 var trEl = E('div', {
783 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
784 'class': 'tr cbi-section-table-row',
785 'data-sid': cfgsections[i],
786 'draggable': this.sortable ? true : null,
787 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
788 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
789 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
790 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
791 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
792 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
793 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
794 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null
797 if (this.extedit || this.rowcolors)
798 trEl.classList.add(!(tableEl.childNodes.length % 2)
799 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
801 for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
802 trEl.appendChild(nodes[i].firstChild);
804 trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
805 tableEl.appendChild(trEl);
808 if (nodes.length == 0)
809 tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
810 E('div', { 'class': 'td' },
811 E('em', {}, _('This section contains no values yet')))));
813 sectionEl.appendChild(tableEl);
815 sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
817 L.dom.bindClassInstance(sectionEl, this);
822 renderHeaderRows: function(max_cols) {
823 var has_titles = false,
824 has_descriptions = false,
825 anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
828 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
829 if (opt.optional || opt.modalonly)
832 has_titles = has_titles || !!opt.title;
833 has_descriptions = has_descriptions || !!opt.description;
837 var trEl = E('div', {
838 'class': 'tr cbi-section-table-titles ' + anon_class,
839 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
842 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
843 if (opt.optional || opt.modalonly)
846 trEl.appendChild(E('div', {
847 'class': 'th cbi-section-table-cell',
848 'data-type': opt.__name__
851 if (opt.width != null)
852 trEl.lastElementChild.style.width =
853 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
856 trEl.lastElementChild.appendChild(E('a', {
857 'href': opt.titleref,
858 'class': 'cbi-title-ref',
859 'title': this.titledesc || _('Go to relevant configuration page')
862 L.dom.content(trEl.lastElementChild, opt.title);
865 if (this.sortable || this.extedit || this.addremove || has_more)
866 trEl.appendChild(E('div', {
867 'class': 'th cbi-section-table-cell cbi-section-actions'
870 trEls.appendChild(trEl);
873 if (has_descriptions) {
874 var trEl = E('div', {
875 'class': 'tr cbi-section-table-descr ' + anon_class
878 for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
879 if (opt.optional || opt.modalonly)
882 trEl.appendChild(E('div', {
883 'class': 'th cbi-section-table-cell',
884 'data-type': opt.__name__
885 }, opt.description));
887 if (opt.width != null)
888 trEl.lastElementChild.style.width =
889 (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
892 if (this.sortable || this.extedit || this.addremove || has_more)
893 trEl.appendChild(E('div', {
894 'class': 'th cbi-section-table-cell cbi-section-actions'
897 trEls.appendChild(trEl);
903 renderRowActions: function(section_id, more_label) {
904 var config_name = this.uciconfig || this.map.config;
906 if (!this.sortable && !this.extedit && !this.addremove && !more_label)
909 var tdEl = E('div', {
910 'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
914 L.dom.append(tdEl.lastElementChild, [
916 'title': _('Drag to reorder'),
917 'class': 'cbi-button drag-handle center',
918 'style': 'cursor:move'
926 if (typeof(this.extedit) == 'function')
927 evFn = L.bind(this.extedit, this);
928 else if (typeof(this.extedit) == 'string')
929 evFn = L.bind(function(sid, ev) {
930 location.href = this.extedit.format(sid);
931 }, this, section_id);
933 L.dom.append(tdEl.lastElementChild,
938 'class': 'cbi-button cbi-button-edit',
945 L.dom.append(tdEl.lastElementChild,
950 'class': 'cbi-button cbi-button-edit',
951 'click': L.bind(this.renderMoreOptionsModal, this, section_id)
956 if (this.addremove) {
957 L.dom.append(tdEl.lastElementChild,
960 'value': _('Delete'),
961 'title': _('Delete'),
962 'class': 'cbi-button cbi-button-remove',
963 'click': L.bind(function(sid, ev) {
964 uci.remove(config_name, sid);
974 handleDragInit: function(ev) {
975 scope.dragState = { node: ev.target };
978 handleDragStart: function(ev) {
979 if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
980 scope.dragState = null;
985 scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
986 ev.dataTransfer.setData('text', 'drag');
987 ev.target.style.opacity = 0.4;
990 handleDragOver: function(ev) {
991 var n = scope.dragState.targetNode,
992 r = scope.dragState.rect,
993 t = r.top + r.height / 2;
995 if (ev.clientY <= t) {
996 n.classList.remove('drag-over-below');
997 n.classList.add('drag-over-above');
1000 n.classList.remove('drag-over-above');
1001 n.classList.add('drag-over-below');
1004 ev.dataTransfer.dropEffect = 'move';
1005 ev.preventDefault();
1009 handleDragEnter: function(ev) {
1010 scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
1011 scope.dragState.targetNode = ev.currentTarget;
1014 handleDragLeave: function(ev) {
1015 ev.currentTarget.classList.remove('drag-over-above');
1016 ev.currentTarget.classList.remove('drag-over-below');
1019 handleDragEnd: function(ev) {
1022 n.style.opacity = '';
1023 n.classList.add('flash');
1024 n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
1025 .forEach(function(tr) {
1026 tr.classList.remove('drag-over-above');
1027 tr.classList.remove('drag-over-below');
1031 handleDrop: function(ev) {
1032 var s = scope.dragState;
1034 if (s.node && s.targetNode) {
1035 var config_name = this.uciconfig || this.map.config,
1036 ref_node = s.targetNode,
1039 if (ref_node.classList.contains('drag-over-below')) {
1040 ref_node = ref_node.nextElementSibling;
1044 var sid1 = s.node.getAttribute('data-sid'),
1045 sid2 = s.targetNode.getAttribute('data-sid');
1047 s.node.parentNode.insertBefore(s.node, ref_node);
1048 uci.move(config_name, sid1, sid2, after);
1051 scope.dragState = null;
1052 ev.target.style.opacity = '';
1053 ev.stopPropagation();
1054 ev.preventDefault();
1058 handleModalCancel: function(modalMap, ev) {
1059 return Promise.resolve(L.ui.hideModal());
1062 handleModalSave: function(modalMap, ev) {
1063 return modalMap.save()
1064 .then(L.bind(this.map.load, this.map))
1065 .then(L.bind(this.map.reset, this.map))
1066 .then(L.ui.hideModal)
1067 .catch(function() {});
1070 addModalOptions: function(modalSection, section_id, ev) {
1074 renderMoreOptionsModal: function(section_id, ev) {
1075 var parent = this.map,
1076 title = parent.title,
1078 m = new CBIMap(this.map.config, null, null),
1079 s = m.section(CBINamedSection, section_id, this.sectiontype);
1081 s.tab_names = this.tab_names;
1083 if (typeof(this.sectiontitle) == 'function')
1084 name = this.stripTags(String(this.sectiontitle(section_id) || ''));
1085 else if (!this.anonymous)
1089 title += ' - ' + name;
1091 for (var i = 0; i < this.children.length; i++) {
1092 var o1 = this.children[i];
1094 if (o1.modalonly === false)
1097 var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
1100 if (!o1.hasOwnProperty(k))
1117 //ev.target.classList.add('spinning');
1118 Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
1119 //ev.target.classList.remove('spinning');
1120 L.ui.showModal(title, [
1122 E('div', { 'class': 'right' }, [
1126 'click': L.bind(this.handleModalCancel, this, m),
1127 'value': _('Dismiss')
1131 'class': 'cbi-button cbi-button-positive important',
1132 'click': L.bind(this.handleModalSave, this, m),
1137 }, this)).catch(L.error);
1141 var CBIGridSection = CBITableSection.extend({
1142 tab: function(name, title, description) {
1143 CBIAbstractSection.prototype.tab.call(this, name, title, description);
1146 handleAdd: function(ev) {
1147 var config_name = this.uciconfig || this.map.config,
1148 section_id = uci.add(config_name, this.sectiontype);
1150 this.addedSection = section_id;
1151 this.renderMoreOptionsModal(section_id);
1154 handleModalSave: function(/* ... */) {
1155 return this.super('handleModalSave', arguments)
1156 .then(L.bind(function() { this.addedSection = null }, this));
1159 handleModalCancel: function(/* ... */) {
1160 var config_name = this.uciconfig || this.map.config;
1162 if (this.addedSection != null) {
1163 uci.remove(config_name, this.addedSection);
1164 this.addedSection = null;
1167 return this.super('handleModalCancel', arguments);
1170 renderUCISection: function(section_id) {
1171 return this.renderOptions(null, section_id);
1174 renderChildren: function(tab_name, section_id, in_table) {
1175 var tasks = [], index = 0;
1177 for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
1178 if (opt.disable || opt.modalonly)
1182 tasks.push(opt.render(index++, section_id, in_table));
1184 tasks.push(this.renderTextValue(section_id, opt));
1187 return Promise.all(tasks);
1190 renderTextValue: function(section_id, opt) {
1191 var title = this.stripTags(opt.title).trim(),
1192 descr = this.stripTags(opt.description).trim(),
1193 value = opt.textvalue(section_id);
1196 'class': 'td cbi-value-field',
1197 'data-title': (title != '') ? title : opt.option,
1198 'data-description': (descr != '') ? descr : null,
1199 'data-name': opt.option,
1200 'data-type': opt.typename || opt.__name__
1201 }, (value != null) ? value : E('em', _('none')));
1204 renderRowActions: function(section_id) {
1205 return this.super('renderRowActions', [ section_id, _('Edit') ]);
1209 var section_ids = this.cfgsections(),
1212 if (Array.isArray(this.children)) {
1213 for (var i = 0; i < section_ids.length; i++) {
1214 for (var j = 0; j < this.children.length; j++) {
1215 if (!this.children[j].editable || this.children[j].modalonly)
1218 tasks.push(this.children[j].parse(section_ids[i]));
1223 return Promise.all(tasks);
1227 var CBINamedSection = CBIAbstractSection.extend({
1228 __name__: 'CBI.NamedSection',
1229 __init__: function(map, section_id /*, ... */) {
1230 this.super('__init__', this.varargs(arguments, 2, map));
1232 this.section = section_id;
1235 cfgsections: function() {
1236 return [ this.section ];
1239 handleAdd: function(ev) {
1240 var section_id = this.section,
1241 config_name = this.uciconfig || this.map.config;
1243 uci.add(config_name, this.sectiontype, section_id);
1247 handleRemove: function(ev) {
1248 var section_id = this.section,
1249 config_name = this.uciconfig || this.map.config;
1251 uci.remove(config_name, section_id);
1255 renderContents: function(data) {
1256 var ucidata = data[0], nodes = data[1],
1257 section_id = this.section,
1258 config_name = this.uciconfig || this.map.config,
1259 sectionEl = E('div', {
1260 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
1261 'class': 'cbi-section'
1264 if (typeof(this.title) === 'string' && this.title !== '')
1265 sectionEl.appendChild(E('legend', {}, this.title));
1267 if (typeof(this.description) === 'string' && this.description !== '')
1268 sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
1271 if (this.addremove) {
1272 sectionEl.appendChild(
1273 E('div', { 'class': 'cbi-section-remove right' },
1276 'class': 'cbi-button',
1277 'value': _('Delete'),
1278 'click': L.bind(this.handleRemove, this)
1282 sectionEl.appendChild(E('div', {
1283 'id': 'cbi-%s-%s'.format(config_name, section_id),
1285 ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node'
1289 ui.tabs.initTabGroup(sectionEl.lastChild.childNodes);
1291 else if (this.addremove) {
1292 sectionEl.appendChild(
1295 'class': 'cbi-button cbi-button-add',
1297 'click': L.bind(this.handleAdd, this)
1301 L.dom.bindClassInstance(sectionEl, this);
1306 render: function() {
1307 var config_name = this.uciconfig || this.map.config,
1308 section_id = this.section;
1310 return Promise.all([
1311 uci.get(config_name, section_id),
1312 this.renderUCISection(section_id)
1313 ]).then(this.renderContents.bind(this));
1317 var CBIValue = CBIAbstractValue.extend({
1318 __name__: 'CBI.Value',
1320 value: function(key, val) {
1321 this.keylist = this.keylist || [];
1322 this.keylist.push(String(key));
1324 this.vallist = this.vallist || [];
1325 this.vallist.push(String(val != null ? val : key));
1328 render: function(option_index, section_id, in_table) {
1329 return Promise.resolve(this.cfgvalue(section_id))
1330 .then(this.renderWidget.bind(this, section_id, option_index))
1331 .then(this.renderFrame.bind(this, section_id, in_table, option_index));
1334 renderFrame: function(section_id, in_table, option_index, nodes) {
1335 var config_name = this.uciconfig || this.map.config,
1336 depend_list = this.transformDepList(section_id),
1340 optionEl = E('div', {
1341 'class': 'td cbi-value-field',
1342 'data-title': this.stripTags(this.title).trim(),
1343 'data-description': this.stripTags(this.description).trim(),
1344 'data-name': this.option,
1345 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
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)
1354 optionEl = E('div', {
1355 'class': 'cbi-value',
1356 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
1357 'data-index': option_index,
1358 'data-depends': depend_list,
1359 'data-field': this.cbid(section_id),
1360 'data-name': this.option,
1361 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
1364 if (this.last_child)
1365 optionEl.classList.add('cbi-value-last');
1367 if (typeof(this.title) === 'string' && this.title !== '') {
1368 optionEl.appendChild(E('label', {
1369 'class': 'cbi-value-title',
1370 'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option)
1372 this.titleref ? E('a', {
1373 'class': 'cbi-title-ref',
1374 'href': this.titleref,
1375 'title': this.titledesc || _('Go to relevant configuration page')
1376 }, this.title) : this.title));
1378 optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
1383 (optionEl.lastChild || optionEl).appendChild(nodes);
1385 if (!in_table && typeof(this.description) === 'string' && this.description !== '')
1386 L.dom.append(optionEl.lastChild || optionEl,
1387 E('div', { 'class': 'cbi-value-description' }, this.description));
1389 if (depend_list && depend_list.length)
1390 optionEl.classList.add('hidden');
1392 optionEl.addEventListener('widget-change',
1393 L.bind(this.map.checkDepends, this.map));
1395 L.dom.bindClassInstance(optionEl, this);
1400 renderWidget: function(section_id, option_index, cfgvalue) {
1401 var value = (cfgvalue != null) ? cfgvalue : this.default,
1402 choices = this.transformChoices(),
1406 var placeholder = (this.optional || this.rmempty)
1407 ? E('em', _('unspecified')) : _('-- Please choose --');
1409 widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
1410 id: this.cbid(section_id),
1412 optional: this.optional || this.rmempty,
1413 datatype: this.datatype,
1414 select_placeholder: this.placeholder || placeholder,
1415 validate: L.bind(this.validate, this, section_id)
1419 widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
1420 id: this.cbid(section_id),
1421 password: this.password,
1422 optional: this.optional || this.rmempty,
1423 datatype: this.datatype,
1424 placeholder: this.placeholder,
1425 validate: L.bind(this.validate, this, section_id)
1429 return widget.render();
1433 var CBIDynamicList = CBIValue.extend({
1434 __name__: 'CBI.DynamicList',
1436 renderWidget: function(section_id, option_index, cfgvalue) {
1437 var value = (cfgvalue != null) ? cfgvalue : this.default,
1438 choices = this.transformChoices(),
1439 items = L.toArray(value);
1441 var widget = new ui.DynamicList(items, choices, {
1442 id: this.cbid(section_id),
1444 optional: this.optional || this.rmempty,
1445 datatype: this.datatype,
1446 placeholder: this.placeholder,
1447 validate: L.bind(this.validate, this, section_id)
1450 return widget.render();
1454 var CBIListValue = CBIValue.extend({
1455 __name__: 'CBI.ListValue',
1457 __init__: function() {
1458 this.super('__init__', arguments);
1459 this.widget = 'select';
1463 renderWidget: function(section_id, option_index, cfgvalue) {
1464 var choices = this.transformChoices();
1465 var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
1466 id: this.cbid(section_id),
1469 optional: this.optional,
1470 placeholder: this.placeholder,
1471 validate: L.bind(this.validate, this, section_id)
1474 return widget.render();
1478 var CBIFlagValue = CBIValue.extend({
1479 __name__: 'CBI.FlagValue',
1481 __init__: function() {
1482 this.super('__init__', arguments);
1485 this.disabled = '0';
1486 this.default = this.disabled;
1489 renderWidget: function(section_id, option_index, cfgvalue) {
1490 var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
1491 id: this.cbid(section_id),
1492 value_enabled: this.enabled,
1493 value_disabled: this.disabled,
1494 validate: L.bind(this.validate, this, section_id)
1497 return widget.render();
1500 formvalue: function(section_id) {
1501 var node = this.map.findElement('id', this.cbid(section_id)),
1502 checked = node ? L.dom.callClassMethod(node, 'isChecked') : false;
1504 return checked ? this.enabled : this.disabled;
1507 textvalue: function(section_id) {
1508 var cval = this.cfgvalue(section_id);
1511 cval = this.default;
1513 return (cval == this.enabled) ? _('Yes') : _('No');
1516 parse: function(section_id) {
1517 if (this.isActive(section_id)) {
1518 var fval = this.formvalue(section_id);
1520 if (!this.isValid(section_id))
1521 return Promise.reject();
1523 if (fval == this.default && (this.optional || this.rmempty))
1524 return Promise.resolve(this.remove(section_id));
1526 return Promise.resolve(this.write(section_id, fval));
1529 return Promise.resolve(this.remove(section_id));
1534 var CBIMultiValue = CBIDynamicList.extend({
1535 __name__: 'CBI.MultiValue',
1537 __init__: function() {
1538 this.super('__init__', arguments);
1539 this.placeholder = _('-- Please choose --');
1542 renderWidget: function(section_id, option_index, cfgvalue) {
1543 var value = (cfgvalue != null) ? cfgvalue : this.default,
1544 choices = this.transformChoices();
1546 var widget = new ui.Dropdown(L.toArray(value), choices, {
1547 id: this.cbid(section_id),
1550 optional: this.optional || this.rmempty,
1551 select_placeholder: this.placeholder,
1552 display_items: this.display_size || this.size || 3,
1553 dropdown_items: this.dropdown_size || this.size || -1,
1554 validate: L.bind(this.validate, this, section_id)
1557 return widget.render();
1561 var CBIDummyValue = CBIValue.extend({
1562 __name__: 'CBI.DummyValue',
1564 renderWidget: function(section_id, option_index, cfgvalue) {
1565 var value = (cfgvalue != null) ? cfgvalue : this.default,
1566 hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
1567 outputEl = E('div');
1570 outputEl.appendChild(E('a', { 'href': this.href }));
1572 L.dom.append(outputEl.lastChild || outputEl,
1573 this.rawhtml ? value : [ value ]);
1582 var CBIButtonValue = CBIValue.extend({
1583 __name__: 'CBI.ButtonValue',
1585 renderWidget: function(section_id, option_index, cfgvalue) {
1586 var value = (cfgvalue != null) ? cfgvalue : this.default;
1588 if (value !== false)
1592 'id': this.cbid(section_id)
1595 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
1597 //'id': this.cbid(section_id),
1598 //'name': this.cbid(section_id),
1599 'value': this.inputtitle || this.title,
1600 'click': L.bind(function(ev) {
1601 ev.target.previousElementSibling.value = ev.target.value;
1607 return document.createTextNode(' - ');
1611 var CBIHiddenValue = CBIValue.extend({
1612 __name__: 'CBI.HiddenValue',
1614 renderWidget: function(section_id, option_index, cfgvalue) {
1615 var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
1616 id: this.cbid(section_id)
1619 return widget.render();
1623 var CBISectionValue = CBIValue.extend({
1624 __name__: 'CBI.ContainerValue',
1625 __init__: function(map, section, option, cbiClass /*, ... */) {
1626 this.super('__init__', [map, section, option]);
1628 if (!CBIAbstractSection.isSubclass(cbiClass))
1629 throw 'Sub section must be a descendent of CBIAbstractSection';
1631 this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
1634 load: function(section_id) {
1635 return this.subsection.load();
1638 parse: function(section_id) {
1639 return this.subsection.parse();
1642 renderWidget: function(section_id, option_index, cfgvalue) {
1643 return this.subsection.render();
1646 checkDepends: function(section_id) {
1647 this.subsection.checkDepends();
1648 return this.super('checkDepends');
1651 write: function() {},
1652 remove: function() {},
1653 cfgvalue: function() { return null },
1654 formvalue: function() { return null }
1657 return L.Class.extend({
1659 AbstractSection: CBIAbstractSection,
1660 AbstractValue: CBIAbstractValue,
1662 TypedSection: CBITypedSection,
1663 TableSection: CBITableSection,
1664 GridSection: CBIGridSection,
1665 NamedSection: CBINamedSection,
1668 DynamicList: CBIDynamicList,
1669 ListValue: CBIListValue,
1671 MultiValue: CBIMultiValue,
1672 DummyValue: CBIDummyValue,
1673 Button: CBIButtonValue,
1674 HiddenValue: CBIHiddenValue,
1675 SectionValue: CBISectionValue