luci-base: cbi: further refactoring
authorJo-Philipp Wich <jow@openwrt.org>
Wed, 10 Feb 2016 17:11:49 +0000 (18:11 +0100)
committerJo-Philipp Wich <jow@openwrt.org>
Wed, 10 Feb 2016 17:11:49 +0000 (18:11 +0100)
Eliminate more inline scripts in favor to global initialization, use a global
object for sharing fixed strings instead of passing them to each invocation.

Signed-off-by: Jo-Philipp Wich <jow@openwrt.org>
modules/luci-base/htdocs/luci-static/resources/cbi.js
modules/luci-base/luasrc/view/cbi/dynlist.htm
modules/luci-base/luasrc/view/cbi/header.htm
modules/luci-base/luasrc/view/cbi/tblsection.htm
modules/luci-base/luasrc/view/cbi/tsection.htm
modules/luci-base/luasrc/view/cbi/ucisection.htm
modules/luci-base/luasrc/view/cbi/value.htm

index a8d323d6a8fcb8831df1e425c44fa96dc3d2f617..b27c564922cfd6f4b3dd0ca73d31422053b332db 100644 (file)
 
 var cbi_d = [];
 var cbi_t = [];
+var cbi_strings = { path: {}, label: {} };
+
+function Int(x) {
+       return (/^-?\d+$/.test(x) ? +x : NaN);
+}
+
+function Dec(x) {
+       return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
+}
 
 var cbi_validators = {
 
        'integer': function()
        {
-               return (this.match(/^-?[0-9]+$/) != null);
+               return !!Int(this);
        },
 
        'uinteger': function()
        {
-               return (cbi_validators.integer.apply(this) && (this >= 0));
+               return (Int(this) >= 0);
        },
 
        'float': function()
        {
-               return !isNaN(parseFloat(this));
+               return !!Dec(this);
        },
 
        'ufloat': function()
        {
-               return (cbi_validators['float'].apply(this) && (this >= 0));
+               return (Dec(this) >= 0);
        },
 
        'ipaddr': function()
@@ -111,26 +120,20 @@ var cbi_validators = {
 
        'port': function()
        {
-               return cbi_validators.integer.apply(this) &&
-                       (this >= 0) && (this <= 65535);
+               var p = Int(this);
+               return (p >= 0 && p <= 65535);
        },
 
        'portrange': function()
        {
                if (this.match(/^(\d+)-(\d+)$/))
                {
-                       var p1 = RegExp.$1;
-                       var p2 = RegExp.$2;
-
-                       return cbi_validators.port.apply(p1) &&
-                              cbi_validators.port.apply(p2) &&
-                              (parseInt(p1) <= parseInt(p2))
-                       ;
-               }
-               else
-               {
-                       return cbi_validators.port.apply(this);
+                       var p1 = +RegExp.$1;
+                       var p2 = +RegExp.$2;
+                       return (p1 <= p2 && p2 <= 65535);
                }
+
+               return cbi_validators.port.apply(this);
        },
 
        'macaddr': function()
@@ -234,56 +237,34 @@ var cbi_validators = {
 
        'range': function(min, max)
        {
-               var val = parseFloat(this);
-               if (!isNaN(min) && !isNaN(max) && !isNaN(val))
-                       return ((val >= min) && (val <= max));
-
-               return false;
+               var val = Dec(this);
+               return (val >= +min && val <= +max);
        },
 
        'min': function(min)
        {
-               var val = parseFloat(this);
-               if (!isNaN(min) && !isNaN(val))
-                       return (val >= min);
-
-               return false;
+               return (Dec(this) >= +min);
        },
 
        'max': function(max)
        {
-               var val = parseFloat(this);
-               if (!isNaN(max) && !isNaN(val))
-                       return (val <= max);
-
-               return false;
+               return (Dec(this) <= +max);
        },
 
        'rangelength': function(min, max)
        {
                var val = '' + this;
-               if (!isNaN(min) && !isNaN(max))
-                       return ((val.length >= min) && (val.length <= max));
-
-               return false;
+               return ((val.length >= +min) && (val.length <= +max));
        },
 
        'minlength': function(min)
        {
-               var val = '' + this;
-               if (!isNaN(min))
-                       return (val.length >= min);
-
-               return false;
+               return ((''+this).length >= +min);
        },
 
        'maxlength': function(max)
        {
-               var val = '' + this;
-               if (!isNaN(max))
-                       return (val.length <= max);
-
-               return false;
+               return ((''+this).length <= +max);
        },
 
        'or': function()
@@ -507,7 +488,21 @@ function cbi_d_update() {
 }
 
 function cbi_init() {
-       var nodes = document.querySelectorAll('[data-depends]');
+       var nodes;
+
+       nodes = document.querySelectorAll('[data-strings]');
+
+       for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+               var str = JSON.parse(node.getAttribute('data-strings'));
+               for (var key in str) {
+                       for (var key2 in str[key]) {
+                               var dst = cbi_strings[key] || (cbi_strings[key] = { });
+                                   dst[key2] = str[key][key2];
+                       }
+               }
+       }
+
+       nodes = document.querySelectorAll('[data-depends]');
 
        for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
                var index = parseInt(node.getAttribute('data-index'), 10);
@@ -528,6 +523,41 @@ function cbi_init() {
                }
        }
 
+       nodes = document.querySelectorAll('[data-type]');
+
+       for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+               cbi_validate_field(node, node.getAttribute('data-optional') === 'true',
+                                  node.getAttribute('data-type'));
+       }
+
+       nodes = document.querySelectorAll('[data-choices]');
+
+       for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+               var choices = JSON.parse(node.getAttribute('data-choices'));
+               var options = {};
+
+               for (var j = 0; j < choices[0].length; j++)
+                       options[choices[0][j]] = choices[1][j];
+
+               var def = (node.getAttribute('data-optional') === 'true')
+                       ? node.placeholder || '' : null;
+
+               cbi_combobox_init(node, options, def,
+                                 node.getAttribute('data-manual'));
+       }
+
+       nodes = document.querySelectorAll('[data-dynlist]');
+
+       for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+               var choices = JSON.parse(node.getAttribute('data-dynlist'));
+               var options = {};
+
+               for (var j = 0; j < choices[0].length; j++)
+                       options[choices[0][j]] = choices[1][j];
+
+               cbi_dynlist_init(node, choices[2], choices[3], options);
+       }
+
        cbi_d_update();
 }
 
