luci-0.9: backport CBI from trunk, adds tab support, auto-hiding of commit notificati...
authorJo-Philipp Wich <jow@openwrt.org>
Tue, 9 Mar 2010 02:05:49 +0000 (02:05 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Tue, 9 Mar 2010 02:05:49 +0000 (02:05 +0000)
libs/cbi/htdocs/luci-static/resources/cbi.js
libs/cbi/luasrc/cbi.lua
libs/cbi/luasrc/view/cbi/map.htm
libs/cbi/luasrc/view/cbi/nsection.htm
libs/cbi/luasrc/view/cbi/tabcontainer.htm [new file with mode: 0644]
libs/cbi/luasrc/view/cbi/tabmenu.htm [new file with mode: 0644]
libs/cbi/luasrc/view/cbi/tsection.htm

index 77c59e5..247228d 100644 (file)
@@ -2,7 +2,7 @@
        LuCI - Lua Configuration Interface
 
        Copyright 2008 Steven Barth <steven@midlink.org>
-       Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+       Copyright 2008-2009 Jo-Philipp Wich <xm@subsignal.org>
 
        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
@@ -14,6 +14,8 @@
 */
 
 var cbi_d = [];
+var cbi_t = [];
+var cbi_c = [];
 
 function cbi_d_add(field, dep, next) {
        var obj = document.getElementById(field);
@@ -43,7 +45,18 @@ function cbi_d_checkvalue(target, ref) {
        var t = document.getElementById(target);
        var value;
 
-       if (!t || !t.value) {
+       if (!t) {
+               var tl = document.getElementsByName(target);
+
+               if( tl.length > 0 && tl[0].type == 'radio' )
+                       for( var i = 0; i < tl.length; i++ )
+                               if( tl[i].checked ) {
+                                       value = tl[i].value;
+                                       break;
+                               }
+
+               value = value ? value : "";
+       } else if (!t.value) {
                value = "";
        } else {
                value = t.value;
@@ -57,15 +70,26 @@ function cbi_d_checkvalue(target, ref) {
 }
 
 function cbi_d_check(deps) {
+       var reverse;
+       var def = false;
        for (var i=0; i<deps.length; i++) {
-               var istat = true
+               var istat = true;
+               reverse = false;
                for (var j in deps[i]) {
-                       istat = (istat && cbi_d_checkvalue(j, deps[i][j]))
+                       if (j == "!reverse") {
+                               reverse = true;
+                       } else if (j == "!default") {
+                               def = true;
+                               istat = false;
+                       } else {
+                               istat = (istat && cbi_d_checkvalue(j, deps[i][j]))
+                       }
                }
                if (istat) {
-                       return true
+                       return !reverse;
                }
        }
+       return def;
 }
 
 function cbi_d_update() {
@@ -78,16 +102,25 @@ function cbi_d_update() {
 
                if (node && node.parentNode && !cbi_d_check(entry.deps)) {
                        node.parentNode.removeChild(node);
-                       state = (state || !node.parentNode)
+                       state = true;
+                       if( entry.parent )
+                               cbi_c[entry.parent]--;
                } else if ((!node || !node.parentNode) && cbi_d_check(entry.deps)) {
                        if (!next) {
                                parent.appendChild(entry.node);
                        } else {
                                next.parentNode.insertBefore(entry.node, next);
                        }
-                       state = (state || (node && node.parentNode))
+                       state = true;
+                       if( entry.parent )
+                               cbi_c[entry.parent]++;
                }
        }
+
+       if (entry.parent) {
+               cbi_t_update();
+       }
+
        if (state) {
                cbi_d_update();
        }
@@ -219,3 +252,50 @@ function cbi_hijack_forms(layer, win, fail, load) {
                });
        }
 }
+
+
+function cbi_t_add(section, tab) {
+       var t = document.getElementById('tab.' + section + '.' + tab);
+       var c = document.getElementById('container.' + section + '.' + tab);
+
+       if( t && c ) {
+               cbi_t[section] = (cbi_t[section] || [ ]);
+               cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
+       }
+}
+
+function cbi_t_switch(section, tab) {
+       if( cbi_t[section] && cbi_t[section][tab] ) {
+               var o = cbi_t[section][tab];
+               var h = document.getElementById('tab.' + section);
+               for( var tid in cbi_t[section] ) {
+                       var o2 = cbi_t[section][tid];
+                       if( o.tab.id != o2.tab.id ) {
+                               o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
+                               o2.container.style.display = 'none';
+                       }
+                       else {
+                               if(h) h.value = tab;
+                               o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
+                               o2.container.style.display = 'block';
+                       }
+               }
+       }
+       return false
+}
+
+function cbi_t_update() {
+       for( var sid in cbi_t )
+               for( var tid in cbi_t[sid] )
+                       if( cbi_c[cbi_t[sid][tid].cid] == 0 ) {
+                               cbi_t[sid][tid].tab.style.display = 'none';
+                       }
+                       else if( cbi_t[sid][tid].tab && cbi_t[sid][tid].tab.style.display == 'none' ) {
+                               cbi_t[sid][tid].tab.style.display = '';
+
+                               var t = cbi_t[sid][tid].tab;
+                               window.setTimeout(function() { t.className = t.className.replace(/ cbi-tab-highlighted/g, '') }, 750);
+                               cbi_t[sid][tid].tab.className += ' cbi-tab-highlighted';
+                       }
+}
+
index c75f4f8..b280944 100644 (file)
@@ -230,7 +230,7 @@ function Node._i18n(self, config, section, option, title, description)
 
                local key = config and config:gsub("[^%w]+", "") or ""
 
-               if section then key = key .. "_" .. section:lower():gsub("[^%w]+", "") end
+               if section then key = key .. "_" .. section:lower():gsub("[^%w]+", "") end
                if option  then key = key .. "_" .. tostring(option):lower():gsub("[^%w]+", "")  end
 
                self.title = title or luci.i18n.translate( key, option or section or config )
@@ -238,6 +238,25 @@ function Node._i18n(self, config, section, option, title, description)
        end
 end
 
+-- hook helper
+function Node._run_hook(self, hook)
+       if type(self[hook]) == "function" then
+               return self[hook](self)
+       end 
+end
+
+function Node._run_hooks(self, ...)
+       local f
+       local r = false
+       for _, f in ipairs(arg) do
+               if type(self[f]) == "function" then
+                       self[f](self)
+                       r = true
+               end
+       end
+       return r
+end
+
 -- Prepare nodes
 function Node.prepare(self, ...)
        for k, child in ipairs(self.children) do
@@ -357,6 +376,7 @@ end
 -- Use optimized UCI writing
 function Map.parse(self, readinput, ...)
        self.readinput = (readinput ~= false)
+       self:_run_hooks("on_parse")
 
        if self:formvalue("cbi.skip") then
                self.state = FORM_SKIP
@@ -370,14 +390,17 @@ function Map.parse(self, readinput, ...)
                        self.uci:save(config)
                end
                if self:submitstate() and ((not self.proceed and self.flow.autoapply) or luci.http.formvalue("cbi.apply")) then
+                       self:_run_hooks("on_before_commit")
                        for i, config in ipairs(self.parsechain) do
                                self.uci:commit(config)
 
                                -- Refresh data because commit changes section names
                                self.uci:load(config)
                        end
+                       self:_run_hooks("on_commit", "on_after_commit", "on_before_apply")
                        if self.apply_on_parse then
                                self.uci:apply(self.parsechain)
+                               self:_run_hooks("on_apply", "on_after_apply")
                        else
                                self._apply = function()
                                        local cmd = self.uci:apply(self.parsechain, true)
@@ -413,11 +436,13 @@ function Map.parse(self, readinput, ...)
 end
 
 function Map.render(self, ...)
+       self:_run_hooks("on_init")
        Node.render(self, ...)
        if self._apply then
                local fp = self._apply()
                fp:read("*a")
                fp:close()
+               self:_run_hooks("on_apply")
        end
 end
 
@@ -506,16 +531,13 @@ function Delegator.__init__(self, ...)
        self.pageaction = false
        self.readinput = true
        self.allow_reset = false
+       self.allow_cancel = false
        self.allow_back = false
        self.allow_finish = false
        self.template = "cbi/delegator"
 end
 
 function Delegator.set(self, name, node)
-       if type(node) == "table" and getmetatable(node) == nil then
-               node = Compound(unpack(node))
-       end
-       assert(type(node) == "function" or instanceof(node, Compound), "Invalid")
        assert(not self.nodes[name], "Duplicate entry")
 
        self.nodes[name] = node
@@ -527,9 +549,9 @@ function Delegator.add(self, name, node)
 end
 
 function Delegator.insert_after(self, name, after)
-       local n = #self.chain
+       local n = #self.chain + 1
        for k, v in ipairs(self.chain) do
-               if v == state then
+               if v == after then
                        n = k + 1
                        break
                end
@@ -555,10 +577,30 @@ function Delegator.set_route(self, ...)
 end
 
 function Delegator.get(self, name)
-       return self.nodes[name]
+       local node = self.nodes[name]
+
+       if type(node) == "string" then
+               node = load(node, name)
+       end
+
+       if type(node) == "table" and getmetatable(node) == nil then
+               node = Compound(unpack(node))
+       end
+
+       return node
 end
 
 function Delegator.parse(self, ...)
+       if self.allow_cancel and Map.formvalue(self, "cbi.cancel") then
+               if self:_run_hooks("on_cancel") then
+                       return FORM_DONE
+               end
+       end
+       
+       if not Map.formvalue(self, "cbi.delg.current") then
+               self:_run_hooks("on_init")
+       end
+
        local newcurrent
        self.chain = self.chain or self:get_chain()
        self.current = self.current or self:get_active()
@@ -586,14 +628,20 @@ function Delegator.parse(self, ...)
 
        if not Map.formvalue(self, "cbi.submit") then
                return FORM_NODATA
-       elseif not newcurrent or not self:get(newcurrent) then
-               return FORM_DONE
+       elseif stat > FORM_PROCEED 
+       and (not newcurrent or not self:get(newcurrent)) then
+               return self:_run_hook("on_done") or FORM_DONE
        else
-               self.current = newcurrent
+               self.current = newcurrent or self.current
                self.active = self:get(self.current)
                if type(self.active) ~= "function" then
-                       self.active:parse(false)
-                       return FROM_PROCEED
+                       self.active:populate_delegator(self)
+                       local stat = self.active:parse(false)
+                       if stat == FORM_SKIP then
+                               return self:parse(...)
+                       else
+                               return FORM_PROCEED
+                       end
                else
                        return self:parse(...)
                end
@@ -659,6 +707,10 @@ function SimpleForm.parse(self, readinput, ...)
                return FORM_SKIP
        end
 
+       if self:formvalue("cbi.cancel") and self:_run_hooks("on_cancel") then
+               return FORM_DONE
+       end
+
        if self:submitstate() then
                Node.parse(self, 1, ...)
        end
@@ -781,6 +833,19 @@ function AbstractSection.__init__(self, map, sectiontype, ...)
        self.dynamic = false
 end
 
+-- Define a tab for the section
+function AbstractSection.tab(self, tab, title, desc)
+       self.tabs      = self.tabs      or { }
+       self.tab_names = self.tab_names or { }
+
+       self.tab_names[#self.tab_names+1] = tab
+       self.tabs[tab] = {
+               title       = title,
+               description = desc,
+               childs      = { }
+       }
+end
+
 -- Appends a new option
 function AbstractSection.option(self, class, option, ...)
        -- Autodetect from UVL
@@ -812,6 +877,31 @@ function AbstractSection.option(self, class, option, ...)
        end
 end
 
+-- Appends a new tabbed option
+function AbstractSection.taboption(self, tab, ...)
+
+       assert(tab and self.tabs and self.tabs[tab],
+               "Cannot assign option to not existing tab %q" % tostring(tab))
+
+       local l = self.tabs[tab].childs
+       local o = AbstractSection.option(self, ...)
+
+       if o then l[#l+1] = o end
+
+       return o
+end
+
+-- Render a single tab
+function AbstractSection.render_tab(self, tab, ...)
+
+       assert(tab and self.tabs and self.tabs[tab],
+               "Cannot render not existing tab %q" % tostring(tab))
+
+       for _, node in ipairs(self.tabs[tab].childs) do
+               node:render(...)
+       end
+end
+
 -- Parse optional options
 function AbstractSection.parse_optionals(self, section)
        if not self.optional then
@@ -1215,6 +1305,7 @@ function AbstractValue.__init__(self, map, section, option, ...)
        self.tag_reqerror = {}
        self.tag_error = {}
        self.deps = {}
+       self.subdeps = {}
        --self.cast = "string"
 
        self.track_missing = false
@@ -1356,6 +1447,7 @@ function AbstractValue.render(self, s, scope)
                scope.section   = s
                scope.cbid      = self:cbid(s)
                scope.striptags = luci.util.striptags
+               scope.pcdata    = luci.util.pcdata
 
                scope.ifattr = function(cond,key,val)
                        if cond then
@@ -1544,7 +1636,7 @@ function ListValue.value(self, key, val, ...)
        table.insert(self.vallist, tostring(val))
 
        for i, deps in ipairs({...}) do
-               table.insert(self.deps, {add = "-"..key, deps=deps})
+               self.subdeps[#self.subdeps + 1] = {add = "-"..key, deps=deps}
        end
 end
 
index 949edea..620203a 100644 (file)
@@ -14,10 +14,25 @@ $Id$
 -%>
 
 <div class="cbi-map" id="cbi-<%=self.config%>">
-       <h2><a id="content" name="content"><%=self.title%></a></h2>
-       <div class="cbi-map-descr"><%=self.description%></div>
+       <% if self.title and #self.title > 0 then %>
+               <h2>
+                       <%
+                               if self.breadcrumb then
+                                       local elem
+                                       for _, elem in ipairs(self.breadcrumb) do
+                       -%>
+                                       <a href="<%=luci.util.pcdata(elem[1])%>"><%=luci.util.pcdata(elem[2])%></a> &raquo;
+                       <%
+                                       end
+                               end
+                       -%>
+                       <a id="content" name="content"><%=self.title%></a>
+               </h2>
+       <% end %>
+
+       <% if self.description and #self.description > 0 then %><div class="cbi-map-descr"><%=self.description%></div><% end %>
        <%- if self._apply then -%>
-               <fieldset class="cbi-section">
+               <fieldset class="cbi-section" id="cbi-apply-<%=self.config%>">
                        <legend><%:cbi_applying%></legend>
                        <ul class="cbi-apply"><%-
                                local fp = self._apply()
@@ -30,6 +45,12 @@ $Id$
                                fp:close()
                        -%></ul>
                </fieldset>
+               <script type="text/javascript">
+                       window.setTimeout(function() {
+                               var e = document.getElementById('cbi-apply-<%=self.config%>');
+                               if(e) e.style.display = 'none';
+                       }, 2000);
+               </script>
        <%- end -%>
        <%- self:render_children() %>
        <br />
index d766464..d096ac3 100644 (file)
@@ -1,7 +1,7 @@
 <%#
 LuCI - Lua Configuration Interface
 Copyright 2008 Steven Barth <steven@midlink.org>
-Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+Copyright 2008-2009 Jo-Philipp Wich <xm@subsignal>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ $Id$
                                <input type="submit" name="cbi.rns.<%=self.config%>.<%=section%>" value="<%:cbi_del%>" />
                        </div>
                <%- end %>
+               <%+cbi/tabmenu%>
                <div class="cbi-section-node" id="cbi-<%=self.config%>-<%=section%>">
                        <%+cbi/ucisection%>
                </div>
diff --git a/libs/cbi/luasrc/view/cbi/tabcontainer.htm b/libs/cbi/luasrc/view/cbi/tabcontainer.htm
new file mode 100644 (file)
index 0000000..b620622
--- /dev/null
@@ -0,0 +1,21 @@
+<%#
+LuCI - Lua Configuration Interface
+Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+$Id: tabcontainer.htm 5269 2009-08-16 03:29:46Z jow $
+
+-%>
+
+<% for tab, data in pairs(self.tabs) do %>
+       <div id="container.<%=self.config%>.<%=section%>.<%=tab%>"<% if tab ~= self.selected_tab then %> style="display:none"<% end %>>
+               <% if data.description then %><div class="cbi-tab-descr"><%=data.description%></div><% end %>
+               <% self:render_tab(tab, section, scope or {}) %>
+       </div>
+       <script type="text/javascript">cbi_t_add('<%=self.config%>.<%=section%>', '<%=tab%>')</script>
+<% end %>
diff --git a/libs/cbi/luasrc/view/cbi/tabmenu.htm b/libs/cbi/luasrc/view/cbi/tabmenu.htm
new file mode 100644 (file)
index 0000000..da538c2
--- /dev/null
@@ -0,0 +1,27 @@
+<%#
+LuCI - Lua Configuration Interface
+Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+$Id: tabmenu.htm 5398 2009-10-10 22:18:50Z jow $
+
+-%>
+
+<%- if self.tabs then %>
+       <ul class="cbi-tabmenu">
+       <%- self.selected_tab = luci.http.formvalue("tab." .. self.config .. "." .. section) %>
+       <%- for _, tab in ipairs(self.tab_names) do if #self.tabs[tab].childs > 0 then %>
+               <script type="text/javascript">cbi_c['container.<%=self.config%>.<%=section%>.<%=tab%>'] = <%=#self.tabs[tab].childs%>;</script>
+               <%- if not self.selected_tab then self.selected_tab = tab end %>
+               <li id="tab.<%=self.config%>.<%=section%>.<%=tab%>" class="cbi-tab<%=(tab == self.selected_tab) and '' or '-disabled'%>">
+                       <a onclick="this.blur(); return cbi_t_switch('<%=self.config%>.<%=section%>', '<%=tab%>')" href="<%=REQUEST_URI%>?tab.<%=self.config%>.<%=section%>=<%=tab%>"><%=self.tabs[tab].title%></a>
+                       <% if tab == self.selected_tab then %><input type="hidden" id="tab.<%=self.config%>.<%=section%>" name="tab.<%=self.config%>.<%=section%>" value="<%=tab%>" /><% end %>
+               </li>
+       <% end end -%>
+       </ul>
+<% end -%>
index db723f9..7fdd7f4 100644 (file)
@@ -24,12 +24,15 @@ $Id$
                                <input type="submit" name="cbi.rts.<%=self.config%>.<%=k%>" value="<%:cbi_del%>" />
                        </div>
                <%- end %>
-               <% section = k; isempty = false %>
+
+               <%- section = k; isempty = false -%>
 
                <% if not self.anonymous then -%>
-                       <h3><%=k:upper()%></h3>
+                       <h3><%=section:upper()%></h3>
                <%- end %>
 
+               <%+cbi/tabmenu%>
+
                <fieldset class="cbi-section-node" id="cbi-<%=self.config%>-<%=section%>">
                        <%+cbi/ucisection%>
                </fieldset>