From 344c4c511936062ec4521d42d1c1ba51577daee0 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Mon, 7 Jan 2019 15:26:08 +0100 Subject: [PATCH] luci-base: luci.js: split ui helper functions into external ui.js Use the new class loader infrastructure to move gui specific functionality out of the luci.js core and dispatch a new event 'luci-loaded' which is fired once all external classes have been fetched. Signed-off-by: Jo-Philipp Wich --- .../htdocs/luci-static/resources/luci.js | 353 +++--------------- .../htdocs/luci-static/resources/ui.js | 296 +++++++++++++++ 2 files changed, 339 insertions(+), 310 deletions(-) create mode 100644 modules/luci-base/htdocs/luci-static/resources/ui.js diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index 610cbcb62a..b86d499c6d 100644 --- a/modules/luci-base/htdocs/luci-static/resources/luci.js +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -423,10 +423,7 @@ }); - var modalDiv = null, - tooltipDiv = null, - tooltipTimeout = null, - dummyElem = null, + var dummyElem = null, domParser = null, originalCBIInit = null, classes = {}; @@ -436,17 +433,6 @@ __init__: function(env) { Object.assign(this.env, env); - modalDiv = document.body.appendChild( - this.dom.create('div', { id: 'modal_overlay' }, - this.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true }))); - - tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' })); - - document.addEventListener('mouseover', this.showTooltip.bind(this), true); - 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)); document.addEventListener('poll-start', function(ev) { @@ -554,32 +540,50 @@ /* DOM setup */ setupDOM: function(ev) { - this.tabs.init(); - - Request.addInterceptor(function(res) { - if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes') - return; - - Request.poll.stop(); - - L.showModal(_('Session expired'), [ - E('div', { class: 'alert-message warning' }, - _('A new login is required since the authentication session expired.')), - E('div', { class: 'right' }, - E('div', { - class: 'btn primary', - click: function() { - var loc = window.location; - window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search; - } - }, _('To login…'))) - ]); + Promise.all([ + L.require('ui') + ]).then(function() { + Request.addInterceptor(function(res) { + if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes') + return; - return Promise.reject(new Error('Session expired')); - }); + Request.poll.stop(); + + L.ui.showModal(_('Session expired'), [ + E('div', { class: 'alert-message warning' }, + _('A new login is required since the authentication session expired.')), + E('div', { class: 'right' }, + E('div', { + class: 'btn primary', + click: function() { + var loc = window.location; + window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search; + } + }, _('To login…'))) + ]); + + return Promise.reject(new Error('Session expired')); + }); + + originalCBIInit(); + Request.poll.start(); - originalCBIInit(); - Request.poll.start(); + document.dispatchEvent(new CustomEvent('luci-loaded')); + }).catch(function(error) { + var msg = (error.stack || '').replace(/(.+?)@(.+):(\d+):(\d+)/g, + ' at $1 ($2:$3:$4)'); + + if (msg.indexOf(error.message) == -1) + msg = error.message + '\n' + msg; + + if (error.name && msg.indexOf(error.name) != 0) + msg = error.name + ': ' + msg; + + alert('LuCI class loading error:\n' + msg); + + if (window.console && console.debug) + console.debug(error); + }); }, env: {}, @@ -649,281 +653,10 @@ halt: function() { return Request.poll.stop() }, run: function() { return Request.poll.start() }, - - /* Modal dialog */ - showModal: function(title, children) { - var dlg = modalDiv.firstElementChild; - - dlg.setAttribute('class', 'modal'); - - this.dom.content(dlg, this.dom.create('h4', {}, title)); - this.dom.append(dlg, children); - - document.body.classList.add('modal-overlay-active'); - - return dlg; - }, - - hideModal: function() { - document.body.classList.remove('modal-overlay-active'); - }, - - - /* Tooltip */ - showTooltip: function(ev) { - var target = findParent(ev.target, '[data-tooltip]'); - - if (!target) - return; - - if (tooltipTimeout !== null) { - window.clearTimeout(tooltipTimeout); - tooltipTimeout = null; - } - - var rect = target.getBoundingClientRect(), - x = rect.left + window.pageXOffset, - y = rect.top + rect.height + window.pageYOffset; - - tooltipDiv.className = 'cbi-tooltip'; - tooltipDiv.innerHTML = '▲ '; - tooltipDiv.firstChild.data += target.getAttribute('data-tooltip'); - - if (target.hasAttribute('data-tooltip-style')) - tooltipDiv.classList.add(target.getAttribute('data-tooltip-style')); - - if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) { - y -= (tooltipDiv.offsetHeight + target.offsetHeight); - tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2); - } - - tooltipDiv.style.top = y + 'px'; - tooltipDiv.style.left = x + 'px'; - tooltipDiv.style.opacity = 1; - - tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', { - bubbles: true, - detail: { target: target } - })); - }, - - hideTooltip: function(ev) { - if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv || - tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget)) - return; - - if (tooltipTimeout !== null) { - window.clearTimeout(tooltipTimeout); - tooltipTimeout = null; - } - - tooltipDiv.style.opacity = 0; - tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250); - - tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true })); - }, - - - /* Widget helper */ - itemlist: function(node, items, separators) { - var children = []; - - if (!Array.isArray(separators)) - separators = [ separators || E('br') ]; - - for (var i = 0; i < items.length; i += 2) { - if (items[i+1] !== null && items[i+1] !== undefined) { - var sep = separators[(i/2) % separators.length], - cld = []; - - children.push(E('span', { class: 'nowrap' }, [ - items[i] ? E('strong', items[i] + ': ') : '', - items[i+1] - ])); - - if ((i+2) < items.length) - children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep); - } - } - - this.dom.content(node, children); - - return node; - }, - Class: Class, Request: Request }); - /* 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) { diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js new file mode 100644 index 0000000000..c69f1cb0ab --- /dev/null +++ b/modules/luci-base/htdocs/luci-static/resources/ui.js @@ -0,0 +1,296 @@ +var modalDiv = null, + tooltipDiv = null, + tooltipTimeout = null; + +return L.Class.extend({ + __init__: function() { + modalDiv = document.body.appendChild( + L.dom.create('div', { id: 'modal_overlay' }, + L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true }))); + + tooltipDiv = document.body.appendChild( + L.dom.create('div', { class: 'cbi-tooltip' })); + + /* setup old aliases */ + L.showModal = this.showModal; + L.hideModal = this.hideModal; + L.showTooltip = this.showTooltip; + L.hideTooltip = this.hideTooltip; + L.itemlist = this.itemlist; + + document.addEventListener('mouseover', this.showTooltip.bind(this), true); + 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('luci-loaded', this.tabs.init.bind(this.tabs)); + }, + + /* Modal dialog */ + showModal: function(title, children) { + var dlg = modalDiv.firstElementChild; + + dlg.setAttribute('class', 'modal'); + + L.dom.content(dlg, L.dom.create('h4', {}, title)); + L.dom.append(dlg, children); + + document.body.classList.add('modal-overlay-active'); + + return dlg; + }, + + hideModal: function() { + document.body.classList.remove('modal-overlay-active'); + }, + + /* Tooltip */ + showTooltip: function(ev) { + var target = findParent(ev.target, '[data-tooltip]'); + + if (!target) + return; + + if (tooltipTimeout !== null) { + window.clearTimeout(tooltipTimeout); + tooltipTimeout = null; + } + + var rect = target.getBoundingClientRect(), + x = rect.left + window.pageXOffset, + y = rect.top + rect.height + window.pageYOffset; + + tooltipDiv.className = 'cbi-tooltip'; + tooltipDiv.innerHTML = '▲ '; + tooltipDiv.firstChild.data += target.getAttribute('data-tooltip'); + + if (target.hasAttribute('data-tooltip-style')) + tooltipDiv.classList.add(target.getAttribute('data-tooltip-style')); + + if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) { + y -= (tooltipDiv.offsetHeight + target.offsetHeight); + tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2); + } + + tooltipDiv.style.top = y + 'px'; + tooltipDiv.style.left = x + 'px'; + tooltipDiv.style.opacity = 1; + + tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', { + bubbles: true, + detail: { target: target } + })); + }, + + hideTooltip: function(ev) { + if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv || + tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget)) + return; + + if (tooltipTimeout !== null) { + window.clearTimeout(tooltipTimeout); + tooltipTimeout = null; + } + + tooltipDiv.style.opacity = 0; + tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250); + + tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true })); + }, + + /* Widget helper */ + itemlist: function(node, items, separators) { + var children = []; + + if (!Array.isArray(separators)) + separators = [ separators || E('br') ]; + + for (var i = 0; i < items.length; i += 2) { + if (items[i+1] !== null && items[i+1] !== undefined) { + var sep = separators[(i/2) % separators.length], + cld = []; + + children.push(E('span', { class: 'nowrap' }, [ + items[i] ? E('strong', items[i] + ': ') : '', + items[i+1] + ])); + + if ((i+2) < items.length) + children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep); + } + } + + L.dom.content(node, children); + + return node; + }, + + /* Tabs */ + tabs: L.Class.singleton({ + 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 (typeof(panes) != 'object' || !('length' in 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.ui.tabs.setActiveTabId(groupId, index); + } + else { + pane.setAttribute('data-tab-active', 'false'); + } + + index++; + } + }); + } + }) +}); -- 2.30.2