luci2: make sort / remove column in L.cbi.TableSection as narrow as possible
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
index 7c09901..7ade584 100644 (file)
@@ -485,6 +485,152 @@ function LuCI2()
                return n;
        };
 
+       this.toColor = function(str)
+       {
+               if (typeof(str) != 'string' || str.length == 0)
+                       return '#CCCCCC';
+
+               if (str == 'wan')
+                       return '#F09090';
+               else if (str == 'lan')
+                       return '#90F090';
+
+               var i = 0, hash = 0;
+
+               while (i < str.length)
+                       hash = str.charCodeAt(i++) + ((hash << 5) - hash);
+
+               var r = (hash & 0xFF) % 128;
+               var g = ((hash >> 8) & 0xFF) % 128;
+
+               var min = 0;
+               var max = 128;
+
+               if ((r + g) < 128)
+                       min = 128 - r - g;
+               else
+                       max = 255 - r - g;
+
+               var b = min + (((hash >> 16) & 0xFF) % (max - min));
+
+               return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b);
+       };
+
+       this.parseIPv4 = function(str)
+       {
+               if ((typeof(str) != 'string' && !(str instanceof String)) ||
+                   !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
+                       return undefined;
+
+               var num = [ ];
+               var parts = str.split(/\./);
+
+               for (var i = 0; i < parts.length; i++)
+               {
+                       var n = parseInt(parts[i], 10);
+                       if (isNaN(n) || n > 255)
+                               return undefined;
+
+                       num.push(n);
+               }
+
+               return num;
+       };
+
+       this.parseIPv6 = function(str)
+       {
+               if ((typeof(str) != 'string' && !(str instanceof String)) ||
+                   !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/))
+                       return undefined;
+
+               var parts = str.split(/::/);
+               if (parts.length == 0 || parts.length > 2)
+                       return undefined;
+
+               var lnum = [ ];
+               if (parts[0].length > 0)
+               {
+                       var left = parts[0].split(/:/);
+                       for (var i = 0; i < left.length; i++)
+                       {
+                               var n = parseInt(left[i], 16);
+                               if (isNaN(n))
+                                       return undefined;
+
+                               lnum.push((n / 256) >> 0);
+                               lnum.push(n % 256);
+                       }
+               }
+
+               var rnum = [ ];
+               if (parts.length > 1 && parts[1].length > 0)
+               {
+                       var right = parts[1].split(/:/);
+
+                       for (var i = 0; i < right.length; i++)
+                       {
+                               if (right[i].indexOf('.') > 0)
+                               {
+                                       var addr = L.parseIPv4(right[i]);
+                                       if (!addr)
+                                               return undefined;
+
+                                       rnum.push.apply(rnum, addr);
+                                       continue;
+                               }
+
+                               var n = parseInt(right[i], 16);
+                               if (isNaN(n))
+                                       return undefined;
+
+                               rnum.push((n / 256) >> 0);
+                               rnum.push(n % 256);
+                       }
+               }
+
+               if (rnum.length > 0 && (lnum.length + rnum.length) > 15)
+                       return undefined;
+
+               var num = [ ];
+
+               num.push.apply(num, lnum);
+
+               for (var i = 0; i < (16 - lnum.length - rnum.length); i++)
+                       num.push(0);
+
+               num.push.apply(num, rnum);
+
+               if (num.length > 16)
+                       return undefined;
+
+               return num;
+       };
+
+       this.isNetmask = function(addr)
+       {
+               if (!$.isArray(addr))
+                       return false;
+
+               var c;
+
+               for (c = 0; (c < addr.length) && (addr[c] == 255); c++);
+
+               if (c == addr.length)
+                       return true;
+
+               if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) ||
+                       (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) ||
+                       (addr[c] == 128) || (addr[c] == 0))
+               {
+                       for (c++; (c < addr.length) && (addr[c] == 0); c++);
+
+                       if (c == addr.length)
+                               return true;
+               }
+
+               return false;
+       };
+
        this.globals = {
                timeout:  15000,
                resource: '/luci2',
@@ -963,6 +1109,35 @@ function LuCI2()
                        return this.set(conf, sid, opt, undefined);
                },
 
