luci-base: luci.js: split ui helper functions into external ui.js
authorJo-Philipp Wich <jo@mein.io>
Mon, 7 Jan 2019 14:26:08 +0000 (15:26 +0100)
committerJo-Philipp Wich <jo@mein.io>
Sun, 7 Jul 2019 13:25:49 +0000 (15:25 +0200)
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 <jo@mein.io>
modules/luci-base/htdocs/luci-static/resources/luci.js
modules/luci-base/htdocs/luci-static/resources/ui.js [new file with mode: 0644]

index 610cbcb62a24aa3b9f5d229fe0d6b6573e3653e6..b86d499c6db4c6d001c896a3d91660506db2ac33 100644 (file)
        });
 
 
-       var modalDiv = null,
-           tooltipDiv = null,
-           tooltipTimeout = null,
-           dummyElem = null,
+       var dummyElem = null,
            domParser = null,
            originalCBIInit = null,
            classes = {};
                __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) {
 
                /* 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: {},
                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 (file)
index 0000000..c69f1cb
--- /dev/null
@@ -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++;
+                               }
+                       });
+               }
+       })
+});