luci-base: rework ui tabbing code
authorJo-Philipp Wich <jo@mein.io>
Wed, 5 Dec 2018 07:48:35 +0000 (08:48 +0100)
committerJo-Philipp Wich <jo@mein.io>
Mon, 10 Dec 2018 12:41:34 +0000 (13:41 +0100)
 - Instantiate tab menus on the client side
 - Simplify server side markup generation
 - Show error indicators in cbi tabs

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/htdocs/luci-static/resources/cbi.js
modules/luci-base/htdocs/luci-static/resources/luci.js
modules/luci-base/luasrc/view/cbi/map.htm
modules/luci-base/luasrc/view/cbi/nsection.htm
modules/luci-base/luasrc/view/cbi/tabcontainer.htm
modules/luci-base/luasrc/view/cbi/tabmenu.htm [deleted file]
modules/luci-base/luasrc/view/cbi/tsection.htm

index 635740a70c1a79203feb080de2114c7e5440e7e8..61b83e82941006f504d51b35cb26a75b5d23f419 100644 (file)
@@ -12,7 +12,6 @@
 */
 
 var cbi_d = [];
-var cbi_t = [];
 var cbi_strings = { path: {}, label: {} };
 
 function s8(bytes, off) {
@@ -727,13 +726,13 @@ function cbi_d_update() {
                        parent.parentNode.style.display = (parent.options.length <= 1) ? 'none' : '';
        }
 
-       if (entry && entry.parent) {
-               if (!cbi_t_update())
-                       cbi_tag_last(parent);
-       }
+       if (entry && entry.parent)
+               cbi_tag_last(parent);
 
        if (state)
                cbi_d_update();
+       else if (parent)
+               parent.dispatchEvent(new CustomEvent('dependency-update', { bubbles: true }));
 }
 
 function cbi_init() {
@@ -1045,75 +1044,6 @@ function cbi_dynlist_init(dl, datatype, optional, choices)
 cbi_dynlist_init.prototype = CBIDynamicList;
 
 
-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.classList.remove('cbi-tab');
-                               o2.tab.classList.add('cbi-tab-disabled');
-                               o2.container.style.display = 'none';
-                       }
-                       else {
-                               if(h)
-                                       h.value = tab;
-
-                               o2.tab.classList.remove('cbi-tab-disabled');
-                               o2.tab.classList.add('cbi-tab');
-                               o2.container.style.display = 'block';
-                       }
-               }
-       }
-
-       return false;
-}
-
-function cbi_t_update() {
-       var hl_tabs = [ ];
-       var updated = false;
-
-       for (var sid in cbi_t)
-               for (var tid in cbi_t[sid]) {
-                       var t = cbi_t[sid][tid].tab;
-                       var c = cbi_t[sid][tid].container;
-
-                       if (!c.firstElementChild) {
-                               t.style.display = 'none';
-                       }
-                       else if (t.style.display == 'none') {
-                               t.style.display = '';
-                               t.classList.add('cbi-tab-highlighted');
-                               hl_tabs.push(t);
-                       }
-
-                       cbi_tag_last(c);
-                       updated = true;
-               }
-
-       if (hl_tabs.length > 0)
-               window.setTimeout(function() {
-                       for (var i = 0; i < hl_tabs.length; i++)
-                               hl_tabs[i].classList.remove('cbi-tab-highlighted');
-               }, 750);
-
-       return updated;
-}
-
-
 function cbi_validate_form(form, errmsg)
 {
        /* if triggered by a section removal or addition, don't validate */
index c1c1b0dd3c238f871de27222e5e8253b41f223cc..4cb8bf4e5d08cb0cb293ade5a5afd719dde6611a 100644 (file)
                }
        };
 