+               get_first: function(conf, type, opt)
+               {
+                       var sid = undefined;
+
+                       L.uci.sections(conf, type, function(s) {
+                               if (typeof(sid) != 'string')
+                                       sid = s['.name'];
+                       });
+
+                       return this.get(conf, sid, opt);
+               },
+
+               set_first: function(conf, type, opt, val)
+               {
+                       var sid = undefined;
+
+                       L.uci.sections(conf, type, function(s) {
+                               if (typeof(sid) != 'string')
+                                       sid = s['.name'];
+                       });
+
+                       return this.set(conf, sid, opt, val);
+               },
+
+               unset_first: function(conf, type, opt)
+               {
+                       return this.set_first(conf, type, opt, undefined);
+               },
+
                _reload: function()
                {
                        var pkgs = [ ];
@@ -2562,6 +2737,30 @@ function LuCI2()
                        return dev.getTrafficHistory();
                },
 
+               renderBadge: function()
+               {
+                       var badge = $('<span />')
+                               .addClass('badge')
+                               .text('%s: '.format(this.name()));
+
+                       var dev = this.getDevice();
+                       var subdevs = this.getSubdevices();
+
+                       if (subdevs.length)
+                               for (var j = 0; j < subdevs.length; j++)
+                                       badge.append($('<img />')
+                                               .attr('src', subdevs[j].icon())
+                                               .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?')));
+                       else if (dev)
+                               badge.append($('<img />')
+                                       .attr('src', dev.icon())
+                                       .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?')));
+                       else
+                               badge.append($('<em />').text(L.tr('(No devices attached)')));
+
+                       return badge;
+               },
+
                setDevices: function(devs)
                {
                        var dev = this.getPhysdev();
@@ -4412,8 +4611,7 @@ function LuCI2()
 
                'ipaddr': function()
                {
-                       if (validation.types['ip4addr'].apply(this) ||
-                               validation.types['ip6addr'].apply(this))
+                       if (L.parseIPv4(this) || L.parseIPv6(this))
                                return true;
 
                        validation.i18n('Must be a valid IP address');
@@ -4422,17 +4620,8 @@ function LuCI2()
 
                'ip4addr': function()
                {
-                       if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
-                       {
-                               if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
-                                   (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
-                                   (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
-                                   (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
-                                   ((RegExp.$6.indexOf('.') < 0)
-                                     ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
-                                     : (validation.types['ip4addr'].apply(RegExp.$6))))
-                                       return true;
-                       }
+                       if (L.parseIPv4(this))
+                               return true;
 
                        validation.i18n('Must be a valid IPv4 address');
                        return false;
@@ -4440,62 +4629,74 @@ function LuCI2()
 
                'ip6addr': function()
                {
-                       if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
-                       {
-                               if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
-                               {
-                                       var addr = RegExp.$1;
+                       if (L.parseIPv6(this))
+                               return true;
 
-                                       if (addr == '::')
-                                       {
-                                               return true;
-                                       }
+                       validation.i18n('Must be a valid IPv6 address');
+                       return false;
+               },
 
-                                       if (addr.indexOf('.') > 0)
-                                       {
-                                               var off = addr.lastIndexOf(':');
+               'netmask4': function()
+               {
+                       if (L.isNetmask(L.parseIPv4(this)))
+                               return true;
 
-                                               if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
-                                               {
-                                                       validation.i18n('Must be a valid IPv6 address');
-                                                       return false;
-                                               }
+                       validation.i18n('Must be a valid IPv4 netmask');
+                       return false;
+               },
 
-                                               addr = addr.substr(0, off) + ':0:0';
-                                       }
+               'netmask6': function()
+               {
+                       if (L.isNetmask(L.parseIPv6(this)))
+                               return true;
 
-                                       if (addr.indexOf('::') >= 0)
-                                       {
-                                               var colons = 0;
-                                               var fill = '0';
+                       validation.i18n('Must be a valid IPv6 netmask6');
+                       return false;
+               },
 
-                                               for (var i = 1; i < (addr.length-1); i++)
-                                                       if (addr.charAt(i) == ':')
-                                                               colons++;
+               'cidr4': function()
+               {
+                       if (this.match(/^([0-9.]+)\/(\d{1,2})$/))
+                               if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1))
+                                       return true;
 
-                                               if (colons > 7)
-                                               {
-                                                       validation.i18n('Must be a valid IPv6 address');
-                                                       return false;
-                                               }
+                       validation.i18n('Must be a valid IPv4 prefix');
+                       return false;
+               },
 
-                                               for (var i = 0; i < (7 - colons); i++)
-                                                       fill += ':0';
+               'cidr6': function()
+               {
+                       if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/))
+                               if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1))
+                                       return true;
 
-                                               if (addr.match(/^(.*?)::(.*?)$/))
-                                                       addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
-                                                                  (RegExp.$2 ? ':' + RegExp.$2 : '');
-                                       }
+                       validation.i18n('Must be a valid IPv6 prefix');
+                       return false;
+               },
 
-                                       if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
-                                               return true;
+               '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 IPv6 address');
-                                       return false;
-                               }
+                       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');
+                       validation.i18n('Must be a valid IPv6 address/netmask pair');
                        return false;
                },
 
