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 <jo@mein.io>
This commit is contained in:
Jo-Philipp Wich 2018-12-05 08:48:35 +01:00
parent 747e10bae6
commit 76e9c0305e
7 changed files with 208 additions and 110 deletions

View 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 */

View file

@ -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;

View 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">

View 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>

View 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 %>

View file

@ -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 -%>

View 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>