+       /* Tabs */
+       LuCI.prototype.tabs = {
+               init: function() {
+                       var groups = [], prevGroup = null, currGroup = null;
+
+                       document.querySelectorAll('[data-tab]').forEach(function(tab) {
+                               var parent = tab.parentNode;
+
+                               if (!parent.hasAttribute('data-tab-group'))
+                                       parent.setAttribute('data-tab-group', groups.length);
+
+                               currGroup = +parent.getAttribute('data-tab-group');
+
+                               if (currGroup !== prevGroup) {
+                                       prevGroup = currGroup;
+
+                                       if (!groups[currGroup])
+                                               groups[currGroup] = [];
+                               }
+
+                               groups[currGroup].push(tab);
+                       });
+
+                       for (var i = 0; i < groups.length; i++)
+                               this.initTabGroup(groups[i]);
+
+                       document.addEventListener('dependency-update', this.updateTabs.bind(this));
+
+                       this.updateTabs();
+
+                       if (!groups.length)
+                               this.setActiveTabId(-1, -1);
+               },
+
+               initTabGroup: function(panes) {
+                       if (!Array.isArray(panes) || panes.length === 0)
+                               return;
+
+                       var menu = E('ul', { 'class': 'cbi-tabmenu' }),
+                           group = panes[0].parentNode,
+                           groupId = +group.getAttribute('data-tab-group'),
+                           selected = null;
+
+                       for (var i = 0, pane; pane = panes[i]; i++) {
+                               var name = pane.getAttribute('data-tab'),
+                                   title = pane.getAttribute('data-tab-title'),
+                                   active = pane.getAttribute('data-tab-active') === 'true';
+
+                               menu.appendChild(E('li', {
+                                       'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
+                                       'data-tab': name
+                               }, E('a', {
+                                       'href': '#',
+                                       'click': this.switchTab.bind(this)
+                               }, title)));
+
+                               if (active)
+                                       selected = i;
+                       }
+
+                       group.parentNode.insertBefore(menu, group);
+
+                       if (selected === null) {
+                               selected = this.getActiveTabId(groupId);
+
+                               if (selected < 0 || selected >= panes.length)
+                                       selected = 0;
+
+                               menu.childNodes[selected].classList.add('cbi-tab');
+                               menu.childNodes[selected].classList.remove('cbi-tab-disabled');
+                               panes[selected].setAttribute('data-tab-active', 'true');
+
+                               this.setActiveTabId(groupId, selected);
+                       }
+               },
+
+               getActiveTabState: function() {
+                       var page = document.body.getAttribute('data-page');
+
+                       try {
+                               var val = JSON.parse(window.sessionStorage.getItem('tab'));
+                               if (val.page === page && Array.isArray(val.groups))
+                                       return val;
+                       }
+                       catch(e) {}
+
+                       window.sessionStorage.removeItem('tab');
+                       return { page: page, groups: [] };
+               },
+
+               getActiveTabId: function(groupId) {
+                       return +this.getActiveTabState().groups[groupId] || 0;
+               },
+
+               setActiveTabId: function(groupId, tabIndex) {
+                       try {
+                               var state = this.getActiveTabState();
+                                   state.groups[groupId] = tabIndex;
+
+                           window.sessionStorage.setItem('tab', JSON.stringify(state));
+                       }
+                       catch (e) { return false; }
+
+                       return true;
+               },
+
+               updateTabs: function(ev) {
+                       document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
+                               var menu = pane.parentNode.previousElementSibling,
+                                   tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
+                                   n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
+
+                               if (!pane.firstElementChild) {
+                                       tab.style.display = 'none';
+                                       tab.classList.remove('flash');
+                               }
+                               else if (tab.style.display === 'none') {
+                                       tab.style.display = '';
+                                       requestAnimationFrame(function() { tab.classList.add('flash') });
+                               }
+
+                               if (n_errors) {
+                                       tab.setAttribute('data-errors', n_errors);
+                                       tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
+                                       tab.setAttribute('data-tooltip-style', 'error');
+                               }
+                               else {
+                                       tab.removeAttribute('data-errors');
+                                       tab.removeAttribute('data-tooltip');
+                               }
+                       });
+               },
+
+               switchTab: function(ev) {
+                       var tab = ev.target.parentNode,
+                           name = tab.getAttribute('data-tab'),
+                           menu = tab.parentNode,
+                           group = menu.nextElementSibling,
+                           groupId = +group.getAttribute('data-tab-group'),
+                           index = 0;
+
+                       ev.preventDefault();
+
+                       if (!tab.classList.contains('cbi-tab-disabled'))
+                               return;
+
+                       menu.querySelectorAll('[data-tab]').forEach(function(tab) {
+                               tab.classList.remove('cbi-tab');
+                               tab.classList.remove('cbi-tab-disabled');
+                               tab.classList.add(
+                                       tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
+                       });
+
+                       group.childNodes.forEach(function(pane) {
+                               if (L.dom.matches(pane, '[data-tab]')) {
+                                       if (pane.getAttribute('data-tab') === name) {
+                                               pane.setAttribute('data-tab-active', 'true');
+                                               L.tabs.setActiveTabId(groupId, index);
+                                       }
+                                       else {
+                                               pane.setAttribute('data-tab-active', 'false');
+                                       }
+
+                                       index++;
+                               }
+                       });
+               }
+       };
+
        /* DOM manipulation */
        LuCI.prototype.dom = {
                elem: function(e) {
                }
        };
 
+       /* Setup */
+       LuCI.prototype.setupDOM = function(ev) {
+               this.tabs.init();
+       };
+
        function LuCI(env) {
                this.env = env;
 
                document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
                document.addEventListener('focus', this.showTooltip.bind(this), true);
                document.addEventListener('blur', this.hideTooltip.bind(this), true);
+
+               document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
        }
 
        window.LuCI = LuCI;