@@ -4898,7 +5099,7 @@ function LuCI2()
                        if (typeof(a) != typeof(b))
                                return true;
 
-                       if (typeof(a) == 'object')
+                       if ($.isArray(a))
                        {
                                if (a.length != b.length)
                                        return true;
@@ -4909,6 +5110,18 @@ function LuCI2()
 
                                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);
                },
@@ -5776,7 +5989,7 @@ function LuCI2()
                        return $('<div />')
                                .addClass('form-control-static')
                                .attr('id', this.id(sid))
-                               .html(this.ucivalue(sid));
+                               .html(this.ucivalue(sid) || this.label('placeholder'));
                },
 
                formvalue: function(sid)
@@ -5835,30 +6048,16 @@ function LuCI2()
                        for (var i = 0; i < interfaces.length; i++)
                        {
                                var iface = interfaces[i];
-                               var badge = $('<span />')
-                                       .addClass('badge')
-                                       .text('%s: '.format(iface.name()));
-
-                               var dev = iface.getDevice();
-                               var subdevs = iface.getSubdevices();
-
-                               if (subdevs.length)
-                                       for (var j = 0; j < subdevs.length; j++)
-                                               badge.append(this._device_icon(subdevs[j]));
-                               else if (dev)
-                                       badge.append(this._device_icon(dev));
-                               else
-                                       badge.append($('<em />').text(L.tr('(No devices attached)')));
 
                                $('<li />')
                                        .append($('<label />')
                                                .addClass(itype + ' inline')
-                                               .append($('<input />')
+                                               .append(this.validator(sid, $('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', iface.name())
-                                                       .prop('checked', !!check[iface.name()]))
-                                               .append(badge))
+                                                       .prop('checked', !!check[iface.name()]), true))
+                                               .append(iface.renderBadge()))
                                        .appendTo(ul);
                        }
 
@@ -5867,11 +6066,11 @@ function LuCI2()
                                $('<li />')
                                        .append($('<label />')
                                                .addClass(itype + ' inline text-muted')
-                                               .append($('<input />')
+                                               .append(this.validator(sid, $('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', '')
-                                                       .prop('checked', $.isEmptyObject(check)))
+                                                       .prop('checked', $.isEmptyObject(check)), true))
                                                .append(L.tr('unspecified')))
                                        .appendTo(ul);
                        }
@@ -6283,6 +6482,11 @@ function LuCI2()
                        return true;
                },
 
+               sort: function(section1, section2)
+               {
+                       return 0;
+               },
+
                sections: function(cb)
                {
                        var s1 = L.uci.sections(this.map.uci_package);
@@ -6293,6 +6497,8 @@ function LuCI2()
                                        if (this.filter(s1[i]))
                                                s2.push(s1[i]);
 
+                       s2.sort(this.sort);
+
                        if (typeof(cb) == 'function')
                                for (var i = 0; i < s2.length; i++)
                                        cb.call(this, s2[i]);
@@ -6847,6 +7053,7 @@ function LuCI2()
                        if (this.options.addremove !== false || this.options.sortable)
                        {
                                row.append($('<td />')
+                                       .css('width', '1%')
                                        .addClass('text-right')
                                        .append($('<div />')
                                                .addClass('btn-group')
@@ -7012,7 +7219,9 @@ function LuCI2()
                {
                        var self = ev.data.self;
 
-                       self.trigger('save', ev);
+                       self.send().then(function() {
+                               self.trigger('save', ev);
+                       });
                },
 
                _ev_reset: function(ev)
@@ -7271,9 +7480,8 @@ function LuCI2()
                        });
                },
 
-               reset: function()
+               revert: function()
                {
-                       var self = this;
                        var packages = { };
 
                        for (var i = 0; i < this.sections.length; i++)
@@ -7282,6 +7490,13 @@ function LuCI2()
                        packages[this.uci_package] = true;
 
                        L.uci.unload(L.toArray(packages));
+               },
+
+               reset: function()
+               {
+                       var self = this;
+
+                       self.revert();
 
                        return self.insertInto(self.target);
                },
@@ -7327,6 +7542,7 @@ function LuCI2()
                        var self = ev.data.self;
 
                        self.trigger('close', ev);
+                       self.revert();
                        self.close();
                },