From 76e9c0305ed65480e0d1e3e86d831ac24de98bcd Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Wed, 5 Dec 2018 08:48:35 +0100 Subject: [PATCH] luci-base: rework ui tabbing code - Instantiate tab menus on the client side - Simplify server side markup generation - Show error indicators in cbi tabs Signed-off-by: Jo-Philipp Wich --- .../htdocs/luci-static/resources/cbi.js | 78 +------- .../htdocs/luci-static/resources/luci.js | 176 ++++++++++++++++++ modules/luci-base/luasrc/view/cbi/map.htm | 34 ++-- .../luci-base/luasrc/view/cbi/nsection.htm | 1 - .../luasrc/view/cbi/tabcontainer.htm | 15 +- modules/luci-base/luasrc/view/cbi/tabmenu.htm | 12 -- .../luci-base/luasrc/view/cbi/tsection.htm | 2 - 7 files changed, 208 insertions(+), 110 deletions(-) delete mode 100644 modules/luci-base/luasrc/view/cbi/tabmenu.htm diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js index 635740a70c..61b83e8294 100644 --- a/modules/luci-base/htdocs/luci-static/resources/cbi.js +++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js @@ -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 */ diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index c1c1b0dd3c..4cb8bf4e5d 100644 --- a/modules/luci-base/htdocs/luci-static/resources/luci.js +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -181,6 +181,175 @@ } }; + /* 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) { @@ -316,6 +485,11 @@ } }; + /* Setup */ + LuCI.prototype.setupDOM = function(ev) { + this.tabs.init(); + }; + function LuCI(env) { this.env = env; @@ -329,6 +503,8 @@ 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; diff --git a/modules/luci-base/luasrc/view/cbi/map.htm b/modules/luci-base/luasrc/view/cbi/map.htm index d65a161673..31997a1c00 100644 --- a/modules/luci-base/luasrc/view/cbi/map.htm +++ b/modules/luci-base/luasrc/view/cbi/map.htm @@ -3,25 +3,25 @@ <%- end end -%>
- <% if self.title and #self.title > 0 then %>

<%=self.title%>

<% end %> - <% if self.description and #self.description > 0 then %>
<%=self.description%>
<% end %> + <% if self.title and #self.title > 0 then %> +

<%=self.title%>

+ <% end %> + <% if self.description and #self.description > 0 then %> +
<%=self.description%>
+ <% end %> <% if self.tabbed then %> -
    - <%- 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 %> -
  • - <%=section.title or section.section or section.sectiontype %> - <% if section.sectiontype == self.selected_tab then %><% end %> -
  • +
    + <% for i, section in ipairs(self.children) do + tab = section.section or section.sectiontype %> +
    > + <% section:render() %> +
    <% end %> -
- <% for i, section in ipairs(self.children) do %> -
style="display:none"<% end %>> - <% section:render() %> -
- - <% end %> +
<% if not self.save then -%>
diff --git a/modules/luci-base/luasrc/view/cbi/nsection.htm b/modules/luci-base/luasrc/view/cbi/nsection.htm index 63abc57734..14232e3d94 100644 --- a/modules/luci-base/luasrc/view/cbi/nsection.htm +++ b/modules/luci-base/luasrc/view/cbi/nsection.htm @@ -11,7 +11,6 @@
<%- end %> - <%+cbi/tabmenu%>
<%+cbi/ucisection%>
diff --git a/modules/luci-base/luasrc/view/cbi/tabcontainer.htm b/modules/luci-base/luasrc/view/cbi/tabcontainer.htm index 38c435d6a1..7fcb835783 100644 --- a/modules/luci-base/luasrc/view/cbi/tabcontainer.htm +++ b/modules/luci-base/luasrc/view/cbi/tabcontainer.htm @@ -1,7 +1,14 @@ -<% for tab, data in pairs(self.tabs) do %> -
style="display:none"<% end %>> - <% if data.description then %>
<%=data.description%>
<% end %> +<% for _, tab in ipairs(self.tab_names) do data = self.tabs[tab] %> +
> + <% if data.description then %> +
<%=data.description%>
+ <% end %> + <% self:render_tab(tab, section, scope or {}) %>
- <% 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 index 06c1414bf3..0000000000 --- a/modules/luci-base/luasrc/view/cbi/tabmenu.htm +++ /dev/null @@ -1,12 +0,0 @@ -<%- if self.tabs then %> -
    - <%- 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 %> -
  • - <%=self.tabs[tab].title%> - <% if tab == self.selected_tab then %><% end %> -
  • - <% end end -%> -
-<% end -%> diff --git a/modules/luci-base/luasrc/view/cbi/tsection.htm b/modules/luci-base/luasrc/view/cbi/tsection.htm index 1a13df0c04..547a793329 100644 --- a/modules/luci-base/luasrc/view/cbi/tsection.htm +++ b/modules/luci-base/luasrc/view/cbi/tsection.htm @@ -18,8 +18,6 @@

<%=section:upper()%>

<%- end %> - <%+cbi/tabmenu%> -
<%+cbi/ucisection%>
-- 2.30.2