summaryrefslogtreecommitdiff
path: root/luci2/htdocs/luci2/cbi.js
diff options
context:
space:
mode:
Diffstat (limited to 'luci2/htdocs/luci2/cbi.js')
-rw-r--r--luci2/htdocs/luci2/cbi.js3200
1 files changed, 3200 insertions, 0 deletions
diff --git a/luci2/htdocs/luci2/cbi.js b/luci2/htdocs/luci2/cbi.js
new file mode 100644
index 0000000..cfc677d
--- /dev/null
+++ b/luci2/htdocs/luci2/cbi.js
@@ -0,0 +1,3200 @@
+(function() {
+ var type = function(f, l)
+ {
+ f.message = l;
+ return f;
+ };
+
+ var cbi_class = {
+ validation: {
+ i18n: function(msg)
+ {
+ L.cbi.validation.message = L.tr(msg);
+ },
+
+ compile: function(code)
+ {
+ var pos = 0;
+ var esc = false;
+ var depth = 0;
+ var types = L.cbi.validation.types;
+ var stack = [ ];
+
+ code += ',';
+
+ for (var i = 0; i < code.length; i++)
+ {
+ if (esc)
+ {
+ esc = false;
+ continue;
+ }
+
+ switch (code.charCodeAt(i))
+ {
+ case 92:
+ esc = true;
+ break;
+
+ case 40:
+ case 44:
+ if (depth <= 0)
+ {
+ if (pos < i)
+ {
+ var label = code.substring(pos, i);
+ label = label.replace(/\\(.)/g, '$1');
+ label = label.replace(/^[ \t]+/g, '');
+ label = label.replace(/[ \t]+$/g, '');
+
+ if (label && !isNaN(label))
+ {
+ stack.push(parseFloat(label));
+ }
+ else if (label.match(/^(['"]).*\1$/))
+ {
+ stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
+ }
+ else if (typeof types[label] == 'function')
+ {
+ stack.push(types[label]);
+ stack.push([ ]);
+ }
+ else
+ {
+ throw "Syntax error, unhandled token '"+label+"'";
+ }
+ }
+ pos = i+1;
+ }
+ depth += (code.charCodeAt(i) == 40);
+ break;
+
+ case 41:
+ if (--depth <= 0)
+ {
+ if (typeof stack[stack.length-2] != 'function')
+ throw "Syntax error, argument list follows non-function";
+
+ stack[stack.length-1] =
+ L.cbi.validation.compile(code.substring(pos, i));
+
+ pos = i+1;
+ }
+ break;
+ }
+ }
+
+ return stack;
+ }
+ }
+ };
+
+ var validation = cbi_class.validation;
+
+ validation.types = {
+ 'integer': function()
+ {
+ if (this.match(/^-?[0-9]+$/) != null)
+ return true;
+
+ validation.i18n('Must be a valid integer');
+ return false;
+ },
+
+ 'uinteger': function()
+ {
+ if (validation.types['integer'].apply(this) && (this >= 0))
+ return true;
+
+ validation.i18n('Must be a positive integer');
+ return false;
+ },
+
+ 'float': function()
+ {
+ if (!isNaN(parseFloat(this)))
+ return true;
+
+ validation.i18n('Must be a valid number');
+ return false;
+ },
+
+ 'ufloat': function()
+ {
+ if (validation.types['float'].apply(this) && (this >= 0))
+ return true;
+
+ validation.i18n('Must be a positive number');
+ return false;
+ },
+
+ 'ipaddr': function()
+ {
+ if (L.parseIPv4(this) || L.parseIPv6(this))
+ return true;
+
+ validation.i18n('Must be a valid IP address');
+ return false;
+ },
+
+ 'ip4addr': function()
+ {
+ if (L.parseIPv4(this))
+ return true;
+
+ validation.i18n('Must be a valid IPv4 address');
+ return false;
+ },
+
+ 'ip6addr': function()
+ {
+ if (L.parseIPv6(this))
+ return true;
+
+ validation.i18n('Must be a valid IPv6 address');
+ return false;
+ },
+
+ 'netmask4': function()
+ {
+ if (L.isNetmask(L.parseIPv4(this)))
+ return true;
+
+ validation.i18n('Must be a valid IPv4 netmask');
+ return false;
+ },
+
+ 'netmask6': function()
+ {
+ if (L.isNetmask(L.parseIPv6(this)))
+ return true;
+
+ validation.i18n('Must be a valid IPv6 netmask6');
+ return false;
+ },
+
+ 'cidr4': function()
+ {
+ if (this.match(/^([0-9.]+)\/(\d{1,2})$/))
+ if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1))
+ return true;
+
+ validation.i18n('Must be a valid IPv4 prefix');
+ return false;
+ },
+
+ 'cidr6': function()
+ {
+ if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/))
+ if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1))
+ return true;
+
+ validation.i18n('Must be a valid IPv6 prefix');
+ return false;
+ },
+
+ 'ipmask4': function()
+ {
+ if (this.match(/^([0-9.]+)\/([0-9.]+)$/))
+ {
+ var addr = RegExp.$1, mask = RegExp.$2;
+ if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask)))
+ return true;
+ }
+
+ validation.i18n('Must be a valid IPv4 address/netmask pair');
+ return false;
+ },
+
+ 'ipmask6': function()
+ {
+ if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/))
+ {
+ var addr = RegExp.$1, mask = RegExp.$2;
+ if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask)))
+ return true;
+ }
+
+ validation.i18n('Must be a valid IPv6 address/netmask pair');
+ return false;
+ },
+
+ 'port': function()
+ {
+ if (validation.types['integer'].apply(this) &&
+ (this >= 0) && (this <= 65535))
+ return true;
+
+ validation.i18n('Must be a valid port number');
+ return false;
+ },
+
+ 'portrange': function()
+ {
+ if (this.match(/^(\d+)-(\d+)$/))
+ {
+ var p1 = RegExp.$1;
+ var p2 = RegExp.$2;
+
+ if (validation.types['port'].apply(p1) &&
+ validation.types['port'].apply(p2) &&
+ (parseInt(p1) <= parseInt(p2)))
+ return true;
+ }
+ else if (validation.types['port'].apply(this))
+ {
+ return true;
+ }
+
+ validation.i18n('Must be a valid port range');
+ return false;
+ },
+
+ 'macaddr': function()
+ {
+ if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null)
+ return true;
+
+ validation.i18n('Must be a valid MAC address');
+ return false;
+ },
+
+ 'host': function()
+ {
+ if (validation.types['hostname'].apply(this) ||
+ validation.types['ipaddr'].apply(this))
+ return true;
+
+ validation.i18n('Must be a valid hostname or IP address');
+ return false;
+ },
+
+ 'hostname': function()
+ {
+ if ((this.length <= 253) &&
+ ((this.match(/^[a-zA-Z0-9]+$/) != null ||
+ (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
+ this.match(/[^0-9.]/)))))
+ return true;
+
+ validation.i18n('Must be a valid host name');
+ return false;
+ },
+
+ 'network': function()
+ {
+ if (validation.types['uciname'].apply(this) ||
+ validation.types['host'].apply(this))
+ return true;
+
+ validation.i18n('Must be a valid network name');
+ return false;
+ },
+
+ 'wpakey': function()
+ {
+ var v = this;
+
+ if ((v.length == 64)
+ ? (v.match(/^[a-fA-F0-9]{64}$/) != null)
+ : ((v.length >= 8) && (v.length <= 63)))
+ return true;
+
+ validation.i18n('Must be a valid WPA key');
+ return false;
+ },
+
+ 'wepkey': function()
+ {
+ var v = this;
+
+ if (v.substr(0,2) == 's:')
+ v = v.substr(2);
+
+ if (((v.length == 10) || (v.length == 26))
+ ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null)
+ : ((v.length == 5) || (v.length == 13)))
+ return true;
+
+ validation.i18n('Must be a valid WEP key');
+ return false;
+ },
+
+ 'uciname': function()
+ {
+ if (this.match(/^[a-zA-Z0-9_]+$/) != null)
+ return true;
+
+ validation.i18n('Must be a valid UCI identifier');
+ return false;
+ },
+
+ 'range': function(min, max)
+ {
+ var val = parseFloat(this);
+
+ if (validation.types['integer'].apply(this) &&
+ !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max)))
+ return true;
+
+ validation.i18n('Must be a number between %d and %d');
+ return false;
+ },
+
+ 'min': function(min)
+ {
+ var val = parseFloat(this);
+
+ if (validation.types['integer'].apply(this) &&
+ !isNaN(min) && !isNaN(val) && (val >= min))
+ return true;
+
+ validation.i18n('Must be a number greater or equal to %d');
+ return false;
+ },
+
+ 'max': function(max)
+ {
+ var val = parseFloat(this);
+
+ if (validation.types['integer'].apply(this) &&
+ !isNaN(max) && !isNaN(val) && (val <= max))
+ return true;
+
+ validation.i18n('Must be a number lower or equal to %d');
+ return false;
+ },
+
+ 'rangelength': function(min, max)
+ {
+ var val = '' + this;
+
+ if (!isNaN(min) && !isNaN(max) &&
+ (val.length >= min) && (val.length <= max))
+ return true;
+
+ if (min != max)
+ validation.i18n('Must be between %d and %d characters');
+ else
+ validation.i18n('Must be %d characters');
+ return false;
+ },
+
+ 'minlength': function(min)
+ {
+ var val = '' + this;
+
+ if (!isNaN(min) && (val.length >= min))
+ return true;
+
+ validation.i18n('Must be at least %d characters');
+ return false;
+ },
+
+ 'maxlength': function(max)
+ {
+ var val = '' + this;
+
+ if (!isNaN(max) && (val.length <= max))
+ return true;
+
+ validation.i18n('Must be at most %d characters');
+ return false;
+ },
+
+ 'or': function()
+ {
+ var msgs = [ ];
+
+ for (var i = 0; i < arguments.length; i += 2)
+ {
+ delete validation.message;
+
+ if (typeof(arguments[i]) != 'function')
+ {
+ if (arguments[i] == this)
+ return true;
+ i--;
+ }
+ else if (arguments[i].apply(this, arguments[i+1]))
+ {
+ return true;
+ }
+
+ if (validation.message)
+ msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
+ }
+
+ validation.message = msgs.join( L.tr(' - or - '));
+ return false;
+ },
+
+ 'and': function()
+ {
+ var msgs = [ ];
+
+ for (var i = 0; i < arguments.length; i += 2)
+ {
+ delete validation.message;
+
+ if (typeof arguments[i] != 'function')
+ {
+ if (arguments[i] != this)
+ return false;
+ i--;
+ }
+ else if (!arguments[i].apply(this, arguments[i+1]))
+ {
+ return false;
+ }
+
+ if (validation.message)
+ msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
+ }
+
+ validation.message = msgs.join(', ');
+ return true;
+ },
+
+ 'neg': function()
+ {
+ return validation.types['or'].apply(
+ this.replace(/^[ \t]*![ \t]*/, ''), arguments);
+ },
+
+ 'list': function(subvalidator, subargs)
+ {
+ if (typeof subvalidator != 'function')
+ return false;
+
+ var tokens = this.match(/[^ \t]+/g);
+ for (var i = 0; i < tokens.length; i++)
+ if (!subvalidator.apply(tokens[i], subargs))
+ return false;
+
+ return true;
+ },
+
+ 'phonedigit': function()
+ {
+ if (this.match(/^[0-9\*#!\.]+$/) != null)
+ return true;
+
+ validation.i18n('Must be a valid phone number digit');
+ return false;
+ },
+
+ 'string': function()
+ {
+ return true;
+ }
+ };
+
+ cbi_class.AbstractValue = L.ui.AbstractWidget.extend({
+ init: function(name, options)
+ {
+ this.name = name;
+ this.instance = { };
+ this.dependencies = [ ];
+ this.rdependency = { };
+
+ this.options = L.defaults(options, {
+ placeholder: '',
+ datatype: 'string',
+ optional: false,
+ keep: true
+ });
+ },
+
+ id: function(sid)
+ {
+ return this.ownerSection.id('field', sid || '__unknown__', this.name);
+ },
+
+ render: function(sid, condensed)
+ {
+ var i = this.instance[sid] = { };
+
+ i.top = $('<div />')
+ .addClass('luci2-field');
+
+ if (!condensed)
+ {
+ i.top.addClass('form-group');
+
+ if (typeof(this.options.caption) == 'string')
+ $('<label />')
+ .addClass('col-lg-2 control-label')
+ .attr('for', this.id(sid))
+ .text(this.options.caption)
+ .appendTo(i.top);
+ }
+
+ i.error = $('<div />')
+ .hide()
+ .addClass('luci2-field-error label label-danger');
+
+ i.widget = $('<div />')
+ .addClass('luci2-field-widget')
+ .append(this.widget(sid))
+ .append(i.error)
+ .appendTo(i.top);
+
+ if (!condensed)
+ {
+ i.widget.addClass('col-lg-5');
+
+ $('<div />')
+ .addClass('col-lg-5')
+ .text((typeof(this.options.description) == 'string') ? this.options.description : '')
+ .appendTo(i.top);
+ }
+
+ return i.top;
+ },
+
+ active: function(sid)
+ {
+ return (this.instance[sid] && !this.instance[sid].disabled);
+ },
+
+ ucipath: function(sid)
+ {
+ return {
+ config: (this.options.uci_package || this.ownerMap.uci_package),
+ section: (this.options.uci_section || sid),
+ option: (this.options.uci_option || this.name)
+ };
+ },
+
+ ucivalue: function(sid)
+ {
+ var uci = this.ucipath(sid);
+ var val = this.ownerMap.get(uci.config, uci.section, uci.option);
+
+ if (typeof(val) == 'undefined')
+ return this.options.initial;
+
+ return val;
+ },
+
+ formvalue: function(sid)
+ {
+ var v = $('#' + this.id(sid)).val();
+ return (v === '') ? undefined : v;
+ },
+
+ textvalue: function(sid)
+ {
+ var v = this.formvalue(sid);
+
+ if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
+ v = this.ucivalue(sid);
+
+ if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
+ v = this.options.placeholder;
+
+ if (typeof(v) == 'undefined' || v === '')
+ return undefined;
+
+ if (typeof(v) == 'string' && $.isArray(this.choices))
+ {
+ for (var i = 0; i < this.choices.length; i++)
+ if (v === this.choices[i][0])
+ return this.choices[i][1];
+ }
+ else if (v === true)
+ return L.tr('yes');
+ else if (v === false)
+ return L.tr('no');
+ else if ($.isArray(v))
+ return v.join(', ');
+
+ return v;
+ },
+
+ changed: function(sid)
+ {
+ var a = this.ucivalue(sid);
+ var b = this.formvalue(sid);
+
+ if (typeof(a) != typeof(b))
+ return true;
+
+ if ($.isArray(a))
+ {
+ if (a.length != b.length)
+ return true;
+
+ for (var i = 0; i < a.length; i++)
+ if (a[i] != b[i])
+ return true;
+
+ return false;
+ }
+ else if ($.isPlainObject(a))
+ {
+ for (var k in a)
+ if (!(k in b))
+ return true;
+
+ for (var k in b)
+ if (!(k in a) || a[k] !== b[k])
+ return true;
+
+ return false;
+ }
+
+ return (a != b);
+ },
+
+ save: function(sid)
+ {
+ var uci = this.ucipath(sid);
+
+ if (this.instance[sid].disabled)
+ {
+ if (!this.options.keep)
+ return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
+
+ return false;
+ }
+
+ var chg = this.changed(sid);
+ var val = this.formvalue(sid);
+
+ if (chg)
+ this.ownerMap.set(uci.config, uci.section, uci.option, val);
+
+ return chg;
+ },
+
+ findSectionID: function($elem)
+ {
+ return this.ownerSection.findParentSectionIDs($elem)[0];
+ },
+
+ setError: function($elem, msg, msgargs)
+ {
+ var $field = $elem.parents('.luci2-field:first');
+ var $error = $field.find('.luci2-field-error:first');
+
+ if (typeof(msg) == 'string' && msg.length > 0)
+ {
+ $field.addClass('luci2-form-error');
+ $elem.parent().addClass('has-error');
+
+ $error.text(msg.format.apply(msg, msgargs)).show();
+ $field.trigger('validate');
+
+ return false;
+ }
+ else
+ {
+ $elem.parent().removeClass('has-error');
+
+ var $other_errors = $field.find('.has-error');
+ if ($other_errors.length == 0)
+ {
+ $field.removeClass('luci2-form-error');
+ $error.text('').hide();
+ $field.trigger('validate');
+
+ return true;
+ }
+
+ return false;
+ }
+ },
+
+ handleValidate: function(ev)
+ {
+ var $elem = $(this);
+
+ var d = ev.data;
+ var rv = true;
+ var val = $elem.val();
+ var vstack = d.vstack;
+
+ if (vstack && typeof(vstack[0]) == 'function')
+ {
+ delete validation.message;
+
+ if ((val.length == 0 && !d.opt))
+ {
+ rv = d.self.setError($elem, L.tr('Field must not be empty'));
+ }
+ else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
+ {
+ rv = d.self.setError($elem, validation.message, vstack[1]);
+ }
+ else
+ {
+ rv = d.self.setError($elem);
+ }
+ }
+
+ if (rv)
+ {
+ var sid = d.self.findSectionID($elem);
+
+ for (var field in d.self.rdependency)
+ {
+ d.self.rdependency[field].toggle(sid);
+ d.self.rdependency[field].validate(sid);
+ }
+
+ d.self.ownerSection.tabtoggle(sid);
+ }
+
+ return rv;
+ },
+
+ attachEvents: function(sid, elem)
+ {
+ var evdata = {
+ self: this,
+ opt: this.options.optional
+ };
+
+ if (this.events)
+ for (var evname in this.events)
+ elem.on(evname, evdata, this.events[evname]);
+
+ if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
+ return elem;
+
+ var vstack;
+ if (typeof(this.options.datatype) == 'string')
+ {
+ try {
+ evdata.vstack = L.cbi.validation.compile(this.options.datatype);
+ } catch(e) { };
+ }
+ else if (typeof(this.options.datatype) == 'function')
+ {
+ var vfunc = this.options.datatype;
+ evdata.vstack = [ function(elem) {
+ var rv = vfunc(this, elem);
+ if (rv !== true)
+ validation.message = rv;
+ return (rv === true);
+ }, [ elem ] ];
+ }
+
+ if (elem.prop('tagName') == 'SELECT')
+ {
+ elem.change(evdata, this.handleValidate);
+ }
+ else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
+ {
+ elem.click(evdata, this.handleValidate);
+ elem.blur(evdata, this.handleValidate);
+ }
+ else
+ {
+ elem.keyup(evdata, this.handleValidate);
+ elem.blur(evdata, this.handleValidate);
+ }
+
+ elem.addClass('luci2-field-validate')
+ .on('validate', evdata, this.handleValidate);
+
+ return elem;
+ },
+
+ validate: function(sid)
+ {
+ var i = this.instance[sid];
+
+ i.widget.find('.luci2-field-validate').trigger('validate');
+
+ return (i.disabled || i.error.text() == '');
+ },
+
+ depends: function(d, v, add)
+ {
+ var dep;
+
+ if ($.isArray(d))
+ {
+ dep = { };
+ for (var i = 0; i < d.length; i++)
+ {
+ if (typeof(d[i]) == 'string')
+ dep[d[i]] = true;
+ else if (d[i] instanceof L.cbi.AbstractValue)
+ dep[d[i].name] = true;
+ }
+ }
+ else if (d instanceof L.cbi.AbstractValue)
+ {
+ dep = { };
+ dep[d.name] = (typeof(v) == 'undefined') ? true : v;
+ }
+ else if (typeof(d) == 'object')
+ {
+ dep = d;
+ }
+ else if (typeof(d) == 'string')
+ {
+ dep = { };
+ dep[d] = (typeof(v) == 'undefined') ? true : v;
+ }
+
+ if (!dep || $.isEmptyObject(dep))
+ return this;
+
+ for (var field in dep)
+ {
+ var f = this.ownerSection.fields[field];
+ if (f)
+ f.rdependency[this.name] = this;
+ else
+ delete dep[field];
+ }
+
+ if ($.isEmptyObject(dep))
+ return this;
+
+ if (!add || !this.dependencies.length)
+ this.dependencies.push(dep);
+ else
+ for (var i = 0; i < this.dependencies.length; i++)
+ $.extend(this.dependencies[i], dep);
+
+ return this;
+ },
+
+ toggle: function(sid)
+ {
+ var d = this.dependencies;
+ var i = this.instance[sid];
+
+ if (!d.length)
+ return true;
+
+ for (var n = 0; n < d.length; n++)
+ {
+ var rv = true;
+
+ for (var field in d[n])
+ {
+ var val = this.ownerSection.fields[field].formvalue(sid);
+ var cmp = d[n][field];
+
+ if (typeof(cmp) == 'boolean')
+ {
+ if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
+ {
+ rv = false;
+ break;
+ }
+ }
+ else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
+ {
+ if (val != cmp)
+ {
+ rv = false;
+ break;
+ }
+ }
+ else if (typeof(cmp) == 'function')
+ {
+ if (!cmp(val))
+ {
+ rv = false;
+ break;
+ }
+ }
+ else if (cmp instanceof RegExp)
+ {
+ if (!cmp.test(val))
+ {
+ rv = false;
+ break;
+ }
+ }
+ }
+
+ if (rv)
+ {
+ if (i.disabled)
+ {
+ i.disabled = false;
+ i.top.removeClass('luci2-field-disabled');
+ i.top.fadeIn();
+ }
+
+ return true;
+ }
+ }
+
+ if (!i.disabled)
+ {
+ i.disabled = true;
+ i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
+ i.top.addClass('luci2-field-disabled');
+ }
+
+ return false;
+ }
+ });
+
+ cbi_class.CheckboxValue = cbi_class.AbstractValue.extend({
+ widget: function(sid)
+ {
+ var o = this.options;
+
+ if (typeof(o.enabled) == 'undefined') o.enabled = '1';
+ if (typeof(o.disabled) == 'undefined') o.disabled = '0';
+
+ var i = $('<input />')
+ .attr('id', this.id(sid))
+ .attr('type', 'checkbox')
+ .prop('checked', this.ucivalue(sid));
+
+ return $('<div />')
+ .addClass('checkbox')
+ .append(this.attachEvents(sid, i));
+ },
+
+ ucivalue: function(sid)
+ {
+ var v = this.callSuper('ucivalue', sid);
+
+ if (typeof(v) == 'boolean')
+ return v;
+
+ return (v == this.options.enabled);
+ },
+
+ formvalue: function(sid)
+ {
+ var v = $('#' + this.id(sid)).prop('checked');
+
+ if (typeof(v) == 'undefined')
+ return !!this.options.initial;
+
+ return v;
+ },
+
+ save: function(sid)
+ {
+ var uci = this.ucipath(sid);
+
+ if (this.instance[sid].disabled)
+ {
+ if (!this.options.keep)
+ return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
+
+ return false;
+ }
+
+ var chg = this.changed(sid);
+ var val = this.formvalue(sid);
+
+ if (chg)
+ {
+ if (this.options.optional && val == this.options.initial)
+ this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
+ else
+ this.ownerMap.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
+ }
+
+ return chg;
+ }
+ });
+
+ cbi_class.InputValue = cbi_class.AbstractValue.extend({
+ widget: function(sid)
+ {
+ var i = $('<input />')
+ .addClass('form-control')
+ .attr('id', this.id(sid))
+ .attr('type', 'text')
+ .attr('placeholder', this.options.placeholder)
+ .val(this.ucivalue(sid));
+
+ return this.attachEvents(sid, i);
+ }
+ });
+
+ cbi_class.PasswordValue = cbi_class.AbstractValue.extend({
+ widget: function(sid)
+ {
+ var i = $('<input />')
+ .addClass('form-control')
+ .attr('id', this.id(sid))
+ .attr('type', 'password')
+ .attr('placeholder', this.options.placeholder)
+ .val(this.ucivalue(sid));
+
+ var t = $('<span />')
+ .addClass('input-group-btn')
+ .append(L.ui.button(L.tr('Reveal'), 'default')
+ .click(function(ev) {
+ var b = $(this);
+ var i = b.parent().prev();
+ var t = i.attr('type');
+ b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
+ i.attr('type', (t == 'password') ? 'text' : 'password');
+ b = i = t = null;
+ }));
+
+ this.attachEvents(sid, i);
+
+ return $('<div />')
+ .addClass('input-group')
+ .append(i)
+ .append(t);
+ }
+ });
+
+ cbi_class.ListValue = cbi_class.AbstractValue.extend({
+ widget: function(sid)
+ {
+ var s = $('<select />')
+ .addClass('form-control');
+
+ if (this.options.optional && !this.has_empty)
+ $('<option />')
+ .attr('value', '')
+ .text(L.tr('-- Please choose --'))
+ .appendTo(s);
+
+ if (this.choices)
+ for (var i = 0; i < this.choices.length; i++)
+ $('<option />')
+ .attr('value', this.choices[i][0])
+ .text(this.choices[i][1])
+ .appendTo(s);
+
+ s.attr('id', this.id(sid)).val(this.ucivalue(sid));
+
+ return this.attachEvents(sid, s);
+ },
+
+ value: function(k, v)
+ {
+ if (!this.choices)
+ this.choices = [ ];
+
+ if (k == '')
+ this.has_empty = true;
+
+ this.choices.push([k, v || k]);
+ return this;
+ }
+ });
+
+ cbi_class.MultiValue = cbi_class.ListValue.extend({
+ widget: function(sid)
+ {
+ var v = this.ucivalue(sid);
+ var t = $('<div />').attr('id', this.id(sid));
+
+ if (!$.isArray(v))
+ v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
+
+ var s = { };
+ for (var i = 0; i < v.length; i++)
+ s[v[i]] = true;
+
+ if (this.choices)
+ for (var i = 0; i < this.choices.length; i++)
+ {
+ $('<label />')
+ .addClass('checkbox')
+ .append($('<input />')
+ .attr('type', 'checkbox')
+ .attr('value', this.choices[i][0])
+ .prop('checked', s[this.choices[i][0]]))
+ .append(this.choices[i][1])
+ .appendTo(t);
+ }
+
+ return t;
+ },
+
+ formvalue: function(sid)
+ {
+ var rv = [ ];
+ var fields = $('#' + this.id(sid) + ' > label > input');
+
+ for (var i = 0; i < fields.length; i++)
+ if (fields[i].checked)
+ rv.push(fields[i].getAttribute('value'));
+
+ return rv;
+ },
+
+ textvalue: function(sid)
+ {
+ var v = this.formvalue(sid);
+ var c = { };
+
+ if (this.choices)
+ for (var i = 0; i < this.choices.length; i++)
+ c[this.choices[i][0]] = this.choices[i][1];
+
+ var t = [ ];
+
+ for (var i = 0; i < v.length; i++)
+ t.push(c[v[i]] || v[i]);
+
+ return t.join(', ');
+ }
+ });
+
+ cbi_class.ComboBox = cbi_class.AbstractValue.extend({
+ _change: function(ev)
+ {
+ var s = ev.target;
+ var self = ev.data.self;
+
+ if (s.selectedIndex == (s.options.length - 1))
+ {
+ ev.data.select.hide();
+ ev.data.input.show().focus();
+ ev.data.input.val('');
+ }
+ else if (self.options.optional && s.selectedIndex == 0)
+ {
+ ev.data.input.val('');
+ }
+ else
+ {
+ ev.data.input.val(ev.data.select.val());
+ }
+
+ ev.stopPropagation();
+ },
+
+ _blur: function(ev)
+ {
+ var seen = false;
+ var val = this.value;
+ var self = ev.data.self;
+
+ ev.data.select.empty();
+
+ if (self.options.optional && !self.has_empty)
+ $('<option />')
+ .attr('value', '')
+ .text(L.tr('-- please choose --'))
+ .appendTo(ev.data.select);
+
+ if (self.choices)
+ for (var i = 0; i < self.choices.length; i++)
+ {
+ if (self.choices[i][0] == val)
+ seen = true;
+
+ $('<option />')
+ .attr('value', self.choices[i][0])
+ .text(self.choices[i][1])
+ .appendTo(ev.data.select);
+ }
+
+ if (!seen && val != '')
+ $('<option />')
+ .attr('value', val)
+ .text(val)
+ .appendTo(ev.data.select);
+
+ $('<option />')
+ .attr('value', ' ')
+ .text(L.tr('-- custom --'))
+ .appendTo(ev.data.select);
+
+ ev.data.input.hide();
+ ev.data.select.val(val).show().blur();
+ },
+
+ _enter: function(ev)
+ {
+ if (ev.which != 13)
+ return true;
+
+ ev.preventDefault();
+ ev.data.self._blur(ev);
+ return false;
+ },
+
+ widget: function(sid)
+ {
+ var d = $('<div />')
+ .attr('id', this.id(sid));
+
+ var t = $('<input />')
+ .addClass('form-control')
+ .attr('type', 'text')
+ .hide()
+ .appendTo(d);
+
+ var s = $('<select />')
+ .addClass('form-control')
+ .appendTo(d);
+
+ var evdata = {
+ self: this,
+ input: t,
+ select: s
+ };
+
+ s.change(evdata, this._change);
+ t.blur(evdata, this._blur);
+ t.keydown(evdata, this._enter);
+
+ t.val(this.ucivalue(sid));
+ t.blur();
+
+ this.attachEvents(sid, t);
+ this.attachEvents(sid, s);
+
+ return d;
+ },
+
+ value: function(k, v)
+ {
+ if (!this.choices)
+ this.choices = [ ];
+
+ if (k == '')
+ this.has_empty = true;
+
+ this.choices.push([k, v || k]);
+ return this;
+ },
+
+ formvalue: function(sid)
+ {
+ var v = $('#' + this.id(sid)).children('input').val();
+ return (v == '') ? undefined : v;
+ }
+ });
+
+ cbi_class.DynamicList = cbi_class.ComboBox.extend({
+ _redraw: function(focus, add, del, s)
+ {
+ var v = s.values || [ ];
+ delete s.values;
+
+ $(s.parent).children('div.input-group').children('input').each(function(i) {
+ if (i != del)
+ v.push(this.value || '');
+ });
+
+ $(s.parent).empty();
+
+ if (add >= 0)
+ {
+ focus = add + 1;
+ v.splice(focus, 0, '');
+ }
+ else if (v.length == 0)
+ {
+ focus = 0;
+ v.push('');
+ }
+
+ for (var i = 0; i < v.length; i++)
+ {
+ var evdata = {
+ sid: s.sid,
+ self: s.self,
+ parent: s.parent,
+ index: i,
+ remove: ((i+1) < v.length)
+ };
+
+ var btn;
+ if (evdata.remove)
+ btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
+ else
+ btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
+
+ if (this.choices)
+ {
+ var txt = $('<input />')
+ .addClass('form-control')
+ .attr('type', 'text')
+ .hide();
+
+ var sel = $('<select />')
+ .addClass('form-control');
+
+ $('<div />')
+ .addClass('input-group')
+ .append(txt)
+ .append(sel)
+ .append($('<span />')
+ .addClass('input-group-btn')
+ .append(btn))
+ .appendTo(s.parent);
+
+ evdata.input = this.attachEvents(s.sid, txt);
+ evdata.select = this.attachEvents(s.sid, sel);
+
+ sel.change(evdata, this._change);
+ txt.blur(evdata, this._blur);
+ txt.keydown(evdata, this._keydown);
+
+ txt.val(v[i]);
+ txt.blur();
+
+ if (i == focus || -(i+1) == focus)
+ sel.focus();
+
+ sel = txt = null;
+ }
+ else
+ {
+ var f = $('<input />')
+ .attr('type', 'text')
+ .attr('index', i)
+ .attr('placeholder', (i == 0) ? this.options.placeholder : '')
+ .addClass('form-control')
+ .keydown(evdata, this._keydown)
+ .keypress(evdata, this._keypress)
+ .val(v[i]);
+
+ $('<div />')
+ .addClass('input-group')
+ .append(f)
+ .append($('<span />')
+ .addClass('input-group-btn')
+ .append(btn))
+ .appendTo(s.parent);
+
+ if (i == focus)
+ {
+ f.focus();
+ }
+ else if (-(i+1) == focus)
+ {
+ f.focus();
+
+ /* force cursor to end */
+ var val = f.val();
+ f.val(' ');
+ f.val(val);
+ }
+
+ evdata.input = this.attachEvents(s.sid, f);
+
+ f = null;
+ }
+
+ evdata = null;
+ }
+
+ s = null;
+ },
+
+ _keypress: function(ev)
+ {
+ switch (ev.which)
+ {
+ /* backspace, delete */
+ case 8:
+ case 46:
+ if (ev.data.input.val() == '')
+ {
+ ev.preventDefault();
+ return false;
+ }
+
+ return true;
+
+ /* enter, arrow up, arrow down */
+ case 13:
+ case 38:
+ case 40:
+ ev.preventDefault();
+ return false;
+ }
+
+ return true;
+ },
+
+ _keydown: function(ev)
+ {
+ var input = ev.data.input;
+
+ switch (ev.which)
+ {
+ /* backspace, delete */
+ case 8:
+ case 46:
+ if (input.val().length == 0)
+ {
+ ev.preventDefault();
+
+ var index = ev.data.index;
+ var focus = index;
+
+ if (ev.which == 8)
+ focus = -focus;
+
+ ev.data.self._redraw(focus, -1, index, ev.data);
+ return false;
+ }
+
+ break;
+
+ /* enter */
+ case 13:
+ ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
+ break;
+
+ /* arrow up */
+ case 38:
+ var prev = input.parent().prevAll('div.input-group:first').children('input');
+ if (prev.is(':visible'))
+ prev.focus();
+ else
+ prev.next('select').focus();
+ break;
+
+ /* arrow down */
+ case 40:
+ var next = input.parent().nextAll('div.input-group:first').children('input');
+ if (next.is(':visible'))
+ next.focus();
+ else
+ next.next('select').focus();
+ break;
+ }
+
+ return true;
+ },
+
+ _btnclick: function(ev)
+ {
+ if (!this.getAttribute('disabled'))
+ {
+ if (ev.data.remove)
+ {
+ var index = ev.data.index;
+ ev.data.self._redraw(-index, -1, index, ev.data);
+ }
+ else
+ {
+ ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
+ }
+ }
+
+ return false;
+ },
+
+ widget: function(sid)
+ {
+ this.options.optional = true;
+
+ var v = this.ucivalue(sid);
+
+ if (!$.isArray(v))
+ v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
+
+ var d = $('<div />')
+ .attr('id', this.id(sid))
+ .addClass('cbi-input-dynlist');
+
+ this._redraw(NaN, -1, -1, {
+ self: this,
+ parent: d[0],
+ values: v,
+ sid: sid
+ });
+
+ return d;
+ },
+
+ ucivalue: function(sid)
+ {
+ var v = this.callSuper('ucivalue', sid);
+
+ if (!$.isArray(v))
+ v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
+
+ return v;
+ },
+
+ formvalue: function(sid)
+ {
+ var rv = [ ];
+ var fields = $('#' + this.id(sid) + ' input');
+
+ for (var i = 0; i < fields.length; i++)
+ if (typeof(fields[i].value) == 'string' && fields[i].value.length)
+ rv.push(fields[i].value);
+
+ return rv;
+ }
+ });
+
+ cbi_class.DummyValue = cbi_class.AbstractValue.extend({
+ widget: function(sid)
+ {
+ return $('<div />')
+ .addClass('form-control-static')
+ .attr('id', this.id(sid))
+ .html(this.ucivalue(sid) || this.label('placeholder'));
+ },
+
+ formvalue: function(sid)
+ {
+ return this.ucivalue(sid);
+ }
+ });
+
+ cbi_class.ButtonValue = cbi_class.AbstractValue.extend({
+ widget: function(sid)
+ {
+ this.options.optional = true;
+
+ var btn = $('<button />')
+ .addClass('btn btn-default')
+ .attr('id', this.id(sid))
+ .attr('type', 'button')
+ .text(this.label('text'));
+
+ return this.attachEvents(sid, btn);
+ }
+ });
+
+ cbi_class.NetworkList = cbi_class.AbstractValue.extend({
+ load: function(sid)
+ {
+ return L.network.load();
+ },
+
+ _device_icon: function(dev)
+ {
+ return $('<img />')
+ .attr('src', dev.icon())
+ .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
+ },
+
+ widget: function(sid)
+ {
+ var id = this.id(sid);
+ var ul = $('<ul />')
+ .attr('id', id)
+ .addClass('list-unstyled');
+
+ var itype = this.options.multiple ? 'checkbox' : 'radio';
+ var value = this.ucivalue(sid);
+ var check = { };
+
+ if (!this.options.multiple)
+ check[value] = true;
+ else
+ for (var i = 0; i < value.length; i++)
+ check[value[i]] = true;
+
+ var interfaces = L.network.getInterfaces();
+
+ for (var i = 0; i < interfaces.length; i++)
+ {
+ var iface = interfaces[i];
+
+ $('<li />')
+ .append($('<label />')
+ .addClass(itype + ' inline')
+ .append(this.attachEvents(sid, $('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', iface.name())
+ .prop('checked', !!check[iface.name()])))
+ .append(iface.renderBadge()))
+ .appendTo(ul);
+ }
+
+ if (!this.options.multiple)
+ {
+ $('<li />')
+ .append($('<label />')
+ .addClass(itype + ' inline text-muted')
+ .append(this.attachEvents(sid, $('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', '')
+ .prop('checked', $.isEmptyObject(check))))
+ .append(L.tr('unspecified')))
+ .appendTo(ul);
+ }
+
+ return ul;
+ },
+
+ ucivalue: function(sid)
+ {
+ var v = this.callSuper('ucivalue', sid);
+
+ if (!this.options.multiple)
+ {
+ if ($.isArray(v))
+ {
+ return v[0];
+ }
+ else if (typeof(v) == 'string')
+ {
+ v = v.match(/\S+/);
+ return v ? v[0] : undefined;
+ }
+
+ return v;
+ }
+ else
+ {
+ if (typeof(v) == 'string')
+ v = v.match(/\S+/g);
+
+ return v || [ ];
+ }
+ },
+
+ formvalue: function(sid)
+ {
+ var inputs = $('#' + this.id(sid) + ' input');
+
+ if (!this.options.multiple)
+ {
+ for (var i = 0; i < inputs.length; i++)
+ if (inputs[i].checked && inputs[i].value !== '')
+ return inputs[i].value;
+
+ return undefined;
+ }
+
+ var rv = [ ];
+
+ for (var i = 0; i < inputs.length; i++)
+ if (inputs[i].checked)
+ rv.push(inputs[i].value);
+
+ return rv.length ? rv : undefined;
+ }
+ });
+
+ cbi_class.DeviceList = cbi_class.NetworkList.extend({
+ handleFocus: function(ev)
+ {
+ var self = ev.data.self;
+ var input = $(this);
+
+ input.parent().prev().prop('checked', true);
+ },
+
+ handleBlur: function(ev)
+ {
+ ev.which = 10;
+ ev.data.self.handleKeydown.call(this, ev);
+ },
+
+ handleKeydown: function(ev)
+ {
+ if (ev.which != 10 && ev.which != 13)
+ return;
+
+ var sid = ev.data.sid;
+ var self = ev.data.self;
+ var input = $(this);
+ var ifnames = L.toArray(input.val());
+
+ if (!ifnames.length)
+ return;
+
+ L.network.createDevice(ifnames[0]);
+
+ self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
+ },
+
+ load: function(sid)
+ {
+ return L.network.load();
+ },
+
+ _redraw: function(sid, ul, sel)
+ {
+ var id = ul.attr('id');
+ var devs = L.network.getDevices();
+ var iface = L.network.getInterface(sid);
+ var itype = this.options.multiple ? 'checkbox' : 'radio';
+ var check = { };
+
+ if (!sel)
+ {
+ for (var i = 0; i < devs.length; i++)
+ if (devs[i].isInNetwork(iface))
+ check[devs[i].name()] = true;
+ }
+ else
+ {
+ if (this.options.multiple)
+ check = L.toObject(this.formvalue(sid));
+
+ check[sel] = true;
+ }
+
+ ul.empty();
+
+ for (var i = 0; i < devs.length; i++)
+ {
+ var dev = devs[i];
+
+ if (dev.isBridge() && this.options.bridges === false)
+ continue;
+
+ if (!dev.isBridgeable() && this.options.multiple)
+ continue;
+
+ var badge = $('<span />')
+ .addClass('badge')
+ .append($('<img />').attr('src', dev.icon()))
+ .append(' %s: %s'.format(dev.name(), dev.description()));
+
+ //var ifcs = dev.getInterfaces();
+ //if (ifcs.length)
+ //{
+ // for (var j = 0; j < ifcs.length; j++)
+ // badge.append((j ? ', ' : ' (') + ifcs[j].name());
+ //
+ // badge.append(')');
+ //}
+
+ $('<li />')
+ .append($('<label />')
+ .addClass(itype + ' inline')
+ .append($('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', dev.name())
+ .prop('checked', !!check[dev.name()]))
+ .append(badge))
+ .appendTo(ul);
+ }
+
+
+ $('<li />')
+ .append($('<label />')
+ .attr('for', 'custom' + id)
+ .addClass(itype + ' inline')
+ .append($('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', ''))
+ .append($('<span />')
+ .addClass('badge')
+ .append($('<input />')
+ .attr('id', 'custom' + id)
+ .attr('type', 'text')
+ .attr('placeholder', L.tr('Custom device …'))
+ .on('focus', { self: this, sid: sid }, this.handleFocus)
+ .on('blur', { self: this, sid: sid }, this.handleBlur)
+ .on('keydown', { self: this, sid: sid }, this.handleKeydown))))
+ .appendTo(ul);
+
+ if (!this.options.multiple)
+ {
+ $('<li />')
+ .append($('<label />')
+ .addClass(itype + ' inline text-muted')
+ .append($('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', '')
+ .prop('checked', $.isEmptyObject(check)))
+ .append(L.tr('unspecified')))
+ .appendTo(ul);
+ }
+ },
+
+ widget: function(sid)
+ {
+ var id = this.id(sid);
+ var ul = $('<ul />')
+ .attr('id', id)
+ .addClass('list-unstyled');
+
+ this._redraw(sid, ul);
+
+ return ul;
+ },
+
+ save: function(sid)
+ {
+ if (this.instance[sid].disabled)
+ return;
+
+ var ifnames = this.formvalue(sid);
+ //if (!ifnames)
+ // return;
+
+ var iface = L.network.getInterface(sid);
+ if (!iface)
+ return;
+
+ iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
+ }
+ });
+
+
+ cbi_class.AbstractSection = L.ui.AbstractWidget.extend({
+ id: function()
+ {
+ var s = [ arguments[0], this.ownerMap.uci_package, this.uci_type ];
+
+ for (var i = 1; i < arguments.length && typeof(arguments[i]) == 'string'; i++)
+ s.push(arguments[i].replace(/\./g, '_'));
+
+ return s.join('_');
+ },
+
+ option: function(widget, name, options)
+ {
+ if (this.tabs.length == 0)
+ this.tab({ id: '__default__', selected: true });
+
+ return this.taboption('__default__', widget, name, options);
+ },
+
+ tab: function(options)
+ {
+ if (options.selected)
+ this.tabs.selected = this.tabs.length;
+
+ this.tabs.push({
+ id: options.id,
+ caption: options.caption,
+ description: options.description,
+ fields: [ ],
+ li: { }
+ });
+ },
+
+ taboption: function(tabid, widget, name, options)
+ {
+ var tab;
+ for (var i = 0; i < this.tabs.length; i++)
+ {
+ if (this.tabs[i].id == tabid)
+ {
+ tab = this.tabs[i];
+ break;
+ }
+ }
+
+ if (!tab)
+ throw 'Cannot append to unknown tab ' + tabid;
+
+ var w = widget ? new widget(name, options) : null;
+
+ if (!(w instanceof L.cbi.AbstractValue))
+ throw 'Widget must be an instance of AbstractValue';
+
+ w.ownerSection = this;
+ w.ownerMap = this.ownerMap;
+
+ this.fields[name] = w;
+ tab.fields.push(w);
+
+ return w;
+ },
+
+ tabtoggle: function(sid)
+ {
+ for (var i = 0; i < this.tabs.length; i++)
+ {
+ var tab = this.tabs[i];
+ var elem = $('#' + this.id('nodetab', sid, tab.id));
+ var empty = true;
+
+ for (var j = 0; j < tab.fields.length; j++)
+ {
+ if (tab.fields[j].active(sid))
+ {
+ empty = false;
+ break;
+ }
+ }
+
+ if (empty && elem.is(':visible'))
+ elem.fadeOut();
+ else if (!empty)
+ elem.fadeIn();
+ }
+ },
+
+ validate: function(parent_sid)
+ {
+ var s = this.getUCISections(parent_sid);
+ var n = 0;
+
+ for (var i = 0; i < s.length; i++)
+ {
+ var $item = $('#' + this.id('sectionitem', s[i]['.name']));
+
+ $item.find('.luci2-field-validate').trigger('validate');
+ n += $item.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
+ }
+
+ return (n == 0);
+ },
+
+ load: function(parent_sid)
+ {
+ var deferreds = [ ];
+
+ var s = this.getUCISections(parent_sid);
+ for (var i = 0; i < s.length; i++)
+ {
+ for (var f in this.fields)
+ {
+ if (typeof(this.fields[f].load) != 'function')
+ continue;
+
+ var rv = this.fields[f].load(s[i]['.name']);
+ if (L.isDeferred(rv))
+ deferreds.push(rv);
+ }
+
+ for (var j = 0; j < this.subsections.length; j++)
+ {
+ var rv = this.subsections[j].load(s[i]['.name']);
+ deferreds.push.apply(deferreds, rv);
+ }
+ }
+
+ return deferreds;
+ },
+
+ save: function(parent_sid)
+ {
+ var deferreds = [ ];
+ var s = this.getUCISections(parent_sid);
+
+ for (i = 0; i < s.length; i++)
+ {
+ if (!this.options.readonly)
+ {
+ for (var f in this.fields)
+ {
+ if (typeof(this.fields[f].save) != 'function')
+ continue;
+
+ var rv = this.fields[f].save(s[i]['.name']);
+ if (L.isDeferred(rv))
+ deferreds.push(rv);
+ }
+ }
+
+ for (var j = 0; j < this.subsections.length; j++)
+ {
+ var rv = this.subsections[j].save(s[i]['.name']);
+ deferreds.push.apply(deferreds, rv);
+ }
+ }
+
+ return deferreds;
+ },
+
+ teaser: function(sid)
+ {
+ var tf = this.teaser_fields;
+
+ if (!tf)
+ {
+ tf = this.teaser_fields = [ ];
+
+ if ($.isArray(this.options.teasers))
+ {
+ for (var i = 0; i < this.options.teasers.length; i++)
+ {
+ var f = this.options.teasers[i];
+ if (f instanceof L.cbi.AbstractValue)
+ tf.push(f);
+ else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
+ tf.push(this.fields[f]);
+ }
+ }
+ else
+ {
+ for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
+ for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
+ tf.push(this.tabs[i].fields[j]);
+ }
+ }
+
+ var t = '';
+
+ for (var i = 0; i < tf.length; i++)
+ {
+ if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
+ continue;
+
+ var n = tf[i].options.caption || tf[i].name;
+ var v = tf[i].textvalue(sid);
+
+ if (typeof(v) == 'undefined')
+ continue;
+
+ t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
+ }
+
+ return t;
+ },
+
+ findAdditionalUCIPackages: function()
+ {
+ var packages = [ ];
+
+ for (var i = 0; i < this.tabs.length; i++)
+ for (var j = 0; j < this.tabs[i].fields.length; j++)
+ if (this.tabs[i].fields[j].options.uci_package)
+ packages.push(this.tabs[i].fields[j].options.uci_package);
+
+ return packages;
+ },
+
+ findParentSectionIDs: function($elem)
+ {
+ var rv = [ ];
+ var $parents = $elem.parents('.luci2-section-item');
+
+ for (var i = 0; i < $parents.length; i++)
+ rv.push($parents[i].getAttribute('data-luci2-sid'));
+
+ return rv;
+ }
+ });
+
+ cbi_class.TypedSection = cbi_class.AbstractSection.extend({
+ init: function(uci_type, options)
+ {
+ this.uci_type = uci_type;
+ this.options = options;
+ this.tabs = [ ];
+ this.fields = { };
+ this.subsections = [ ];
+ this.active_panel = { };
+ this.active_tab = { };
+
+ this.instance = { };
+ },
+
+ filter: function(section, parent_sid)
+ {
+ return true;
+ },
+
+ sort: function(section1, section2)
+ {
+ return 0;
+ },
+
+ subsection: function(widget, uci_type, options)
+ {
+ var w = widget ? new widget(uci_type, options) : null;
+
+ if (!(w instanceof L.cbi.AbstractSection))
+ throw 'Widget must be an instance of AbstractSection';
+
+ w.ownerSection = this;
+ w.ownerMap = this.ownerMap;
+ w.index = this.subsections.length;
+
+ this.subsections.push(w);
+ return w;
+ },
+
+ getUCISections: function(parent_sid)
+ {
+ var s1 = L.uci.sections(this.ownerMap.uci_package);
+ var s2 = [ ];
+
+ for (var i = 0; i < s1.length; i++)
+ if (s1[i]['.type'] == this.uci_type)
+ if (this.filter(s1[i], parent_sid))
+ s2.push(s1[i]);
+
+ s2.sort(this.sort);
+
+ return s2;
+ },
+
+ add: function(name, parent_sid)
+ {
+ return this.ownerMap.add(this.ownerMap.uci_package, this.uci_type, name);
+ },
+
+ remove: function(sid, parent_sid)
+ {
+ return this.ownerMap.remove(this.ownerMap.uci_package, sid);
+ },
+
+ handleAdd: function(ev)
+ {
+ var addb = $(this);
+ var name = undefined;
+ var self = ev.data.self;
+ var sid = self.findParentSectionIDs(addb)[0];
+
+ if (addb.prev().prop('nodeName') == 'INPUT')
+ name = addb.prev().val();
+
+ if (addb.prop('disabled') || name === '')
+ return;
+
+ L.ui.saveScrollTop();
+
+ self.setPanelIndex(sid, -1);
+ self.ownerMap.save();
+
+ ev.data.sid = self.add(name, sid);
+ ev.data.type = self.uci_type;
+ ev.data.name = name;
+
+ self.trigger('add', ev);
+
+ self.ownerMap.redraw();
+
+ L.ui.restoreScrollTop();
+ },
+
+ handleRemove: function(ev)
+ {
+ var self = ev.data.self;
+ var sids = self.findParentSectionIDs($(this));
+
+ if (sids.length)
+ {
+ L.ui.saveScrollTop();
+
+ ev.sid = sids[0];
+ ev.parent_sid = sids[1];
+
+ self.trigger('remove', ev);
+
+ self.ownerMap.save();
+ self.remove(ev.sid, ev.parent_sid);
+ self.ownerMap.redraw();
+
+ L.ui.restoreScrollTop();
+ }
+
+ ev.stopPropagation();
+ },
+
+ handleSID: function(ev)
+ {
+ var self = ev.data.self;
+ var text = $(this);
+ var addb = text.next();
+ var errt = addb.next();
+ var name = text.val();
+
+ if (!/^[a-zA-Z0-9_]*$/.test(name))
+ {
+ errt.text(L.tr('Invalid section name')).show();
+ text.addClass('error');
+ addb.prop('disabled', true);
+ return false;
+ }
+
+ if (L.uci.get(self.ownerMap.uci_package, name))
+ {
+ errt.text(L.tr('Name already used')).show();
+ text.addClass('error');
+ addb.prop('disabled', true);
+ return false;
+ }
+
+ errt.text('').hide();
+ text.removeClass('error');
+ addb.prop('disabled', false);
+ return true;
+ },
+
+ handleTab: function(ev)
+ {
+ var self = ev.data.self;
+ var $tab = $(this);
+ var sid = self.findParentSectionIDs($tab)[0];
+
+ self.active_tab[sid] = $tab.parent().index();
+ },
+
+ handleTabValidate: function(ev)
+ {
+ var $pane = $(ev.delegateTarget);
+ var $badge = $pane.parent()
+ .children('.nav-tabs')
+ .children('li')
+ .eq($pane.index() - 1) // item #1 is the <ul>
+ .find('.badge:first');
+
+ var err_count = $pane.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
+ if (err_count > 0)
+ $badge
+ .text(err_count)
+ .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
+ .show();
+ else
+ $badge.hide();
+ },
+
+ handlePanelValidate: function(ev)
+ {
+ var $elem = $(this);
+ var $badge = $elem
+ .prevAll('.luci2-section-header:first')
+ .children('.luci2-section-teaser')
+ .find('.badge:first');
+
+ var err_count = $elem.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
+ if (err_count > 0)
+ $badge
+ .text(err_count)
+ .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
+ .show();
+ else
+ $badge.hide();
+ },
+
+ handlePanelCollapse: function(ev)
+ {
+ var self = ev.data.self;
+
+ var $items = $(ev.delegateTarget).children('.luci2-section-item');
+
+ var $this_panel = $(ev.target);
+ var $this_teaser = $this_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
+
+ var $prev_panel = $items.children('.luci2-section-panel.in');
+ var $prev_teaser = $prev_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
+
+ var sids = self.findParentSectionIDs($prev_panel);
+
+ self.setPanelIndex(sids[1], $this_panel.parent().index());
+
+ $prev_panel
+ .removeClass('in')
+ .addClass('collapse');
+
+ $prev_teaser
+ .show()
+ .children('span:last')
+ .empty()
+ .append(self.teaser(sids[0]));
+
+ $this_teaser
+ .hide();
+
+ ev.stopPropagation();
+ },
+
+ handleSort: function(ev)
+ {
+ var self = ev.data.self;
+
+ var $item = $(this).parents('.luci2-section-item:first');
+ var $next = ev.data.up ? $item.prev() : $item.next();
+
+ if ($item.length && $next.length)
+ {
+ var cur_sid = $item.attr('data-luci2-sid');
+ var new_sid = $next.attr('data-luci2-sid');
+
+ L.uci.swap(self.ownerMap.uci_package, cur_sid, new_sid);
+
+ self.ownerMap.save();
+ self.ownerMap.redraw();
+ }
+
+ ev.stopPropagation();
+ },
+
+ getPanelIndex: function(parent_sid)
+ {
+ return (this.active_panel[parent_sid || '__top__'] || 0);
+ },
+
+ setPanelIndex: function(parent_sid, new_index)
+ {
+ if (typeof(new_index) == 'number')
+ this.active_panel[parent_sid || '__top__'] = new_index;
+ },
+
+ renderAdd: function()
+ {
+ if (!this.options.addremove)
+ return null;
+
+ var text = L.tr('Add section');
+ var ttip = L.tr('Create new section...');
+
+ if ($.isArray(this.options.add_caption))
+ text = this.options.add_caption[0], ttip = this.options.add_caption[1];
+ else if (typeof(this.options.add_caption) == 'string')
+ text = this.options.add_caption, ttip = '';
+
+ var add = $('<div />');
+
+ if (this.options.anonymous === false)
+ {
+ $('<input />')
+ .addClass('cbi-input-text')
+ .attr('type', 'text')
+ .attr('placeholder', ttip)
+ .blur({ self: this }, this.handleSID)
+ .keyup({ self: this }, this.handleSID)
+ .appendTo(add);
+
+ $('<img />')
+ .attr('src', L.globals.resource + '/icons/cbi/add.gif')
+ .attr('title', text)
+ .addClass('cbi-button')
+ .click({ self: this }, this.handleAdd)
+ .appendTo(add);
+
+ $('<div />')
+ .addClass('cbi-value-error')
+ .hide()
+ .appendTo(add);
+ }
+ else
+ {
+ L.ui.button(text, 'success', ttip)
+ .click({ self: this }, this.handleAdd)
+ .appendTo(add);
+ }
+
+ return add;
+ },
+
+ renderRemove: function(index)
+ {
+ if (!this.options.addremove)
+ return null;
+
+ var text = L.tr('Remove');
+ var ttip = L.tr('Remove this section');
+
+ if ($.isArray(this.options.remove_caption))
+ text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
+ else if (typeof(this.options.remove_caption) == 'string')
+ text = this.options.remove_caption, ttip = '';
+
+ return L.ui.button(text, 'danger', ttip)
+ .click({ self: this, index: index }, this.handleRemove);
+ },
+
+ renderSort: function(index)
+ {
+ if (!this.options.sortable)
+ return null;
+
+ var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
+ .click({ self: this, index: index, up: true }, this.handleSort);
+
+ var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
+ .click({ self: this, index: index, up: false }, this.handleSort);
+
+ return b1.add(b2);
+ },
+
+ renderCaption: function()
+ {
+ return $('<h3 />')
+ .addClass('panel-title')
+ .append(this.label('caption') || this.uci_type);
+ },
+
+ renderDescription: function()
+ {
+ var text = this.label('description');
+
+ if (text)
+ return $('<div />')
+ .addClass('luci2-section-description')
+ .text(text);
+
+ return null;
+ },
+
+ renderTeaser: function(sid, index)
+ {
+ if (this.options.collabsible || this.ownerMap.options.collabsible)
+ {
+ return $('<div />')
+ .attr('id', this.id('teaser', sid))
+ .addClass('luci2-section-teaser well well-sm')
+ .append($('<span />')
+ .addClass('badge'))
+ .append($('<span />'));
+ }
+
+ return null;
+ },
+
+ renderHead: function(condensed)
+ {
+ if (condensed)
+ return null;
+
+ return $('<div />')
+ .addClass('panel-heading')
+ .append(this.renderCaption())
+ .append(this.renderDescription());
+ },
+
+ renderTabDescription: function(sid, index, tab_index)
+ {
+ var tab = this.tabs[tab_index];
+
+ if (typeof(tab.description) == 'string')
+ {
+ return $('<div />')
+ .addClass('cbi-tab-descr')
+ .text(tab.description);
+ }
+
+ return null;
+ },
+
+ renderTabHead: function(sid, index, tab_index)
+ {
+ var tab = this.tabs[tab_index];
+ var cur = this.active_tab[sid] || 0;
+
+ var tabh = $('<li />')
+ .append($('<a />')
+ .attr('id', this.id('nodetab', sid, tab.id))
+ .attr('href', '#' + this.id('node', sid, tab.id))
+ .attr('data-toggle', 'tab')
+ .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
+ .append($('<span />')
+ .addClass('badge'))
+ .on('shown.bs.tab', { self: this, sid: sid }, this.handleTab));
+
+ if (cur == tab_index)
+ tabh.addClass('active');
+
+ if (!tab.fields.length)
+ tabh.hide();
+
+ return tabh;
+ },
+
+ renderTabBody: function(sid, index, tab_index)
+ {
+ var tab = this.tabs[tab_index];
+ var cur = this.active_tab[sid] || 0;
+
+ var tabb = $('<div />')
+ .addClass('tab-pane')
+ .attr('id', this.id('node', sid, tab.id))
+ .append(this.renderTabDescription(sid, index, tab_index))
+ .on('validate', this.handleTabValidate);
+
+ if (cur == tab_index)
+ tabb.addClass('active');
+
+ for (var i = 0; i < tab.fields.length; i++)
+ tabb.append(tab.fields[i].render(sid));
+
+ return tabb;
+ },
+
+ renderPanelHead: function(sid, index, parent_sid)
+ {
+ var head = $('<div />')
+ .addClass('luci2-section-header')
+ .append(this.renderTeaser(sid, index))
+ .append($('<div />')
+ .addClass('btn-group')
+ .append(this.renderSort(index))
+ .append(this.renderRemove(index)));
+
+ if (this.options.collabsible)
+ {
+ head.attr('data-toggle', 'collapse')
+ .attr('data-parent', this.id('sectiongroup', parent_sid))
+ .attr('data-target', '#' + this.id('panel', sid));
+ }
+
+ return head;
+ },
+
+ renderPanelBody: function(sid, index, parent_sid)
+ {
+ var body = $('<div />')
+ .attr('id', this.id('panel', sid))
+ .addClass('luci2-section-panel')
+ .on('validate', this.handlePanelValidate);
+
+ if (this.options.collabsible || this.ownerMap.options.collabsible)
+ {
+ body.addClass('panel-collapse collapse');
+
+ if (index == this.getPanelIndex(parent_sid))
+ body.addClass('in');
+ }
+
+ var tab_heads = $('<ul />')
+ .addClass('nav nav-tabs');
+
+ var tab_bodies = $('<div />')
+ .addClass('form-horizontal tab-content')
+ .append(tab_heads);
+
+ for (var j = 0; j < this.tabs.length; j++)
+ {
+ tab_heads.append(this.renderTabHead(sid, index, j));
+ tab_bodies.append(this.renderTabBody(sid, index, j));
+ }
+
+ body.append(tab_bodies);
+
+ if (this.tabs.length <= 1)
+ tab_heads.hide();
+
+ for (var i = 0; i < this.subsections.length; i++)
+ body.append(this.subsections[i].render(false, sid));
+
+ return body;
+ },
+
+ renderBody: function(condensed, parent_sid)
+ {
+ var s = this.getUCISections(parent_sid);
+ var n = this.getPanelIndex(parent_sid);
+
+ if (n < 0)
+ this.setPanelIndex(parent_sid, n + s.length);
+ else if (n >= s.length)
+ this.setPanelIndex(parent_sid, s.length - 1);
+
+ var body = $('<ul />')
+ .addClass('luci2-section-group list-group');
+
+ if (this.options.collabsible)
+ {
+ body.attr('id', this.id('sectiongroup', parent_sid))
+ .on('show.bs.collapse', { self: this }, this.handlePanelCollapse);
+ }
+
+ if (s.length == 0)
+ {
+ body.append($('<li />')
+ .addClass('list-group-item text-muted')
+ .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
+ }
+
+ for (var i = 0; i < s.length; i++)
+ {
+ var sid = s[i]['.name'];
+ var inst = this.instance[sid] = { tabs: [ ] };
+
+ body.append($('<li />')
+ .addClass('luci2-section-item list-group-item')
+ .attr('id', this.id('sectionitem', sid))
+ .attr('data-luci2-sid', sid)
+ .append(this.renderPanelHead(sid, i, parent_sid))
+ .append(this.renderPanelBody(sid, i, parent_sid)));
+ }
+
+ return body;
+ },
+
+ render: function(condensed, parent_sid)
+ {
+ this.instance = { };
+
+ var panel = $('<div />')
+ .addClass('panel panel-default')
+ .append(this.renderHead(condensed))
+ .append(this.renderBody(condensed, parent_sid));
+
+ if (this.options.addremove)
+ panel.append($('<div />')
+ .addClass('panel-footer')
+ .append(this.renderAdd()));
+
+ return panel;
+ },
+
+ finish: function(parent_sid)
+ {
+ var s = this.getUCISections(parent_sid);
+
+ for (var i = 0; i < s.length; i++)
+ {
+ var sid = s[i]['.name'];
+
+ if (i != this.getPanelIndex(parent_sid))
+ $('#' + this.id('teaser', sid)).children('span:last')
+ .append(this.teaser(sid));
+ else
+ $('#' + this.id('teaser', sid))
+ .hide();
+
+ for (var j = 0; j < this.subsections.length; j++)
+ this.subsections[j].finish(sid);
+ }
+ }
+ });
+
+ cbi_class.TableSection = cbi_class.TypedSection.extend({
+ renderTableHead: function()
+ {
+ var thead = $('<thead />')
+ .append($('<tr />')
+ .addClass('cbi-section-table-titles'));
+
+ for (var j = 0; j < this.tabs[0].fields.length; j++)
+ thead.children().append($('<th />')
+ .addClass('cbi-section-table-cell')
+ .css('width', this.tabs[0].fields[j].options.width || '')
+ .append(this.tabs[0].fields[j].label('caption')));
+
+ if (this.options.addremove !== false || this.options.sortable)
+ thead.children().append($('<th />')
+ .addClass('cbi-section-table-cell')
+ .text(' '));
+
+ return thead;
+ },
+
+ renderTableRow: function(sid, index)
+ {
+ var row = $('<tr />')
+ .addClass('luci2-section-item')
+ .attr('id', this.id('sectionitem', sid))
+ .attr('data-luci2-sid', sid);
+
+ for (var j = 0; j < this.tabs[0].fields.length; j++)
+ {
+ row.append($('<td />')
+ .css('width', this.tabs[0].fields[j].options.width || '')
+ .append(this.tabs[0].fields[j].render(sid, true)));
+ }
+
+ if (this.options.addremove !== false || this.options.sortable)
+ {
+ row.append($('<td />')
+ .css('width', '1%')
+ .addClass('text-right')
+ .append($('<div />')
+ .addClass('btn-group')
+ .append(this.renderSort(index))
+ .append(this.renderRemove(index))));
+ }
+
+ return row;
+ },
+
+ renderTableBody: function(parent_sid)
+ {
+ var s = this.getUCISections(parent_sid);
+
+ var tbody = $('<tbody />');
+
+ if (s.length == 0)
+ {
+ var cols = this.tabs[0].fields.length;
+
+ if (this.options.addremove !== false || this.options.sortable)
+ cols++;
+
+ tbody.append($('<tr />')
+ .append($('<td />')
+ .addClass('text-muted')
+ .attr('colspan', cols)
+ .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
+ }
+
+ for (var i = 0; i < s.length; i++)
+ {
+ var sid = s[i]['.name'];
+ var inst = this.instance[sid] = { tabs: [ ] };
+
+ tbody.append(this.renderTableRow(sid, i));
+ }
+
+ return tbody;
+ },
+
+ renderBody: function(condensed, parent_sid)
+ {
+ return $('<table />')
+ .addClass('table table-condensed table-hover')
+ .append(this.renderTableHead())
+ .append(this.renderTableBody(parent_sid));
+ }
+ });
+
+ cbi_class.NamedSection = cbi_class.TypedSection.extend({
+ getUCISections: function(cb)
+ {
+ var sa = [ ];
+ var sl = L.uci.sections(this.ownerMap.uci_package);
+
+ for (var i = 0; i < sl.length; i++)
+ if (sl[i]['.name'] == this.uci_type)
+ {
+ sa.push(sl[i]);
+ break;
+ }
+
+ if (typeof(cb) == 'function' && sa.length > 0)
+ cb.call(this, sa[0]);
+
+ return sa;
+ }
+ });
+
+ cbi_class.SingleSection = cbi_class.NamedSection.extend({
+ render: function()
+ {
+ this.instance = { };
+ this.instance[this.uci_type] = { tabs: [ ] };
+
+ return $('<div />')
+ .addClass('luci2-section-item')
+ .attr('id', this.id('sectionitem', this.uci_type))
+ .attr('data-luci2-sid', this.uci_type)
+ .append(this.renderPanelBody(this.uci_type, 0));
+ }
+ });
+
+ cbi_class.DummySection = cbi_class.TypedSection.extend({
+ getUCISections: function(cb)
+ {
+ if (typeof(cb) == 'function')
+ cb.apply(this, [ { '.name': this.uci_type } ]);
+
+ return [ { '.name': this.uci_type } ];
+ }
+ });
+
+ cbi_class.Map = L.ui.AbstractWidget.extend({
+ init: function(uci_package, options)
+ {
+ var self = this;
+
+ this.uci_package = uci_package;
+ this.sections = [ ];
+ this.options = L.defaults(options, {
+ save: function() { },
+ prepare: function() { }
+ });
+ },
+
+ loadCallback: function()
+ {
+ var deferreds = [ L.deferrable(this.options.prepare.call(this)) ];
+
+ for (var i = 0; i < this.sections.length; i++)
+ {
+ var rv = this.sections[i].load();
+ deferreds.push.apply(deferreds, rv);
+ }
+
+ return $.when.apply($, deferreds);
+ },
+
+ load: function()
+ {
+ var self = this;
+ var packages = [ this.uci_package ];
+
+ for (var i = 0; i < this.sections.length; i++)
+ packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
+
+ for (var i = 0; i < packages.length; i++)
+ if (!L.uci.writable(packages[i]))
+ {
+ this.options.readonly = true;
+ break;
+ }
+
+ return L.uci.load(packages).then(function() {
+ return self.loadCallback();
+ });
+ },
+
+ handleTab: function(ev)
+ {
+ ev.data.self.active_tab = $(ev.target).parent().index();
+ },
+
+ handleApply: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.trigger('apply', ev);
+ },
+
+ handleSave: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.send().then(function() {
+ self.trigger('save', ev);
+ });
+ },
+
+ handleReset: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.trigger('reset', ev);
+ self.reset();
+ },
+
+ renderTabHead: function(tab_index)
+ {
+ var section = this.sections[tab_index];
+ var cur = this.active_tab || 0;
+
+ var tabh = $('<li />')
+ .append($('<a />')
+ .attr('id', section.id('sectiontab'))
+ .attr('href', '#' + section.id('section'))
+ .attr('data-toggle', 'tab')
+ .text(section.label('caption') + ' ')
+ .append($('<span />')
+ .addClass('badge'))
+ .on('shown.bs.tab', { self: this }, this.handleTab));
+
+ if (cur == tab_index)
+ tabh.addClass('active');
+
+ return tabh;
+ },
+
+ renderTabBody: function(tab_index)
+ {
+ var section = this.sections[tab_index];
+ var desc = section.label('description');
+ var cur = this.active_tab || 0;
+
+ var tabb = $('<div />')
+ .addClass('tab-pane')
+ .attr('id', section.id('section'));
+
+ if (cur == tab_index)
+ tabb.addClass('active');
+
+ if (desc)
+ tabb.append($('<p />')
+ .text(desc));
+
+ var s = section.render(this.options.tabbed);
+
+ if (this.options.readonly || section.options.readonly)
+ s.find('input, select, button, img.cbi-button').attr('disabled', true);
+
+ tabb.append(s);
+
+ return tabb;
+ },
+
+ renderBody: function()
+ {
+ var tabs = $('<ul />')
+ .addClass('nav nav-tabs');
+
+ var body = $('<div />')
+ .append(tabs);
+
+ for (var i = 0; i < this.sections.length; i++)
+ {
+ tabs.append(this.renderTabHead(i));
+ body.append(this.renderTabBody(i));
+ }
+
+ if (this.options.tabbed)
+ body.addClass('tab-content');
+ else
+ tabs.hide();
+
+ return body;
+ },
+
+ renderFooter: function()
+ {
+ var evdata = {
+ self: this
+ };
+
+ return $('<div />')
+ .addClass('panel panel-default panel-body text-right')
+ .append($('<div />')
+ .addClass('btn-group')
+ .append(L.ui.button(L.tr('Save & Apply'), 'primary')
+ .click(evdata, this.handleApply))
+ .append(L.ui.button(L.tr('Save'), 'default')
+ .click(evdata, this.handleSave))
+ .append(L.ui.button(L.tr('Reset'), 'default')
+ .click(evdata, this.handleReset)));
+ },
+
+ render: function()
+ {
+ var map = $('<form />');
+
+ if (typeof(this.options.caption) == 'string')
+ map.append($('<h2 />')
+ .text(this.options.caption));
+
+ if (typeof(this.options.description) == 'string')
+ map.append($('<p />')
+ .text(this.options.description));
+
+ map.append(this.renderBody());
+
+ if (this.options.pageaction !== false)
+ map.append(this.renderFooter());
+
+ return map;
+ },
+
+ finish: function()
+ {
+ for (var i = 0; i < this.sections.length; i++)
+ this.sections[i].finish();
+
+ this.validate();
+ },
+
+ redraw: function()
+ {
+ this.target.hide().empty().append(this.render());
+ this.finish();
+ this.target.show();
+ },
+
+ section: function(widget, uci_type, options)
+ {
+ var w = widget ? new widget(uci_type, options) : null;
+
+ if (!(w instanceof L.cbi.AbstractSection))
+ throw 'Widget must be an instance of AbstractSection';
+
+ w.ownerMap = this;
+ w.index = this.sections.length;
+
+ this.sections.push(w);
+ return w;
+ },
+
+ add: function(conf, type, name)
+ {
+ return L.uci.add(conf, type, name);
+ },
+
+ remove: function(conf, sid)
+ {
+ return L.uci.remove(conf, sid);
+ },
+
+ get: function(conf, sid, opt)
+ {
+ return L.uci.get(conf, sid, opt);
+ },
+
+ set: function(conf, sid, opt, val)
+ {
+ return L.uci.set(conf, sid, opt, val);
+ },
+
+ validate: function()
+ {
+ var rv = true;
+
+ for (var i = 0; i < this.sections.length; i++)
+ {
+ if (!this.sections[i].validate())
+ rv = false;
+ }
+
+ return rv;
+ },
+
+ save: function()
+ {
+ var self = this;
+
+ if (self.options.readonly)
+ return L.deferrable();
+
+ var deferreds = [ ];
+
+ for (var i = 0; i < self.sections.length; i++)
+ {
+ var rv = self.sections[i].save();
+ deferreds.push.apply(deferreds, rv);
+ }
+
+ return $.when.apply($, deferreds).then(function() {
+ return L.deferrable(self.options.save.call(self));
+ });
+ },
+
+ send: function()
+ {
+ if (!this.validate())
+ return L.deferrable();
+
+ var self = this;
+
+ L.ui.saveScrollTop();
+ L.ui.loading(true);
+
+ return this.save().then(function() {
+ return L.uci.save();
+ }).then(function() {
+ return L.ui.updateChanges();
+ }).then(function() {
+ return self.load();
+ }).then(function() {
+ self.redraw();
+ self = null;
+
+ L.ui.loading(false);
+ L.ui.restoreScrollTop();
+ });
+ },
+
+ revert: function()
+ {
+ var packages = [ this.uci_package ];
+
+ for (var i = 0; i < this.sections.length; i++)
+ packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
+
+ L.uci.unload(packages);
+ },
+
+ reset: function()
+ {
+ var self = this;
+
+ self.revert();
+
+ return self.insertInto(self.target);
+ },
+
+ insertInto: function(id)
+ {
+ var self = this;
+ self.target = $(id);
+
+ L.ui.loading(true);
+ self.target.hide();
+
+ return self.load().then(function() {
+ self.target.empty().append(self.render());
+ self.finish();
+ self.target.show();
+ self = null;
+ L.ui.loading(false);
+ });
+ }
+ });
+
+ cbi_class.Modal = cbi_class.Map.extend({
+ handleApply: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.trigger('apply', ev);
+ },
+
+ handleSave: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.send().then(function() {
+ self.trigger('save', ev);
+ self.close();
+ });
+ },
+
+ handleReset: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.trigger('close', ev);
+ self.revert();
+ self.close();
+ },
+
+ renderFooter: function()
+ {
+ var evdata = {
+ self: this
+ };
+
+ return $('<div />')
+ .addClass('btn-group')
+ .append(L.ui.button(L.tr('Save & Apply'), 'primary')
+ .click(evdata, this.handleApply))
+ .append(L.ui.button(L.tr('Save'), 'default')
+ .click(evdata, this.handleSave))
+ .append(L.ui.button(L.tr('Cancel'), 'default')
+ .click(evdata, this.handleReset));
+ },
+
+ render: function()
+ {
+ var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
+ var map = $('<form />');
+
+ var desc = this.label('description');
+ if (desc)
+ map.append($('<p />').text(desc));
+
+ map.append(this.renderBody());
+
+ modal.find('.modal-body').append(map);
+ modal.find('.modal-footer').append(this.renderFooter());
+
+ return modal;
+ },
+
+ redraw: function()
+ {
+ this.render();
+ this.finish();
+ },
+
+ show: function()
+ {
+ var self = this;
+
+ L.ui.loading(true);
+
+ return self.load().then(function() {
+ self.render();
+ self.finish();
+
+ L.ui.loading(false);
+ });
+ },
+
+ close: function()
+ {
+ L.ui.dialog(false);
+ }
+ });
+
+ return Class.extend(cbi_class);
+})();