@@ -576,7 +606,7 @@ function cbi_combobox(id, values, def, man, focus) {
                if (obj.value == "") {
                        var optdef = document.createElement("option");
                        optdef.value = "";
-                       optdef.appendChild(document.createTextNode(def));
+                       optdef.appendChild(document.createTextNode(typeof(def) === 'string' ? def : cbi_strings.label.choose));
                        sel.appendChild(optdef);
                } else {
                        var opt = document.createElement("option");
@@ -601,7 +631,7 @@ function cbi_combobox(id, values, def, man, focus) {
 
        var optman = document.createElement("option");
        optman.value = "";
-       optman.appendChild(document.createTextNode(man));
+       optman.appendChild(document.createTextNode(typeof(man) === 'string' ? man : cbi_strings.label.custom));
        sel.appendChild(optman);
 
        obj.style.display = "none";
@@ -631,27 +661,27 @@ function cbi_combobox(id, values, def, man, focus) {
 }
 
 function cbi_combobox_init(id, values, def, man) {
-       var obj = document.getElementById(id);
+       var obj = (typeof(id) === 'string') ? document.getElementById(id) : id;
        cbi_bind(obj, "blur", function() {
-               cbi_combobox(id, values, def, man, true);
+               cbi_combobox(obj.id, values, def, man, true);
        });
-       cbi_combobox(id, values, def, man, false);
+       cbi_combobox(obj.id, values, def, man, false);
 }
 
-function cbi_filebrowser(id, url, defpath) {
+function cbi_filebrowser(id, defpath) {
        var field   = document.getElementById(id);
        var browser = window.open(
-               url + ( field.value || defpath || '' ) + '?field=' + id,
+               cbi_strings.path.browser + ( field.value || defpath || '' ) + '?field=' + id,
                "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes"
        );
 
        browser.focus();
 }
 
-function cbi_browser_init(id, respath, url, defpath)
+function cbi_browser_init(id, defpath)
 {
        function cbi_browser_btnclick(e) {
-               cbi_filebrowser(id, url, defpath);
+               cbi_filebrowser(id, defpath);
                return false;
        }
 
@@ -659,18 +689,16 @@ function cbi_browser_init(id, respath, url, defpath)
 
        var btn = document.createElement('img');
        btn.className = 'cbi-image-button';
-       btn.src = respath + '/cbi/folder.gif';
+       btn.src = cbi_strings.path.resource + '/cbi/folder.gif';
        field.parentNode.insertBefore(btn, field.nextSibling);
 
        cbi_bind(btn, 'click', cbi_browser_btnclick);
 }
 
-function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choices)
+function cbi_dynlist_init(parent, datatype, optional, choices)
 {
-       var input0 = document.getElementsByName(name)[0];
-       var prefix = input0.name;
-       var parent = input0.parentNode;
-       var holder = input0.placeholder;
+       var prefix = parent.getAttribute('data-prefix');
+       var holder = parent.getAttribute('data-placeholder');
 
        var values;
 
@@ -681,7 +709,7 @@ function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choic
                while (parent.firstChild)
                {
                        var n = parent.firstChild;
-                       var i = parseInt(n.index);
+                       var i = +n.index;
 
                        if (i != del)
                        {
@@ -721,14 +749,14 @@ function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choic
                        }
 
                        var b = document.createElement('img');
-                               b.src = respath + ((i+1) < values.length ? '/cbi/remove.gif' : '/cbi/add.gif');
+                               b.src = cbi_strings.path.resource + ((i+1) < values.length ? '/cbi/remove.gif' : '/cbi/add.gif');
                                b.className = 'cbi-image-button';
 
                        parent.appendChild(t);
                        parent.appendChild(b);
                        if (datatype == 'file')
                        {
-                               cbi_browser_init(t.id, respath, url, defpath);
+                               cbi_browser_init(t.id, parent.getAttribute('data-browser-path'));
                        }
 
                        parent.appendChild(document.createElement('br'));
@@ -740,7 +768,7 @@ function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choic
 
                        if (choices)
                        {
-                               cbi_combobox_init(t.id, choices[0], '', choices[1]);
+                               cbi_combobox_init(t.id, choices, '', cbi_strings.label.custom);
                                b.index = i;
 
                                cbi_bind(b, 'keydown',  cbi_dynlist_keydown);
@@ -820,15 +848,15 @@ function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choic
                        se = se.parentNode;
 
                var prev = se.previousSibling;
-               while (prev && prev.name != name)
+               while (prev && prev.name != prefix)
                        prev = prev.previousSibling;
 
                var next = se.nextSibling;
-               while (next && next.name != name)
+               while (next && next.name != prefix)
                        next = next.nextSibling;
 
                /* advance one further in combobox case */
-               if (next && next.nextSibling.name == name)
+               if (next && next.nextSibling.name == prefix)
                        next = next.nextSibling;
 
                switch (ev.keyCode)
@@ -884,7 +912,7 @@ function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choic
 
                var se = ev.target ? ev.target : ev.srcElement;
                var input = se.previousSibling;
-               while (input && input.name != name) {
+               while (input && input.name != prefix) {
                        input = input.previousSibling;
                }
 
@@ -893,14 +921,14 @@ function cbi_dynlist_init(name, respath, datatype, optional, url, defpath, choic
                        input.value = '';
 
                        cbi_dynlist_keydown({
-                               target:  input,
+                               target:  se,
                                keyCode: 8
                        });
                }
                else
                {
                        cbi_dynlist_keydown({
-                               target:  input,
+                               target:  se,
                                keyCode: 13
                        });
                }
@@ -1307,40 +1335,40 @@ String.prototype.format = function()
 
                                var minLength = -1;
                                if (pMinLength)
-                                       minLength = parseInt(pMinLength);
+                                       minLength = +pMinLength;
 
                                var precision = -1;
                                if (pPrecision && pType == 'f')
-                                       precision = parseInt(pPrecision.substring(1));
+                                       precision = +pPrecision.substring(1);
 
                                var subst = param;
 
                                switch(pType)
                                {
                                        case 'b':
-                                               subst = (parseInt(param) || 0).toString(2);
+                                               subst = (+param || 0).toString(2);
                                                break;
 
                                        case 'c':
-                                               subst = String.fromCharCode(parseInt(param) || 0);
+                                               subst = String.fromCharCode(+param || 0);
                                                break;
 
                                        case 'd':
-                                               subst = (parseInt(param) || 0);
+                                               subst = (+param || 0);
                                                break;
 
                                        case 'u':
-                                               subst = Math.abs(parseInt(param) || 0);
+                                               subst = Math.abs(+param || 0);
                                                break;
 
                                        case 'f':
                                                subst = (precision > -1)
-                                                       ? ((parseFloat(param) || 0.0)).toFixed(precision)
-                                                       : (parseFloat(param) || 0.0);
+                                                       ? ((+param || 0.0)).toFixed(precision)
+                                                       : (+param || 0.0);
                                                break;
 
                                        case 'o':
-                                               subst = (parseInt(param) || 0).toString(8);
+                                               subst = (+param || 0).toString(8);
                                                break;
 
                                        case 's':
@@ -1348,11 +1376,11 @@ String.prototype.format = function()
                                                break;
 
                                        case 'x':
-                                               subst = ('' + (parseInt(param) || 0).toString(16)).toLowerCase();
+                                               subst = ('' + (+param || 0).toString(16)).toLowerCase();
                                                break;
 
                                        case 'X':
-                                               subst = ('' + (parseInt(param) || 0).toString(16)).toUpperCase();
+                                               subst = ('' + (+param || 0).toString(16)).toUpperCase();
                                                break;
 
                                        case 'h':
@@ -1395,11 +1423,11 @@ String.prototype.format = function()
                                                break;
 
                                        case 'm':
-                                               var mf = pMinLength ? parseInt(pMinLength) : 1000;
-                                               var pr = pPrecision ? Math.floor(10*parseFloat('0'+pPrecision)) : 2;
+                                               var mf = pMinLength ? +pMinLength : 1000;
+                                               var pr = pPrecision ? ~~(10 * +('0' + pPrecision)) : 2;
 
                                                var i = 0;
-                                               var val = parseFloat(param || 0);
+                                               var val = (+param || 0);
                                                var units = [ '', 'K', 'M', 'G', 'T', 'P', 'E' ];
 
                                                for (i = 0; (i < units.length) && (val > mf); i++)
index 80cbee83948f14eac07f45bd3f4c3e9ddd94ea93..4d0b50942b51c50a3b7d206fa93133877c082641 100644 (file)
@@ -1,5 +1,15 @@
 <%+cbi/valueheader%>
-<div>
+<div<%=
+       attr("data-prefix", cbid) ..
+       attr("data-browser-path", self.default_path) ..
+       attr("data-dynlist", luci.util.serialize_json({
+               self.keylist, self.vallist,
+               self.datatype, self.optional or self.rmempty
+       })) ..
+
+       ifattr(self.size, "data-size", self.size) ..
+       ifattr(self.placeholder, "data-placeholder", self.placeholder)
+%>>
 <%
        local vals = self:cfgvalue(section) or {}
        for i=1, #vals + 1 do
                if (val and #val > 0) or (i == 1) then
 %>
        <input class="cbi-input-text" value="<%=pcdata(val)%>" data-update="change" type="text"<%=
-               attr("id", cbid .. "." .. i) .. attr("name", cbid) .. ifattr(self.size, "size") ..
+               attr("id", cbid .. "." .. i) ..
+               attr("name", cbid) ..
+               ifattr(self.size, "size") ..
                ifattr(i == 1 and self.placeholder, "placeholder", self.placeholder)
        %> /><br />
 <% end end %>
 </div>
-<script type="text/javascript">
-cbi_dynlist_init(
-       '<%=cbid%>', '<%=resource%>', '<%=self.datatype%>',
-       <%=tostring(self.optional or self.rmempty)%>,
-       '<%=url('admin/filebrowser')%>',
-       '<%=self.default_path and self.default_path%>'
-       <%- if #self.keylist > 0 then -%>, [{
-               <%- for i, k in ipairs(self.keylist) do -%>
-                       <%-=string.format("%q", k) .. ":" .. string.format("%q", self.vallist[i])-%>
-                       <%-if i<#self.keylist then-%>,<%-end-%>
-               <%-     end     -%>
-       }, '<%: -- custom -- %>']<% end -%>);
-</script>
 <%+cbi/valuefooter%>
index 302df1d2fda4a24f56de05ca352f8b1863055769..9710bae8f4f10a768559dc89f6c3c571b67de66e 100644 (file)
@@ -1,7 +1,18 @@
 <%+header%>
 <form method="post" name="cbi" action="<%=REQUEST_URI%>" enctype="multipart/form-data" onreset="return cbi_validate_reset(this)" onsubmit="return cbi_validate_form(this, '<%:Some fields are invalid, cannot save values!%>')">
        <div>
-               <script type="text/javascript" src="<%=resource%>/cbi.js"></script>
+               <script type="text/javascript" src="<%=resource%>/cbi.js"<%=
+                       attr("data-strings", luci.util.serialize_json({
+                               label = {
+                                       choose = translate('-- Please choose --'),
+                                       custom = translate('-- custom --'),
+                               },
+                               path = {
+                                       resource = resource,
+                                       browser  = url("admin/filebrowser")
+                               }
+                       }))
+               %>></script>
                <input type="hidden" name="token" value="<%=token%>" />
                <input type="hidden" name="cbi.submit" value="1" />
                <input type="submit" value="<%:Save%>" class="hidden" />
index fcf216125b785562dc47b5067141778d50ad4201..26d13f9372929918873d48e43bfd63914a79753f 100644 (file)
@@ -131,8 +131,7 @@ end
                                        <input class="cbi-button cbi-button-add" type="submit" value="<%:Add%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" title="<%:Add%>" />
                                <% else %>
                                        <% if self.invalid_cts then -%><div class="cbi-section-error"><% end %>
-                                       <input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" />
-                                       <script type="text/javascript">cbi_validate_field('cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>', true, 'uciname');</script>
+                                       <input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" data-type="uciname" data-optional="true" />
                                        <input class="cbi-button cbi-button-add" type="submit" onclick="this.form.cbi_state='add-section'; return true" value="<%:Add%>" title="<%:Add%>" />
                                        <% if self.invalid_cts then -%>
                                                <br /><%:Invalid%></div>
index fcffbe0e7b1b0f499340db07e309fe061c2fb7f3..726521ae3f97193cd0c244eed1b07ef299e525ab 100644 (file)
@@ -37,8 +37,7 @@
                                <input type="submit" class="cbi-button cbi-button-add" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" value="<%:Add%>" />
                        <%- else -%>
                                <% if self.invalid_cts then -%><div class="cbi-section-error"><% end %>
-                               <input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" />
-                               <script type="text/javascript">cbi_validate_field('cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>', true, 'uciname');</script>
+                               <input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" data-type="uciname" data-optional="true" />
                                <input type="submit" class="cbi-button cbi-button-add" onclick="this.form.cbi_state='add-section'; return true" value="<%:Add%>" />
                                <% if self.invalid_cts then -%>
                                        <br /><%:Invalid%></div>
index ba0cc53e87be9f08585b8cb3c169036a70ada5c9..2cb1e75d0ee27055a8183339648e0d8f71aa9dd2 100644 (file)
 
 <% if self.optionals[section] and #self.optionals[section] > 0 or self.dynamic then %>
        <div class="cbi-optionals" data-index="<%=#self.children + 1%>">
-               <% if self.dynamic then %>
-                       <input type="text" id="cbi.opt.<%=self.config%>.<%=section%>" name="cbi.opt.<%=self.config%>.<%=section%>" />
-                       <% if self.optionals[section] and #self.optionals[section] > 0 then %>
-                       <script type="text/javascript">
-                               cbi_combobox_init('cbi.opt.<%=self.config%>.<%=section%>', {
-                               <%-
-                                       for i, val in pairs(self.optionals[section]) do
-                               -%>
-                                       <%-=string.format("%q", val.option) .. ":" .. string.format("%q", striptags(val.title))-%>
-                                       <%-if next(self.optionals[section], i) then-%>,<%-end-%>
-                               <%-
-                                       end
-                               -%>
-                               }, '', '<%-: -- custom -- -%>');
-                       </script>
-                       <% end %>
+               <%
+               if self.dynamic then
+                       local keys, vals, name, opt = { }, { }
+                       for name, opt in pairs(self.optionals[section]) do
+                               keys[#keys+1] = name
+                               vals[#vals+1] = opt.title
+                       end
+               %>
+                       <input type="text" id="cbi.opt.<%=self.config%>.<%=section%>" name="cbi.opt.<%=self.config%>.<%=section%>" data-type="uciname" data-optional="true"<%=
+                               ifattr(#keys > 0, "data-choices", luci.util.json_encode({keys, vals}))
+                       %> />
                <% else %>
-               <select id="cbi.opt.<%=self.config%>.<%=section%>" name="cbi.opt.<%=self.config%>.<%=section%>" data-optionals="true">
-                       <option><%: -- Additional Field -- %></option>
-                       <% for key, val in pairs(self.optionals[section]) do -%>
-                               <option id="cbi-<%=self.config.."-"..section.."-"..val.option%>" value="<%=val.option%>" data-index="<%=val.index%>" data-depends="<%=pcdata(val:deplist2json(section))%>"><%=striptags(val.title)%></option>
-                       <%- end %>
-               </select>
-       <% end %>
+                       <select id="cbi.opt.<%=self.config%>.<%=section%>" name="cbi.opt.<%=self.config%>.<%=section%>" data-optionals="true">
+                               <option><%: -- Additional Field -- %></option>
+                               <% for key, val in pairs(self.optionals[section]) do -%>
+                                       <option id="cbi-<%=self.config.."-"..section.."-"..val.option%>" value="<%=val.option%>" data-index="<%=val.index%>" data-depends="<%=pcdata(val:deplist2json(section))%>"><%=striptags(val.title)%></option>
+                               <%- end %>
+                       </select>
+               <% end %>
                <input type="submit" class="cbi-button cbi-button-fieldadd" value="<%:Add%>" />
        </div>
 <% end %>
index 9bb4f3b0f655851319080f626aaa8a66c50da9ad..e6e0287ef334d65ac0a905f420504ebeece05dfd 100644 (file)
@@ -8,35 +8,11 @@
                ifattr(self.size, "size") ..
                ifattr(self.placeholder, "placeholder") ..
                ifattr(self.readonly, "readonly") ..
-               ifattr(self.maxlength, "maxlength")
+               ifattr(self.maxlength, "maxlength") ..
+               ifattr(self.datatype, "data-type", self.datatype) ..
+               ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
+               ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
+               ifattr(#self.keylist > 0, "data-choices", luci.util.serialize_json({ self.keylist, self.vallist }))
        %> />
        <% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %>
-       <% if #self.keylist > 0 or self.datatype then -%>
-       <script type="text/javascript">//<![CDATA[
-               <% if #self.keylist > 0 then -%>
-               cbi_combobox_init('<%=cbid%>', {
-               <%-
-                       for i, k in ipairs(self.keylist) do
-               -%>
-                       <%-=string.format("%q", k) .. ":" .. string.format("%q", self.vallist[i])-%>
-                       <%-if i<#self.keylist then-%>,<%-end-%>
-               <%-
-                       end
-               -%>
-               }, '<%- if not self.rmempty and not self.optional then -%>
-                       <%-: -- Please choose -- -%>
-                       <%- elseif self.placeholder then -%>
-                       <%-= pcdata(self.placeholder) -%>
-               <%- end -%>', '
-               <%- if self.combobox_manual then -%>
-                       <%-=self.combobox_manual-%>
-               <%- else -%>
-                       <%-: -- custom -- -%>
-               <%- end -%>');
-               <%- end %>
-               <% if self.datatype then -%>
-               cbi_validate_field('<%=cbid%>', <%=tostring((self.optional or self.rmempty) == true)%>, '<%=self.datatype:gsub("\\", "\\\\"):gsub("'", "\\'")%>');
-               <%- end %>
-       //]]></script>
-       <% end -%>
 <%+cbi/valuefooter%>