index d65a16167342f8dd5c90b8482662d79f20eb7172..31997a1c00bb7cf689e3e8071f4d7dd3627f9e41 100644 (file)
@@ -3,25 +3,25 @@
 <%- end end -%>
 
 <div class="cbi-map" id="cbi-<%=self.config%>">
-       <% if self.title and #self.title > 0 then %><h2 name="content"><%=self.title%></h2><% end %>
-       <% if self.description and #self.description > 0 then %><div class="cbi-map-descr"><%=self.description%></div><% end %>
+       <% if self.title and #self.title > 0 then %>
+               <h2 name="content"><%=self.title%></h2>
+       <% end %>
+       <% if self.description and #self.description > 0 then %>
+               <div class="cbi-map-descr"><%=self.description%></div>
+       <% end %>
        <% if self.tabbed then %>
-               <ul class="cbi-tabmenu map">
-                       <%- self.selected_tab = luci.http.formvalue("tab.m-" .. self.config) %>
-                       <% for i, section in ipairs(self.children) do %>
-                               <%- if not self.selected_tab then self.selected_tab = section.sectiontype end %>
-                               <li id="tab.m-<%=self.config%>.<%=section.section or section.sectiontype%>" class="cbi-tab<%=(section.sectiontype == self.selected_tab) and '' or '-disabled'%>">
-                                       <a onclick="this.blur(); return cbi_t_switch('m-<%=self.config%>', '<%=section.section or section.sectiontype%>')" href="<%=REQUEST_URI%>?tab.m-<%=self.config%>=<%=section.section or section.sectiontype%>"><%=section.title or section.section or section.sectiontype %></a>
-                                       <% if section.sectiontype == self.selected_tab then %><input type="hidden" id="tab.m-<%=self.config%>" name="tab.m-<%=self.config%>" value="<%=section.section or section.sectiontype%>" /><% end %>
-                               </li>
+               <div>
+                       <% for i, section in ipairs(self.children) do
+                              tab = section.section or section.sectiontype %>
+                               <div class="cbi-tabcontainer"<%=
+                                       attr("id", "container.m-%s.%s" %{ self.config, tab }) ..
+                                       attr("data-tab", tab) ..
+                                       attr("data-tab-title", section.title or tab))
+                               %>>
+                                       <% section:render() %>
+                               </div>
                        <% end %>
-               </ul>
-               <% for i, section in ipairs(self.children) do %>
-                       <div class="cbi-tabcontainer" id="container.m-<%=self.config%>.<%=section.section or section.sectiontype%>"<% if section.sectiontype ~= self.selected_tab then %> style="display:none"<% end %>>
-                               <% section:render() %>
-                       </div>
-                       <script type="text/javascript">cbi_t_add('m-<%=self.config%>', '<%=section.section or section.sectiontype%>')</script>
-               <% end %>
+               </div>
 
                <% if not self.save then -%>
                        <div class="cbi-section-error">
index 63abc577342687645f9630c2934541ee3ef34816..14232e3d944ae3992699b96013c16c7fec4d45cb 100644 (file)
@@ -11,7 +11,6 @@
                                <input type="submit" class="cbi-button" name="cbi.rns.<%=self.config%>.<%=section%>" value="<%:Delete%>" />
                        </div>
                <%- end %>
-               <%+cbi/tabmenu%>
                <div class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>">
                        <%+cbi/ucisection%>
                </div>
index 38c435d6a112e69b3240e5cc290bec365fa4d4a0..7fcb83578306316349eec2a160dcc4d03ad411df 100644 (file)
@@ -1,7 +1,14 @@
-<% for tab, data in pairs(self.tabs) do %>
-       <div class="cbi-tabcontainer" 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 %>
+<% for _, tab in ipairs(self.tab_names) do data = self.tabs[tab] %>
+       <div class="cbi-tabcontainer"<%=
+               attr("id", "container.%s.%s.%s" %{ self.config, section, tab }) ..
+               attr("data-tab", tab) ..
+               attr("data-tab-title", data.title) ..
+               attr("data-tab-active", tostring(tab == self.selected_tab))
+       %>>
+               <% 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/modules/luci-base/luasrc/view/cbi/tabmenu.htm b/modules/luci-base/luasrc/view/cbi/tabmenu.htm
deleted file mode 100644 (file)
index 06c1414..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<%- 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 %>
-               <%- 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 1a13df0c0402f88521818ab6589dc7faaeae4792..547a793329489614e2bb62c3b91f7c08befc34d2 100644 (file)
@@ -18,8 +18,6 @@
                        <h3><%=section:upper()%></h3>
                <%- end %>
 
-               <%+cbi/tabmenu%>
-
                <div class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>">
                        <%+cbi/ucisection%>
                </div>