2 var type = function(f, l)
12 L.cbi.validation.message = L.tr(msg);
15 compile: function(code)
20 var types = L.cbi.validation.types;
25 for (var i = 0; i < code.length; i++)
33 switch (code.charCodeAt(i))
45 var label = code.substring(pos, i);
46 label = label.replace(/\\(.)/g, '$1');
47 label = label.replace(/^[ \t]+/g, '');
48 label = label.replace(/[ \t]+$/g, '');
50 if (label && !isNaN(label))
52 stack.push(parseFloat(label));
54 else if (label.match(/^(['"]).*\1$/))
56 stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
58 else if (typeof types[label] == 'function')
60 stack.push(types[label]);
65 throw "Syntax error, unhandled token '"+label+"'";
70 depth += (code.charCodeAt(i) == 40);
76 if (typeof stack[stack.length-2] != 'function')
77 throw "Syntax error, argument list follows non-function";
79 stack[stack.length-1] =
80 L.cbi.validation.compile(code.substring(pos, i));
93 var validation = cbi_class.validation;
98 if (this.match(/^-?[0-9]+$/) != null)
101 validation.i18n('Must be a valid integer');
105 'uinteger': function()
107 if (validation.types['integer'].apply(this) && (this >= 0))
110 validation.i18n('Must be a positive integer');
116 if (!isNaN(parseFloat(this)))
119 validation.i18n('Must be a valid number');
125 if (validation.types['float'].apply(this) && (this >= 0))
128 validation.i18n('Must be a positive number');
134 if (L.parseIPv4(this) || L.parseIPv6(this))
137 validation.i18n('Must be a valid IP address');
141 'ip4addr': function()
143 if (L.parseIPv4(this))
146 validation.i18n('Must be a valid IPv4 address');
150 'ip6addr': function()
152 if (L.parseIPv6(this))
155 validation.i18n('Must be a valid IPv6 address');
159 'netmask4': function()
161 if (L.isNetmask(L.parseIPv4(this)))
164 validation.i18n('Must be a valid IPv4 netmask');
168 'netmask6': function()
170 if (L.isNetmask(L.parseIPv6(this)))
173 validation.i18n('Must be a valid IPv6 netmask6');
179 if (this.match(/^([0-9.]+)\/(\d{1,2})$/))
180 if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1))
183 validation.i18n('Must be a valid IPv4 prefix');
189 if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/))
190 if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1))
193 validation.i18n('Must be a valid IPv6 prefix');
197 'ipmask4': function()
199 if (this.match(/^([0-9.]+)\/([0-9.]+)$/))
201 var addr = RegExp.$1, mask = RegExp.$2;
202 if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask)))
206 validation.i18n('Must be a valid IPv4 address/netmask pair');
210 'ipmask6': function()
212 if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/))
214 var addr = RegExp.$1, mask = RegExp.$2;
215 if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask)))
219 validation.i18n('Must be a valid IPv6 address/netmask pair');
225 if (validation.types['integer'].apply(this) &&
226 (this >= 0) && (this <= 65535))
229 validation.i18n('Must be a valid port number');
233 'portrange': function()
235 if (this.match(/^(\d+)-(\d+)$/))
240 if (validation.types['port'].apply(p1) &&
241 validation.types['port'].apply(p2) &&
242 (parseInt(p1) <= parseInt(p2)))
245 else if (validation.types['port'].apply(this))
250 validation.i18n('Must be a valid port range');
254 'macaddr': function()
256 if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null)
259 validation.i18n('Must be a valid MAC address');
265 if (validation.types['hostname'].apply(this) ||
266 validation.types['ipaddr'].apply(this))
269 validation.i18n('Must be a valid hostname or IP address');
273 'hostname': function()
275 if ((this.length <= 253) &&
276 ((this.match(/^[a-zA-Z0-9]+$/) != null ||
277 (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
278 this.match(/[^0-9.]/)))))
281 validation.i18n('Must be a valid host name');
285 'network': function()
287 if (validation.types['uciname'].apply(this) ||
288 validation.types['host'].apply(this))
291 validation.i18n('Must be a valid network name');
300 ? (v.match(/^[a-fA-F0-9]{64}$/) != null)
301 : ((v.length >= 8) && (v.length <= 63)))
304 validation.i18n('Must be a valid WPA key');
312 if (v.substr(0,2) == 's:')
315 if (((v.length == 10) || (v.length == 26))
316 ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null)
317 : ((v.length == 5) || (v.length == 13)))
320 validation.i18n('Must be a valid WEP key');
324 'uciname': function()
326 if (this.match(/^[a-zA-Z0-9_]+$/) != null)
329 validation.i18n('Must be a valid UCI identifier');
333 'range': function(min, max)
335 var val = parseFloat(this);
337 if (validation.types['integer'].apply(this) &&
338 !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max)))
341 validation.i18n('Must be a number between %d and %d');
347 var val = parseFloat(this);
349 if (validation.types['integer'].apply(this) &&
350 !isNaN(min) && !isNaN(val) && (val >= min))
353 validation.i18n('Must be a number greater or equal to %d');
359 var val = parseFloat(this);
361 if (validation.types['integer'].apply(this) &&
362 !isNaN(max) && !isNaN(val) && (val <= max))
365 validation.i18n('Must be a number lower or equal to %d');
369 'rangelength': function(min, max)
373 if (!isNaN(min) && !isNaN(max) &&
374 (val.length >= min) && (val.length <= max))
378 validation.i18n('Must be between %d and %d characters');
380 validation.i18n('Must be %d characters');
384 'minlength': function(min)
388 if (!isNaN(min) && (val.length >= min))
391 validation.i18n('Must be at least %d characters');
395 'maxlength': function(max)
399 if (!isNaN(max) && (val.length <= max))
402 validation.i18n('Must be at most %d characters');
410 for (var i = 0; i < arguments.length; i += 2)
412 delete validation.message;
414 if (typeof(arguments[i]) != 'function')
416 if (arguments[i] == this)
420 else if (arguments[i].apply(this, arguments[i+1]))
425 if (validation.message)
426 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
429 validation.message = msgs.join( L.tr(' - or - '));
437 for (var i = 0; i < arguments.length; i += 2)
439 delete validation.message;
441 if (typeof arguments[i] != 'function')
443 if (arguments[i] != this)
447 else if (!arguments[i].apply(this, arguments[i+1]))
452 if (validation.message)
453 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
456 validation.message = msgs.join(', ');
462 return validation.types['or'].apply(
463 this.replace(/^[ \t]*![ \t]*/, ''), arguments);
466 'list': function(subvalidator, subargs)
468 if (typeof subvalidator != 'function')
471 var tokens = this.match(/[^ \t]+/g);
472 for (var i = 0; i < tokens.length; i++)
473 if (!subvalidator.apply(tokens[i], subargs))
479 'phonedigit': function()
481 if (this.match(/^[0-9\*#!\.]+$/) != null)
484 validation.i18n('Must be a valid phone number digit');
494 cbi_class.AbstractValue = L.ui.AbstractWidget.extend({
495 init: function(name, options)
499 this.dependencies = [ ];
500 this.rdependency = { };
502 this.options = L.defaults(options, {
512 return this.ownerSection.id('field', sid || '__unknown__', this.name);
515 render: function(sid, condensed)
517 var i = this.instance[sid] = { };
520 .addClass('luci2-field');
524 i.top.addClass('form-group');
526 if (typeof(this.options.caption) == 'string')
528 .addClass('col-lg-2 control-label')
529 .attr('for', this.id(sid))
530 .text(this.options.caption)
534 i.error = $('<div />')
536 .addClass('luci2-field-error label label-danger');
538 i.widget = $('<div />')
539 .addClass('luci2-field-widget')
540 .append(this.widget(sid))
546 i.widget.addClass('col-lg-5');
549 .addClass('col-lg-5')
550 .text((typeof(this.options.description) == 'string') ? this.options.description : '')
557 active: function(sid)
559 return (this.instance[sid] && !this.instance[sid].disabled);
562 ucipath: function(sid)
565 config: (this.options.uci_package || this.ownerMap.uci_package),
566 section: (this.options.uci_section || sid),
567 option: (this.options.uci_option || this.name)
571 ucivalue: function(sid)
573 var uci = this.ucipath(sid);
574 var val = this.ownerMap.get(uci.config, uci.section, uci.option);
576 if (typeof(val) == 'undefined')
577 return this.options.initial;
582 formvalue: function(sid)
584 var v = $('#' + this.id(sid)).val();
585 return (v === '') ? undefined : v;
588 textvalue: function(sid)
590 var v = this.formvalue(sid);
592 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
593 v = this.ucivalue(sid);
595 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
596 v = this.options.placeholder;
598 if (typeof(v) == 'undefined' || v === '')
601 if (typeof(v) == 'string' && $.isArray(this.choices))
603 for (var i = 0; i < this.choices.length; i++)
604 if (v === this.choices[i][0])
605 return this.choices[i][1];
609 else if (v === false)
611 else if ($.isArray(v))
617 changed: function(sid)
619 var a = this.ucivalue(sid);
620 var b = this.formvalue(sid);
622 if (typeof(a) != typeof(b))
627 if (a.length != b.length)
630 for (var i = 0; i < a.length; i++)
636 else if ($.isPlainObject(a))
643 if (!(k in a) || a[k] !== b[k])
654 var uci = this.ucipath(sid);
656 if (this.instance[sid].disabled)
658 if (!this.options.keep)
659 return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
664 var chg = this.changed(sid);
665 var val = this.formvalue(sid);
668 this.ownerMap.set(uci.config, uci.section, uci.option, val);
673 findSectionID: function($elem)
675 return this.ownerSection.findParentSectionIDs($elem)[0];
678 setError: function($elem, msg, msgargs)
680 var $field = $elem.parents('.luci2-field:first');
681 var $error = $field.find('.luci2-field-error:first');
683 if (typeof(msg) == 'string' && msg.length > 0)
685 $field.addClass('luci2-form-error');
686 $elem.parent().addClass('has-error');
688 $error.text(msg.format.apply(msg, msgargs)).show();
689 $field.trigger('validate');
695 $elem.parent().removeClass('has-error');
697 var $other_errors = $field.find('.has-error');
698 if ($other_errors.length == 0)
700 $field.removeClass('luci2-form-error');
701 $error.text('').hide();
702 $field.trigger('validate');
711 handleValidate: function(ev)
717 var val = $elem.val();
718 var vstack = d.vstack;
720 if (vstack && typeof(vstack[0]) == 'function')
722 delete validation.message;
724 if ((val.length == 0 && !d.opt))
726 rv = d.self.setError($elem, L.tr('Field must not be empty'));
728 else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
730 rv = d.self.setError($elem, validation.message, vstack[1]);
734 rv = d.self.setError($elem);
740 var sid = d.self.findSectionID($elem);
742 for (var field in d.self.rdependency)
744 d.self.rdependency[field].toggle(sid);
745 d.self.rdependency[field].validate(sid);
748 d.self.ownerSection.tabtoggle(sid);
754 attachEvents: function(sid, elem)
758 opt: this.options.optional
762 for (var evname in this.events)
763 elem.on(evname, evdata, this.events[evname]);
765 if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
769 if (typeof(this.options.datatype) == 'string')
772 evdata.vstack = L.cbi.validation.compile(this.options.datatype);
775 else if (typeof(this.options.datatype) == 'function')
777 var vfunc = this.options.datatype;
778 evdata.vstack = [ function(elem) {
779 var rv = vfunc(this, elem);
781 validation.message = rv;
782 return (rv === true);
786 if (elem.prop('tagName') == 'SELECT')
788 elem.change(evdata, this.handleValidate);
790 else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
792 elem.click(evdata, this.handleValidate);
793 elem.blur(evdata, this.handleValidate);
797 elem.keyup(evdata, this.handleValidate);
798 elem.blur(evdata, this.handleValidate);
801 elem.addClass('luci2-field-validate')
802 .on('validate', evdata, this.handleValidate);
807 validate: function(sid)
809 var i = this.instance[sid];
811 i.widget.find('.luci2-field-validate').trigger('validate');
813 return (i.disabled || i.error.text() == '');
816 depends: function(d, v, add)
823 for (var i = 0; i < d.length; i++)
825 if (typeof(d[i]) == 'string')
827 else if (d[i] instanceof L.cbi.AbstractValue)
828 dep[d[i].name] = true;
831 else if (d instanceof L.cbi.AbstractValue)
834 dep[d.name] = (typeof(v) == 'undefined') ? true : v;
836 else if (typeof(d) == 'object')
840 else if (typeof(d) == 'string')
843 dep[d] = (typeof(v) == 'undefined') ? true : v;
846 if (!dep || $.isEmptyObject(dep))
849 for (var field in dep)
851 var f = this.ownerSection.fields[field];
853 f.rdependency[this.name] = this;
858 if ($.isEmptyObject(dep))
861 if (!add || !this.dependencies.length)
862 this.dependencies.push(dep);
864 for (var i = 0; i < this.dependencies.length; i++)
865 $.extend(this.dependencies[i], dep);
870 toggle: function(sid)
872 var d = this.dependencies;
873 var i = this.instance[sid];
878 for (var n = 0; n < d.length; n++)
882 for (var field in d[n])
884 var val = this.ownerSection.fields[field].formvalue(sid);
885 var cmp = d[n][field];
887 if (typeof(cmp) == 'boolean')
889 if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
895 else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
903 else if (typeof(cmp) == 'function')
911 else if (cmp instanceof RegExp)
926 i.top.removeClass('luci2-field-disabled');
937 i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
938 i.top.addClass('luci2-field-disabled');
945 cbi_class.CheckboxValue = cbi_class.AbstractValue.extend({
946 widget: function(sid)
948 var o = this.options;
950 if (typeof(o.enabled) == 'undefined') o.enabled = '1';
951 if (typeof(o.disabled) == 'undefined') o.disabled = '0';
953 var i = $('<input />')
954 .attr('id', this.id(sid))
955 .attr('type', 'checkbox')
956 .prop('checked', this.ucivalue(sid));
959 .addClass('checkbox')
960 .append(this.attachEvents(sid, i));
963 ucivalue: function(sid)
965 var v = this.callSuper('ucivalue', sid);
967 if (typeof(v) == 'boolean')
970 return (v == this.options.enabled);
973 formvalue: function(sid)
975 var v = $('#' + this.id(sid)).prop('checked');
977 if (typeof(v) == 'undefined')
978 return !!this.options.initial;
985 var uci = this.ucipath(sid);
987 if (this.instance[sid].disabled)
989 if (!this.options.keep)
990 return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
995 var chg = this.changed(sid);
996 var val = this.formvalue(sid);
1000 if (this.options.optional && val == this.options.initial)
1001 this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
1003 this.ownerMap.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
1010 cbi_class.InputValue = cbi_class.AbstractValue.extend({
1011 widget: function(sid)
1013 var i = $('<input />')
1014 .addClass('form-control')
1015 .attr('id', this.id(sid))
1016 .attr('type', 'text')
1017 .attr('placeholder', this.options.placeholder)
1018 .val(this.ucivalue(sid));
1020 return this.attachEvents(sid, i);
1024 cbi_class.PasswordValue = cbi_class.AbstractValue.extend({
1025 widget: function(sid)
1027 var i = $('<input />')
1028 .addClass('form-control')
1029 .attr('id', this.id(sid))
1030 .attr('type', 'password')
1031 .attr('placeholder', this.options.placeholder)
1032 .val(this.ucivalue(sid));
1034 var t = $('<span />')
1035 .addClass('input-group-btn')
1036 .append(L.ui.button(L.tr('Reveal'), 'default')
1037 .click(function(ev) {
1039 var i = b.parent().prev();
1040 var t = i.attr('type');
1041 b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
1042 i.attr('type', (t == 'password') ? 'text' : 'password');
1046 this.attachEvents(sid, i);
1049 .addClass('input-group')
1055 cbi_class.ListValue = cbi_class.AbstractValue.extend({
1056 widget: function(sid)
1058 var s = $('<select />')
1059 .addClass('form-control');
1061 if (this.options.optional && !this.has_empty)
1064 .text(L.tr('-- Please choose --'))
1068 for (var i = 0; i < this.choices.length; i++)
1070 .attr('value', this.choices[i][0])
1071 .text(this.choices[i][1])
1074 s.attr('id', this.id(sid)).val(this.ucivalue(sid));
1076 return this.attachEvents(sid, s);
1079 value: function(k, v)
1085 this.has_empty = true;
1087 this.choices.push([k, v || k]);
1092 cbi_class.MultiValue = cbi_class.ListValue.extend({
1093 widget: function(sid)
1095 var v = this.ucivalue(sid);
1096 var t = $('<div />').attr('id', this.id(sid));
1099 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
1102 for (var i = 0; i < v.length; i++)
1106 for (var i = 0; i < this.choices.length; i++)
1109 .addClass('checkbox')
1110 .append($('<input />')
1111 .attr('type', 'checkbox')
1112 .attr('value', this.choices[i][0])
1113 .prop('checked', s[this.choices[i][0]]))
1114 .append(this.choices[i][1])
1121 formvalue: function(sid)
1124 var fields = $('#' + this.id(sid) + ' > label > input');
1126 for (var i = 0; i < fields.length; i++)
1127 if (fields[i].checked)
1128 rv.push(fields[i].getAttribute('value'));
1133 textvalue: function(sid)
1135 var v = this.formvalue(sid);
1139 for (var i = 0; i < this.choices.length; i++)
1140 c[this.choices[i][0]] = this.choices[i][1];
1144 for (var i = 0; i < v.length; i++)
1145 t.push(c[v[i]] || v[i]);
1147 return t.join(', ');
1151 cbi_class.ComboBox = cbi_class.AbstractValue.extend({
1152 _change: function(ev)
1155 var self = ev.data.self;
1157 if (s.selectedIndex == (s.options.length - 1))
1159 ev.data.select.hide();
1160 ev.data.input.show().focus();
1161 ev.data.input.val('');
1163 else if (self.options.optional && s.selectedIndex == 0)
1165 ev.data.input.val('');
1169 ev.data.input.val(ev.data.select.val());
1172 ev.stopPropagation();
1178 var val = this.value;
1179 var self = ev.data.self;
1181 ev.data.select.empty();
1183 if (self.options.optional && !self.has_empty)
1186 .text(L.tr('-- please choose --'))
1187 .appendTo(ev.data.select);
1190 for (var i = 0; i < self.choices.length; i++)
1192 if (self.choices[i][0] == val)
1196 .attr('value', self.choices[i][0])
1197 .text(self.choices[i][1])
1198 .appendTo(ev.data.select);
1201 if (!seen && val != '')
1205 .appendTo(ev.data.select);
1209 .text(L.tr('-- custom --'))
1210 .appendTo(ev.data.select);
1212 ev.data.input.hide();
1213 ev.data.select.val(val).show().blur();
1216 _enter: function(ev)
1221 ev.preventDefault();
1222 ev.data.self._blur(ev);
1226 widget: function(sid)
1228 var d = $('<div />')
1229 .attr('id', this.id(sid));
1231 var t = $('<input />')
1232 .addClass('form-control')
1233 .attr('type', 'text')
1237 var s = $('<select />')
1238 .addClass('form-control')
1247 s.change(evdata, this._change);
1248 t.blur(evdata, this._blur);
1249 t.keydown(evdata, this._enter);
1251 t.val(this.ucivalue(sid));
1254 this.attachEvents(sid, t);
1255 this.attachEvents(sid, s);
1260 value: function(k, v)
1266 this.has_empty = true;
1268 this.choices.push([k, v || k]);
1272 formvalue: function(sid)
1274 var v = $('#' + this.id(sid)).children('input').val();
1275 return (v == '') ? undefined : v;
1279 cbi_class.DynamicList = cbi_class.ComboBox.extend({
1280 _redraw: function(focus, add, del, s)
1282 var v = s.values || [ ];
1285 $(s.parent).children('div.input-group').children('input').each(function(i) {
1287 v.push(this.value || '');
1290 $(s.parent).empty();
1295 v.splice(focus, 0, '');
1297 else if (v.length == 0)
1303 for (var i = 0; i < v.length; i++)
1310 remove: ((i+1) < v.length)
1315 btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
1317 btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
1321 var txt = $('<input />')
1322 .addClass('form-control')
1323 .attr('type', 'text')
1326 var sel = $('<select />')
1327 .addClass('form-control');
1330 .addClass('input-group')
1333 .append($('<span />')
1334 .addClass('input-group-btn')
1336 .appendTo(s.parent);
1338 evdata.input = this.attachEvents(s.sid, txt);
1339 evdata.select = this.attachEvents(s.sid, sel);
1341 sel.change(evdata, this._change);
1342 txt.blur(evdata, this._blur);
1343 txt.keydown(evdata, this._keydown);
1348 if (i == focus || -(i+1) == focus)
1355 var f = $('<input />')
1356 .attr('type', 'text')
1358 .attr('placeholder', (i == 0) ? this.options.placeholder : '')
1359 .addClass('form-control')
1360 .keydown(evdata, this._keydown)
1361 .keypress(evdata, this._keypress)
1365 .addClass('input-group')
1367 .append($('<span />')
1368 .addClass('input-group-btn')
1370 .appendTo(s.parent);
1376 else if (-(i+1) == focus)
1380 /* force cursor to end */
1386 evdata.input = this.attachEvents(s.sid, f);
1397 _keypress: function(ev)
1401 /* backspace, delete */
1404 if (ev.data.input.val() == '')
1406 ev.preventDefault();
1412 /* enter, arrow up, arrow down */
1416 ev.preventDefault();
1423 _keydown: function(ev)
1425 var input = ev.data.input;
1429 /* backspace, delete */
1432 if (input.val().length == 0)
1434 ev.preventDefault();
1436 var index = ev.data.index;
1442 ev.data.self._redraw(focus, -1, index, ev.data);
1450 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
1455 var prev = input.parent().prevAll('div.input-group:first').children('input');
1456 if (prev.is(':visible'))
1459 prev.next('select').focus();
1464 var next = input.parent().nextAll('div.input-group:first').children('input');
1465 if (next.is(':visible'))
1468 next.next('select').focus();
1475 _btnclick: function(ev)
1477 if (!this.getAttribute('disabled'))
1481 var index = ev.data.index;
1482 ev.data.self._redraw(-index, -1, index, ev.data);
1486 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
1493 widget: function(sid)
1495 this.options.optional = true;
1497 var v = this.ucivalue(sid);
1500 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
1502 var d = $('<div />')
1503 .attr('id', this.id(sid))
1504 .addClass('cbi-input-dynlist');
1506 this._redraw(NaN, -1, -1, {
1516 ucivalue: function(sid)
1518 var v = this.callSuper('ucivalue', sid);
1521 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
1526 formvalue: function(sid)
1529 var fields = $('#' + this.id(sid) + ' input');
1531 for (var i = 0; i < fields.length; i++)
1532 if (typeof(fields[i].value) == 'string' && fields[i].value.length)
1533 rv.push(fields[i].value);
1539 cbi_class.DummyValue = cbi_class.AbstractValue.extend({
1540 widget: function(sid)
1543 .addClass('form-control-static')
1544 .attr('id', this.id(sid))
1545 .html(this.ucivalue(sid) || this.label('placeholder'));
1548 formvalue: function(sid)
1550 return this.ucivalue(sid);
1554 cbi_class.ButtonValue = cbi_class.AbstractValue.extend({
1555 widget: function(sid)
1557 this.options.optional = true;
1559 var btn = $('<button />')
1560 .addClass('btn btn-default')
1561 .attr('id', this.id(sid))
1562 .attr('type', 'button')
1563 .text(this.label('text'));
1565 return this.attachEvents(sid, btn);
1569 cbi_class.NetworkList = cbi_class.AbstractValue.extend({
1572 return L.network.load();
1575 _device_icon: function(dev)
1578 .attr('src', dev.icon())
1579 .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
1582 widget: function(sid)
1584 var id = this.id(sid);
1585 var ul = $('<ul />')
1587 .addClass('list-unstyled');
1589 var itype = this.options.multiple ? 'checkbox' : 'radio';
1590 var value = this.ucivalue(sid);
1593 if (!this.options.multiple)
1594 check[value] = true;
1596 for (var i = 0; i < value.length; i++)
1597 check[value[i]] = true;
1599 var interfaces = L.network.getInterfaces();
1601 for (var i = 0; i < interfaces.length; i++)
1603 var iface = interfaces[i];
1606 .append($('<label />')
1607 .addClass(itype + ' inline')
1608 .append(this.attachEvents(sid, $('<input />')
1609 .attr('name', itype + id)
1610 .attr('type', itype)
1611 .attr('value', iface.name())
1612 .prop('checked', !!check[iface.name()])))
1613 .append(iface.renderBadge()))
1617 if (!this.options.multiple)
1620 .append($('<label />')
1621 .addClass(itype + ' inline text-muted')
1622 .append(this.attachEvents(sid, $('<input />')
1623 .attr('name', itype + id)
1624 .attr('type', itype)
1626 .prop('checked', $.isEmptyObject(check))))
1627 .append(L.tr('unspecified')))
1634 ucivalue: function(sid)
1636 var v = this.callSuper('ucivalue', sid);
1638 if (!this.options.multiple)
1644 else if (typeof(v) == 'string')
1647 return v ? v[0] : undefined;
1654 if (typeof(v) == 'string')
1655 v = v.match(/\S+/g);
1661 formvalue: function(sid)
1663 var inputs = $('#' + this.id(sid) + ' input');
1665 if (!this.options.multiple)
1667 for (var i = 0; i < inputs.length; i++)
1668 if (inputs[i].checked && inputs[i].value !== '')
1669 return inputs[i].value;
1676 for (var i = 0; i < inputs.length; i++)
1677 if (inputs[i].checked)
1678 rv.push(inputs[i].value);
1680 return rv.length ? rv : undefined;
1684 cbi_class.DeviceList = cbi_class.NetworkList.extend({
1685 handleFocus: function(ev)
1687 var self = ev.data.self;
1688 var input = $(this);
1690 input.parent().prev().prop('checked', true);
1693 handleBlur: function(ev)
1696 ev.data.self.handleKeydown.call(this, ev);
1699 handleKeydown: function(ev)
1701 if (ev.which != 10 && ev.which != 13)
1704 var sid = ev.data.sid;
1705 var self = ev.data.self;
1706 var input = $(this);
1707 var ifnames = L.toArray(input.val());
1709 if (!ifnames.length)
1712 L.network.createDevice(ifnames[0]);
1714 self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
1719 return L.network.load();
1722 _redraw: function(sid, ul, sel)
1724 var id = ul.attr('id');
1725 var devs = L.network.getDevices();
1726 var iface = L.network.getInterface(sid);
1727 var itype = this.options.multiple ? 'checkbox' : 'radio';
1732 for (var i = 0; i < devs.length; i++)
1733 if (devs[i].isInNetwork(iface))
1734 check[devs[i].name()] = true;
1738 if (this.options.multiple)
1739 check = L.toObject(this.formvalue(sid));
1746 for (var i = 0; i < devs.length; i++)
1750 if (dev.isBridge() && this.options.bridges === false)
1753 if (!dev.isBridgeable() && this.options.multiple)
1756 var badge = $('<span />')
1758 .append($('<img />').attr('src', dev.icon()))
1759 .append(' %s: %s'.format(dev.name(), dev.description()));
1761 //var ifcs = dev.getInterfaces();
1764 // for (var j = 0; j < ifcs.length; j++)
1765 // badge.append((j ? ', ' : ' (') + ifcs[j].name());
1767 // badge.append(')');
1771 .append($('<label />')
1772 .addClass(itype + ' inline')
1773 .append($('<input />')
1774 .attr('name', itype + id)
1775 .attr('type', itype)
1776 .attr('value', dev.name())
1777 .prop('checked', !!check[dev.name()]))
1784 .append($('<label />')
1785 .attr('for', 'custom' + id)
1786 .addClass(itype + ' inline')
1787 .append($('<input />')
1788 .attr('name', itype + id)
1789 .attr('type', itype)
1791 .append($('<span />')
1793 .append($('<input />')
1794 .attr('id', 'custom' + id)
1795 .attr('type', 'text')
1796 .attr('placeholder', L.tr('Custom device …'))
1797 .on('focus', { self: this, sid: sid }, this.handleFocus)
1798 .on('blur', { self: this, sid: sid }, this.handleBlur)
1799 .on('keydown', { self: this, sid: sid }, this.handleKeydown))))
1802 if (!this.options.multiple)
1805 .append($('<label />')
1806 .addClass(itype + ' inline text-muted')
1807 .append($('<input />')
1808 .attr('name', itype + id)
1809 .attr('type', itype)
1811 .prop('checked', $.isEmptyObject(check)))
1812 .append(L.tr('unspecified')))
1817 widget: function(sid)
1819 var id = this.id(sid);
1820 var ul = $('<ul />')
1822 .addClass('list-unstyled');
1824 this._redraw(sid, ul);
1831 if (this.instance[sid].disabled)
1834 var ifnames = this.formvalue(sid);
1838 var iface = L.network.getInterface(sid);
1842 iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
1847 cbi_class.AbstractSection = L.ui.AbstractWidget.extend({
1850 var s = [ arguments[0], this.ownerMap.uci_package, this.uci_type ];
1852 for (var i = 1; i < arguments.length && typeof(arguments[i]) == 'string'; i++)
1853 s.push(arguments[i].replace(/\./g, '_'));
1858 option: function(widget, name, options)
1860 if (this.tabs.length == 0)
1861 this.tab({ id: '__default__', selected: true });
1863 return this.taboption('__default__', widget, name, options);
1866 tab: function(options)
1868 if (options.selected)
1869 this.tabs.selected = this.tabs.length;
1873 caption: options.caption,
1874 description: options.description,
1880 taboption: function(tabid, widget, name, options)
1883 for (var i = 0; i < this.tabs.length; i++)
1885 if (this.tabs[i].id == tabid)
1893 throw 'Cannot append to unknown tab ' + tabid;
1895 var w = widget ? new widget(name, options) : null;
1897 if (!(w instanceof L.cbi.AbstractValue))
1898 throw 'Widget must be an instance of AbstractValue';
1900 w.ownerSection = this;
1901 w.ownerMap = this.ownerMap;
1903 this.fields[name] = w;
1909 tabtoggle: function(sid)
1911 for (var i = 0; i < this.tabs.length; i++)
1913 var tab = this.tabs[i];
1914 var elem = $('#' + this.id('nodetab', sid, tab.id));
1917 for (var j = 0; j < tab.fields.length; j++)
1919 if (tab.fields[j].active(sid))
1926 if (empty && elem.is(':visible'))
1933 validate: function(parent_sid)
1935 var s = this.getUCISections(parent_sid);
1938 for (var i = 0; i < s.length; i++)
1940 var $item = $('#' + this.id('sectionitem', s[i]['.name']));
1942 $item.find('.luci2-field-validate').trigger('validate');
1943 n += $item.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
1949 load: function(parent_sid)
1951 var deferreds = [ ];
1953 var s = this.getUCISections(parent_sid);
1954 for (var i = 0; i < s.length; i++)
1956 for (var f in this.fields)
1958 if (typeof(this.fields[f].load) != 'function')
1961 var rv = this.fields[f].load(s[i]['.name']);
1962 if (L.isDeferred(rv))
1966 for (var j = 0; j < this.subsections.length; j++)
1968 var rv = this.subsections[j].load(s[i]['.name']);
1969 deferreds.push.apply(deferreds, rv);
1976 save: function(parent_sid)
1978 var deferreds = [ ];
1979 var s = this.getUCISections(parent_sid);
1981 for (i = 0; i < s.length; i++)
1983 if (!this.options.readonly)
1985 for (var f in this.fields)
1987 if (typeof(this.fields[f].save) != 'function')
1990 var rv = this.fields[f].save(s[i]['.name']);
1991 if (L.isDeferred(rv))
1996 for (var j = 0; j < this.subsections.length; j++)
1998 var rv = this.subsections[j].save(s[i]['.name']);
1999 deferreds.push.apply(deferreds, rv);
2006 teaser: function(sid)
2008 var tf = this.teaser_fields;
2012 tf = this.teaser_fields = [ ];
2014 if ($.isArray(this.options.teasers))
2016 for (var i = 0; i < this.options.teasers.length; i++)
2018 var f = this.options.teasers[i];
2019 if (f instanceof L.cbi.AbstractValue)
2021 else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
2022 tf.push(this.fields[f]);
2027 for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
2028 for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
2029 tf.push(this.tabs[i].fields[j]);
2035 for (var i = 0; i < tf.length; i++)
2037 if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
2040 var n = tf[i].options.caption || tf[i].name;
2041 var v = tf[i].textvalue(sid);
2043 if (typeof(v) == 'undefined')
2046 t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
2052 findAdditionalUCIPackages: function()
2056 for (var i = 0; i < this.tabs.length; i++)
2057 for (var j = 0; j < this.tabs[i].fields.length; j++)
2058 if (this.tabs[i].fields[j].options.uci_package)
2059 packages.push(this.tabs[i].fields[j].options.uci_package);
2064 findParentSectionIDs: function($elem)
2067 var $parents = $elem.parents('.luci2-section-item');
2069 for (var i = 0; i < $parents.length; i++)
2070 rv.push($parents[i].getAttribute('data-luci2-sid'));
2076 cbi_class.TypedSection = cbi_class.AbstractSection.extend({
2077 init: function(uci_type, options)
2079 this.uci_type = uci_type;
2080 this.options = options;
2083 this.subsections = [ ];
2084 this.active_panel = { };
2085 this.active_tab = { };
2087 this.instance = { };
2090 filter: function(section, parent_sid)
2095 sort: function(section1, section2)
2100 subsection: function(widget, uci_type, options)
2102 var w = widget ? new widget(uci_type, options) : null;
2104 if (!(w instanceof L.cbi.AbstractSection))
2105 throw 'Widget must be an instance of AbstractSection';
2107 w.ownerSection = this;
2108 w.ownerMap = this.ownerMap;
2109 w.index = this.subsections.length;
2111 this.subsections.push(w);
2115 getUCISections: function(parent_sid)
2117 var s1 = L.uci.sections(this.ownerMap.uci_package);
2120 for (var i = 0; i < s1.length; i++)
2121 if (s1[i]['.type'] == this.uci_type)
2122 if (this.filter(s1[i], parent_sid))
2130 add: function(name, parent_sid)
2132 return this.ownerMap.add(this.ownerMap.uci_package, this.uci_type, name);
2135 remove: function(sid, parent_sid)
2137 return this.ownerMap.remove(this.ownerMap.uci_package, sid);
2140 handleAdd: function(ev)
2143 var name = undefined;
2144 var self = ev.data.self;
2145 var sid = self.findParentSectionIDs(addb)[0];
2147 if (addb.prev().prop('nodeName') == 'INPUT')
2148 name = addb.prev().val();
2150 if (addb.prop('disabled') || name === '')
2153 L.ui.saveScrollTop();
2155 self.setPanelIndex(sid, -1);
2156 self.ownerMap.save();
2158 ev.data.sid = self.add(name, sid);
2159 ev.data.type = self.uci_type;
2160 ev.data.name = name;
2162 self.trigger('add', ev);
2164 self.ownerMap.redraw();
2166 L.ui.restoreScrollTop();
2169 handleRemove: function(ev)
2171 var self = ev.data.self;
2172 var sids = self.findParentSectionIDs($(this));
2176 L.ui.saveScrollTop();
2179 ev.parent_sid = sids[1];
2181 self.trigger('remove', ev);
2183 self.ownerMap.save();
2184 self.remove(ev.sid, ev.parent_sid);
2185 self.ownerMap.redraw();
2187 L.ui.restoreScrollTop();
2190 ev.stopPropagation();
2193 handleSID: function(ev)
2195 var self = ev.data.self;
2197 var addb = text.next();
2198 var errt = addb.next();
2199 var name = text.val();
2201 if (!/^[a-zA-Z0-9_]*$/.test(name))
2203 errt.text(L.tr('Invalid section name')).show();
2204 text.addClass('error');
2205 addb.prop('disabled', true);
2209 if (L.uci.get(self.ownerMap.uci_package, name))
2211 errt.text(L.tr('Name already used')).show();
2212 text.addClass('error');
2213 addb.prop('disabled', true);
2217 errt.text('').hide();
2218 text.removeClass('error');
2219 addb.prop('disabled', false);
2223 handleTab: function(ev)
2225 var self = ev.data.self;
2227 var sid = self.findParentSectionIDs($tab)[0];
2229 self.active_tab[sid] = $tab.parent().index();
2232 handleTabValidate: function(ev)
2234 var $pane = $(ev.delegateTarget);
2235 var $badge = $pane.parent()
2236 .children('.nav-tabs')
2238 .eq($pane.index() - 1) // item #1 is the <ul>
2239 .find('.badge:first');
2241 var err_count = $pane.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
2245 .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
2251 handlePanelValidate: function(ev)
2253 var $elem = $(this);
2255 .prevAll('.luci2-section-header:first')
2256 .children('.luci2-section-teaser')
2257 .find('.badge:first');
2259 var err_count = $elem.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
2263 .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
2269 handlePanelCollapse: function(ev)
2271 var self = ev.data.self;
2273 var $items = $(ev.delegateTarget).children('.luci2-section-item');
2275 var $this_panel = $(ev.target);
2276 var $this_teaser = $this_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
2278 var $prev_panel = $items.children('.luci2-section-panel.in');
2279 var $prev_teaser = $prev_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
2281 var sids = self.findParentSectionIDs($prev_panel);
2283 self.setPanelIndex(sids[1], $this_panel.parent().index());
2287 .addClass('collapse');
2291 .children('span:last')
2293 .append(self.teaser(sids[0]));
2298 ev.stopPropagation();
2301 handleSort: function(ev)
2303 var self = ev.data.self;
2305 var $item = $(this).parents('.luci2-section-item:first');
2306 var $next = ev.data.up ? $item.prev() : $item.next();
2308 if ($item.length && $next.length)
2310 var cur_sid = $item.attr('data-luci2-sid');
2311 var new_sid = $next.attr('data-luci2-sid');
2313 L.uci.swap(self.ownerMap.uci_package, cur_sid, new_sid);
2315 self.ownerMap.save();
2316 self.ownerMap.redraw();
2319 ev.stopPropagation();
2322 getPanelIndex: function(parent_sid)
2324 return (this.active_panel[parent_sid || '__top__'] || 0);
2327 setPanelIndex: function(parent_sid, new_index)
2329 if (typeof(new_index) == 'number')
2330 this.active_panel[parent_sid || '__top__'] = new_index;
2333 renderAdd: function()
2335 if (!this.options.addremove)
2338 var text = L.tr('Add section');
2339 var ttip = L.tr('Create new section...');
2341 if ($.isArray(this.options.add_caption))
2342 text = this.options.add_caption[0], ttip = this.options.add_caption[1];
2343 else if (typeof(this.options.add_caption) == 'string')
2344 text = this.options.add_caption, ttip = '';
2346 var add = $('<div />');
2348 if (this.options.anonymous === false)
2351 .addClass('cbi-input-text')
2352 .attr('type', 'text')
2353 .attr('placeholder', ttip)
2354 .blur({ self: this }, this.handleSID)
2355 .keyup({ self: this }, this.handleSID)
2359 .attr('src', L.globals.resource + '/icons/cbi/add.gif')
2360 .attr('title', text)
2361 .addClass('cbi-button')
2362 .click({ self: this }, this.handleAdd)
2366 .addClass('cbi-value-error')
2372 L.ui.button(text, 'success', ttip)
2373 .click({ self: this }, this.handleAdd)
2380 renderRemove: function(index)
2382 if (!this.options.addremove)
2385 var text = L.tr('Remove');
2386 var ttip = L.tr('Remove this section');
2388 if ($.isArray(this.options.remove_caption))
2389 text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
2390 else if (typeof(this.options.remove_caption) == 'string')
2391 text = this.options.remove_caption, ttip = '';
2393 return L.ui.button(text, 'danger', ttip)
2394 .click({ self: this, index: index }, this.handleRemove);
2397 renderSort: function(index)
2399 if (!this.options.sortable)
2402 var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
2403 .click({ self: this, index: index, up: true }, this.handleSort);
2405 var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
2406 .click({ self: this, index: index, up: false }, this.handleSort);
2411 renderCaption: function()
2414 .addClass('panel-title')
2415 .append(this.label('caption') || this.uci_type);
2418 renderDescription: function()
2420 var text = this.label('description');
2424 .addClass('luci2-section-description')
2430 renderTeaser: function(sid, index)
2432 if (this.options.collabsible || this.ownerMap.options.collabsible)
2435 .attr('id', this.id('teaser', sid))
2436 .addClass('luci2-section-teaser well well-sm')
2437 .append($('<span />')
2439 .append($('<span />'));
2445 renderHead: function(condensed)
2451 .addClass('panel-heading')
2452 .append(this.renderCaption())
2453 .append(this.renderDescription());
2456 renderTabDescription: function(sid, index, tab_index)
2458 var tab = this.tabs[tab_index];
2460 if (typeof(tab.description) == 'string')
2463 .addClass('cbi-tab-descr')
2464 .text(tab.description);
2470 renderTabHead: function(sid, index, tab_index)
2472 var tab = this.tabs[tab_index];
2473 var cur = this.active_tab[sid] || 0;
2475 var tabh = $('<li />')
2477 .attr('id', this.id('nodetab', sid, tab.id))
2478 .attr('href', '#' + this.id('node', sid, tab.id))
2479 .attr('data-toggle', 'tab')
2480 .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
2481 .append($('<span />')
2483 .on('shown.bs.tab', { self: this, sid: sid }, this.handleTab));
2485 if (cur == tab_index)
2486 tabh.addClass('active');
2488 if (!tab.fields.length)
2494 renderTabBody: function(sid, index, tab_index)
2496 var tab = this.tabs[tab_index];
2497 var cur = this.active_tab[sid] || 0;
2499 var tabb = $('<div />')
2500 .addClass('tab-pane')
2501 .attr('id', this.id('node', sid, tab.id))
2502 .append(this.renderTabDescription(sid, index, tab_index))
2503 .on('validate', this.handleTabValidate);
2505 if (cur == tab_index)
2506 tabb.addClass('active');
2508 for (var i = 0; i < tab.fields.length; i++)
2509 tabb.append(tab.fields[i].render(sid));
2514 renderPanelHead: function(sid, index, parent_sid)
2516 var head = $('<div />')
2517 .addClass('luci2-section-header')
2518 .append(this.renderTeaser(sid, index))
2519 .append($('<div />')
2520 .addClass('btn-group')
2521 .append(this.renderSort(index))
2522 .append(this.renderRemove(index)));
2524 if (this.options.collabsible)
2526 head.attr('data-toggle', 'collapse')
2527 .attr('data-parent', this.id('sectiongroup', parent_sid))
2528 .attr('data-target', '#' + this.id('panel', sid));
2534 renderPanelBody: function(sid, index, parent_sid)
2536 var body = $('<div />')
2537 .attr('id', this.id('panel', sid))
2538 .addClass('luci2-section-panel')
2539 .on('validate', this.handlePanelValidate);
2541 if (this.options.collabsible || this.ownerMap.options.collabsible)
2543 body.addClass('panel-collapse collapse');
2545 if (index == this.getPanelIndex(parent_sid))
2546 body.addClass('in');
2549 var tab_heads = $('<ul />')
2550 .addClass('nav nav-tabs');
2552 var tab_bodies = $('<div />')
2553 .addClass('form-horizontal tab-content')
2556 for (var j = 0; j < this.tabs.length; j++)
2558 tab_heads.append(this.renderTabHead(sid, index, j));
2559 tab_bodies.append(this.renderTabBody(sid, index, j));
2562 body.append(tab_bodies);
2564 if (this.tabs.length <= 1)
2567 for (var i = 0; i < this.subsections.length; i++)
2568 body.append(this.subsections[i].render(false, sid));
2573 renderBody: function(condensed, parent_sid)
2575 var s = this.getUCISections(parent_sid);
2576 var n = this.getPanelIndex(parent_sid);
2579 this.setPanelIndex(parent_sid, n + s.length);
2580 else if (n >= s.length)
2581 this.setPanelIndex(parent_sid, s.length - 1);
2583 var body = $('<ul />')
2584 .addClass('luci2-section-group list-group');
2586 if (this.options.collabsible)
2588 body.attr('id', this.id('sectiongroup', parent_sid))
2589 .on('show.bs.collapse', { self: this }, this.handlePanelCollapse);
2594 body.append($('<li />')
2595 .addClass('list-group-item text-muted')
2596 .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
2599 for (var i = 0; i < s.length; i++)
2601 var sid = s[i]['.name'];
2602 var inst = this.instance[sid] = { tabs: [ ] };
2604 body.append($('<li />')
2605 .addClass('luci2-section-item list-group-item')
2606 .attr('id', this.id('sectionitem', sid))
2607 .attr('data-luci2-sid', sid)
2608 .append(this.renderPanelHead(sid, i, parent_sid))
2609 .append(this.renderPanelBody(sid, i, parent_sid)));
2615 render: function(condensed, parent_sid)
2617 this.instance = { };
2619 var panel = $('<div />')
2620 .addClass('panel panel-default')
2621 .append(this.renderHead(condensed))
2622 .append(this.renderBody(condensed, parent_sid));
2624 if (this.options.addremove)
2625 panel.append($('<div />')
2626 .addClass('panel-footer')
2627 .append(this.renderAdd()));
2632 finish: function(parent_sid)
2634 var s = this.getUCISections(parent_sid);
2636 for (var i = 0; i < s.length; i++)
2638 var sid = s[i]['.name'];
2640 if (i != this.getPanelIndex(parent_sid))
2641 $('#' + this.id('teaser', sid)).children('span:last')
2642 .append(this.teaser(sid));
2644 $('#' + this.id('teaser', sid))
2647 for (var j = 0; j < this.subsections.length; j++)
2648 this.subsections[j].finish(sid);
2653 cbi_class.TableSection = cbi_class.TypedSection.extend({
2654 renderTableHead: function()
2656 var thead = $('<thead />')
2658 .addClass('cbi-section-table-titles'));
2660 for (var j = 0; j < this.tabs[0].fields.length; j++)
2661 thead.children().append($('<th />')
2662 .addClass('cbi-section-table-cell')
2663 .css('width', this.tabs[0].fields[j].options.width || '')
2664 .append(this.tabs[0].fields[j].label('caption')));
2666 if (this.options.addremove !== false || this.options.sortable)
2667 thead.children().append($('<th />')
2668 .addClass('cbi-section-table-cell')
2674 renderTableRow: function(sid, index)
2676 var row = $('<tr />')
2677 .addClass('luci2-section-item')
2678 .attr('id', this.id('sectionitem', sid))
2679 .attr('data-luci2-sid', sid);
2681 for (var j = 0; j < this.tabs[0].fields.length; j++)
2683 row.append($('<td />')
2684 .css('width', this.tabs[0].fields[j].options.width || '')
2685 .append(this.tabs[0].fields[j].render(sid, true)));
2688 if (this.options.addremove !== false || this.options.sortable)
2690 row.append($('<td />')
2692 .addClass('text-right')
2693 .append($('<div />')
2694 .addClass('btn-group')
2695 .append(this.renderSort(index))
2696 .append(this.renderRemove(index))));
2702 renderTableBody: function(parent_sid)
2704 var s = this.getUCISections(parent_sid);
2706 var tbody = $('<tbody />');
2710 var cols = this.tabs[0].fields.length;
2712 if (this.options.addremove !== false || this.options.sortable)
2715 tbody.append($('<tr />')
2717 .addClass('text-muted')
2718 .attr('colspan', cols)
2719 .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
2722 for (var i = 0; i < s.length; i++)
2724 var sid = s[i]['.name'];
2725 var inst = this.instance[sid] = { tabs: [ ] };
2727 tbody.append(this.renderTableRow(sid, i));
2733 renderBody: function(condensed, parent_sid)
2735 return $('<table />')
2736 .addClass('table table-condensed table-hover')
2737 .append(this.renderTableHead())
2738 .append(this.renderTableBody(parent_sid));
2742 cbi_class.GridSection = cbi_class.TypedSection.extend({
2743 renderGridHead: function()
2745 var ghead = $('<div />').addClass('row hidden-xs');
2747 for (var j = 0; j < this.tabs[0].fields.length; j++)
2749 var wdh = this.tabs[0].fields[j].options.width;
2750 wdh = isNaN(wdh) ? this.options.dyn_width : wdh;
2752 ghead.append($('<div />')
2753 .addClass('col-sm-%d cell caption clearfix'.format(wdh))
2754 .append(this.tabs[0].fields[j].label('caption')));
2757 if (this.options.addremove !== false || this.options.sortable)
2759 var wdh = this.options.dyn_width + this.options.pad_width;
2760 ghead.append($('<div />')
2761 .addClass('col-xs-8 col-sm-%d cell'.format(wdh))
2768 renderGridRow: function(sid, index)
2770 var row = $('<div />')
2771 .addClass('row luci2-section-item')
2772 .attr('id', this.id('sectionitem', sid))
2773 .attr('data-luci2-sid', sid);
2775 for (var j = 0; j < this.tabs[0].fields.length; j++)
2777 var wdh = this.tabs[0].fields[j].options.width;
2778 wdh = isNaN(wdh) ? this.options.dyn_width : wdh;
2780 row.append($('<div />')
2781 .addClass('col-xs-4 hidden-sm hidden-md hidden-lg cell caption')
2782 .append(this.tabs[0].fields[j].label('caption')));
2784 row.append($('<div />')
2785 .addClass('col-xs-8 col-sm-%d cell content clearfix'.format(wdh))
2786 .append(this.tabs[0].fields[j].render(sid, true)));
2789 if (this.options.addremove !== false || this.options.sortable)
2791 var wdh = this.options.dyn_width + this.options.pad_width;
2792 row.append($('<div />')
2793 .addClass('col-xs-12 col-sm-%d cell'.format(wdh))
2794 .append($('<div />')
2795 .addClass('btn-group pull-right')
2796 .append(this.renderSort(index))
2797 .append(this.renderRemove(index))));
2803 renderGridBody: function(parent_sid)
2805 var s = this.getUCISections(parent_sid);
2810 var cols = this.tabs[0].fields.length;
2812 if (this.options.addremove !== false || this.options.sortable)
2815 rows.push($('<div />')
2817 .append($('<div />')
2818 .addClass('col-sm-12 cell placeholder text-muted')
2819 .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
2822 for (var i = 0; i < s.length; i++)
2824 var sid = s[i]['.name'];
2825 var inst = this.instance[sid] = { tabs: [ ] };
2827 rows.push(this.renderGridRow(sid, i));
2833 renderBody: function(condensed, parent_sid)
2840 var cols = this.tabs[0].fields.length;
2842 if (this.options.addremove !== false || this.options.sortable)
2845 for (var i = 0; i < cols; i++)
2847 var col = this.tabs[0].fields[i];
2848 if (col && !isNaN(col.options.width))
2849 fix_width += col.options.width;
2856 this.options.dyn_width = Math.floor((12 - fix_width) / n_dynamic);
2857 this.options.pad_width = (12 - fix_width) % n_dynamic;
2861 this.options.pad_width = 12 - fix_width;
2865 .addClass('luci2-grid luci2-grid-condensed')
2866 .append(this.renderGridHead())
2867 .append(this.renderGridBody(parent_sid));
2871 cbi_class.NamedSection = cbi_class.TypedSection.extend({
2872 getUCISections: function(cb)
2875 var sl = L.uci.sections(this.ownerMap.uci_package);
2877 for (var i = 0; i < sl.length; i++)
2878 if (sl[i]['.name'] == this.uci_type)
2884 if (typeof(cb) == 'function' && sa.length > 0)
2885 cb.call(this, sa[0]);
2891 cbi_class.SingleSection = cbi_class.NamedSection.extend({
2894 this.instance = { };
2895 this.instance[this.uci_type] = { tabs: [ ] };
2898 .addClass('luci2-section-item')
2899 .attr('id', this.id('sectionitem', this.uci_type))
2900 .attr('data-luci2-sid', this.uci_type)
2901 .append(this.renderPanelBody(this.uci_type, 0));
2905 cbi_class.DummySection = cbi_class.TypedSection.extend({
2906 getUCISections: function(cb)
2908 if (typeof(cb) == 'function')
2909 cb.apply(this, [ { '.name': this.uci_type } ]);
2911 return [ { '.name': this.uci_type } ];
2915 cbi_class.Map = L.ui.AbstractWidget.extend({
2916 init: function(uci_package, options)
2920 this.uci_package = uci_package;
2921 this.sections = [ ];
2922 this.options = L.defaults(options, {
2923 save: function() { },
2924 prepare: function() { }
2928 loadCallback: function()
2930 var deferreds = [ L.deferrable(this.options.prepare.call(this)) ];
2932 for (var i = 0; i < this.sections.length; i++)
2934 var rv = this.sections[i].load();
2935 deferreds.push.apply(deferreds, rv);
2938 return $.when.apply($, deferreds);
2944 var packages = [ this.uci_package ];
2946 for (var i = 0; i < this.sections.length; i++)
2947 packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
2949 for (var i = 0; i < packages.length; i++)
2950 if (!L.uci.writable(packages[i]))
2952 this.options.readonly = true;
2956 return L.uci.load(packages).then(function() {
2957 return self.loadCallback();
2961 handleTab: function(ev)
2963 ev.data.self.active_tab = $(ev.target).parent().index();
2966 handleApply: function(ev)
2968 var self = ev.data.self;
2970 self.trigger('apply', ev);
2973 handleSave: function(ev)
2975 var self = ev.data.self;
2977 self.send().then(function() {
2978 self.trigger('save', ev);
2982 handleReset: function(ev)
2984 var self = ev.data.self;
2986 self.trigger('reset', ev);
2990 renderTabHead: function(tab_index)
2992 var section = this.sections[tab_index];
2993 var cur = this.active_tab || 0;
2995 var tabh = $('<li />')
2997 .attr('id', section.id('sectiontab'))
2998 .attr('href', '#' + section.id('section'))
2999 .attr('data-toggle', 'tab')
3000 .text(section.label('caption') + ' ')
3001 .append($('<span />')
3003 .on('shown.bs.tab', { self: this }, this.handleTab));
3005 if (cur == tab_index)
3006 tabh.addClass('active');
3011 renderTabBody: function(tab_index)
3013 var section = this.sections[tab_index];
3014 var desc = section.label('description');
3015 var cur = this.active_tab || 0;
3017 var tabb = $('<div />')
3018 .addClass('tab-pane')
3019 .attr('id', section.id('section'));
3021 if (cur == tab_index)
3022 tabb.addClass('active');
3025 tabb.append($('<p />')
3028 var s = section.render(this.options.tabbed);
3030 if (this.options.readonly || section.options.readonly)
3031 s.find('input, select, button, img.cbi-button').attr('disabled', true);
3038 renderBody: function()
3040 var tabs = $('<ul />')
3041 .addClass('nav nav-tabs');
3043 var body = $('<div />')
3046 for (var i = 0; i < this.sections.length; i++)
3048 tabs.append(this.renderTabHead(i));
3049 body.append(this.renderTabBody(i));
3052 if (this.options.tabbed)
3053 body.addClass('tab-content');
3060 renderFooter: function()
3067 .addClass('panel panel-default panel-body text-right')
3068 .append($('<div />')
3069 .addClass('btn-group')
3070 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
3071 .click(evdata, this.handleApply))
3072 .append(L.ui.button(L.tr('Save'), 'default')
3073 .click(evdata, this.handleSave))
3074 .append(L.ui.button(L.tr('Reset'), 'default')
3075 .click(evdata, this.handleReset)));
3080 var map = $('<form />');
3082 if (typeof(this.options.caption) == 'string')
3083 map.append($('<h2 />')
3084 .text(this.options.caption));
3086 if (typeof(this.options.description) == 'string')
3087 map.append($('<p />')
3088 .text(this.options.description));
3090 map.append(this.renderBody());
3092 if (this.options.pageaction !== false)
3093 map.append(this.renderFooter());
3100 for (var i = 0; i < this.sections.length; i++)
3101 this.sections[i].finish();
3108 this.target.hide().empty().append(this.render());
3113 section: function(widget, uci_type, options)
3115 var w = widget ? new widget(uci_type, options) : null;
3117 if (!(w instanceof L.cbi.AbstractSection))
3118 throw 'Widget must be an instance of AbstractSection';
3121 w.index = this.sections.length;
3123 this.sections.push(w);
3127 add: function(conf, type, name)
3129 return L.uci.add(conf, type, name);
3132 remove: function(conf, sid)
3134 return L.uci.remove(conf, sid);
3137 get: function(conf, sid, opt)
3139 return L.uci.get(conf, sid, opt);
3142 set: function(conf, sid, opt, val)
3144 return L.uci.set(conf, sid, opt, val);
3147 validate: function()
3151 for (var i = 0; i < this.sections.length; i++)
3153 if (!this.sections[i].validate())
3164 if (self.options.readonly)
3165 return L.deferrable();
3167 var deferreds = [ ];
3169 for (var i = 0; i < self.sections.length; i++)
3171 var rv = self.sections[i].save();
3172 deferreds.push.apply(deferreds, rv);
3175 return $.when.apply($, deferreds).then(function() {
3176 return L.deferrable(self.options.save.call(self));
3182 if (!this.validate())
3183 return L.deferrable();
3187 L.ui.saveScrollTop();
3190 return this.save().then(function() {
3191 return L.uci.save();
3192 }).then(function() {
3193 return L.ui.updateChanges();
3194 }).then(function() {
3196 }).then(function() {
3200 L.ui.loading(false);
3201 L.ui.restoreScrollTop();
3207 var packages = [ this.uci_package ];
3209 for (var i = 0; i < this.sections.length; i++)
3210 packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
3212 L.uci.unload(packages);
3221 return self.insertInto(self.target);
3224 insertInto: function(id)
3227 self.target = $(id);
3232 return self.load().then(function() {
3233 self.target.empty().append(self.render());
3237 L.ui.loading(false);
3242 cbi_class.Modal = cbi_class.Map.extend({
3243 handleApply: function(ev)
3245 var self = ev.data.self;
3247 self.trigger('apply', ev);
3250 handleSave: function(ev)
3252 var self = ev.data.self;
3254 self.send().then(function() {
3255 self.trigger('save', ev);
3260 handleReset: function(ev)
3262 var self = ev.data.self;
3264 self.trigger('close', ev);
3269 renderFooter: function()
3276 .addClass('btn-group')
3277 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
3278 .click(evdata, this.handleApply))
3279 .append(L.ui.button(L.tr('Save'), 'default')
3280 .click(evdata, this.handleSave))
3281 .append(L.ui.button(L.tr('Cancel'), 'default')
3282 .click(evdata, this.handleReset));
3287 var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
3288 var map = $('<form />');
3290 var desc = this.label('description');
3292 map.append($('<p />').text(desc));
3294 map.append(this.renderBody());
3296 modal.find('.modal-body').append(map);
3297 modal.find('.modal-footer').append(this.renderFooter());
3314 return self.load().then(function() {
3318 L.ui.loading(false);
3328 return Class.extend(cbi_class);