1 (function(window, document, undefined) {
9 /* URL construction helpers */
10 path: function(prefix, parts) {
11 var url = [ prefix || '' ];
13 for (var i = 0; i < parts.length; i++)
14 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
15 url.push('/', parts[i]);
24 return this.path(this.env.scriptname, arguments);
27 resource: function() {
28 return this.path(this.env.resource, arguments);
31 location: function() {
32 return this.path(this.env.scriptname, this.env.requestpath);
36 /* HTTP resource fetching */
37 get: function(url, args, cb) {
38 return this.poll(0, url, args, cb, false);
41 post: function(url, args, cb) {
42 return this.poll(0, url, args, cb, true);
45 poll: function(interval, url, args, cb, post) {
46 var data = post ? { token: this.env.token } : null;
48 if (!/^(?:\/|\S+:\/\/)/.test(url))
51 if (typeof(args) === 'object' && args !== null) {
55 if (args.hasOwnProperty(key))
56 switch (typeof(args[key])) {
60 data[key] = args[key];
64 data[key] = JSON.stringify(args[key]);
70 return XHR.poll(interval, url, data, cb, post);
72 return XHR.post(url, data, cb);
74 return XHR.get(url, data, cb);
77 stop: function(entry) { XHR.stop(entry) },
78 halt: function() { XHR.halt() },
79 run: function() { XHR.run() },
83 showModal: function(title, children) {
84 var dlg = modalDiv.firstElementChild;
86 dlg.setAttribute('class', 'modal');
88 this.dom.content(dlg, this.dom.create('h4', {}, title));
89 this.dom.append(dlg, children);
91 document.body.classList.add('modal-overlay-active');
96 hideModal: function() {
97 document.body.classList.remove('modal-overlay-active');
102 showTooltip: function(ev) {
103 var target = findParent(ev.target, '[data-tooltip]');
108 if (tooltipTimeout !== null) {
109 window.clearTimeout(tooltipTimeout);
110 tooltipTimeout = null;
113 var rect = target.getBoundingClientRect(),
114 x = rect.left + window.pageXOffset,
115 y = rect.top + rect.height + window.pageYOffset;
117 tooltipDiv.className = 'cbi-tooltip';
118 tooltipDiv.innerHTML = '▲ ';
119 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
121 if (target.hasAttribute('data-tooltip-style'))
122 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
124 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
125 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
126 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
129 tooltipDiv.style.top = y + 'px';
130 tooltipDiv.style.left = x + 'px';
131 tooltipDiv.style.opacity = 1;
133 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
135 detail: { target: target }
139 hideTooltip: function(ev) {
140 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
141 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
144 if (tooltipTimeout !== null) {
145 window.clearTimeout(tooltipTimeout);
146 tooltipTimeout = null;
149 tooltipDiv.style.opacity = 0;
150 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
152 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
157 itemlist: function(node, items, separators) {
160 if (!Array.isArray(separators))
161 separators = [ separators || E('br') ];
163 for (var i = 0; i < items.length; i += 2) {
164 if (items[i+1] !== null && items[i+1] !== undefined) {
165 var sep = separators[(i/2) % separators.length],
168 children.push(E('span', { class: 'nowrap' }, [
169 items[i] ? E('strong', items[i] + ': ') : '',
173 if ((i+2) < items.length)
174 children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep);
178 this.dom.content(node, children);
185 LuCI.prototype.tabs = {
187 var groups = [], prevGroup = null, currGroup = null;
189 document.querySelectorAll('[data-tab]').forEach(function(tab) {
190 var parent = tab.parentNode;
192 if (!parent.hasAttribute('data-tab-group'))
193 parent.setAttribute('data-tab-group', groups.length);
195 currGroup = +parent.getAttribute('data-tab-group');
197 if (currGroup !== prevGroup) {
198 prevGroup = currGroup;
200 if (!groups[currGroup])
201 groups[currGroup] = [];
204 groups[currGroup].push(tab);
207 for (var i = 0; i < groups.length; i++)
208 this.initTabGroup(groups[i]);
210 document.addEventListener('dependency-update', this.updateTabs.bind(this));
215 this.setActiveTabId(-1, -1);
218 initTabGroup: function(panes) {
219 if (!Array.isArray(panes) || panes.length === 0)
222 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
223 group = panes[0].parentNode,
224 groupId = +group.getAttribute('data-tab-group'),
227 for (var i = 0, pane; pane = panes[i]; i++) {
228 var name = pane.getAttribute('data-tab'),
229 title = pane.getAttribute('data-tab-title'),
230 active = pane.getAttribute('data-tab-active') === 'true';
232 menu.appendChild(E('li', {
233 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
237 'click': this.switchTab.bind(this)
244 group.parentNode.insertBefore(menu, group);
246 if (selected === null) {
247 selected = this.getActiveTabId(groupId);
249 if (selected < 0 || selected >= panes.length)
252 menu.childNodes[selected].classList.add('cbi-tab');
253 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
254 panes[selected].setAttribute('data-tab-active', 'true');
256 this.setActiveTabId(groupId, selected);
260 getActiveTabState: function() {
261 var page = document.body.getAttribute('data-page');
264 var val = JSON.parse(window.sessionStorage.getItem('tab'));
265 if (val.page === page && Array.isArray(val.groups))
270 window.sessionStorage.removeItem('tab');
271 return { page: page, groups: [] };
274 getActiveTabId: function(groupId) {
275 return +this.getActiveTabState().groups[groupId] || 0;
278 setActiveTabId: function(groupId, tabIndex) {
280 var state = this.getActiveTabState();
281 state.groups[groupId] = tabIndex;
283 window.sessionStorage.setItem('tab', JSON.stringify(state));
285 catch (e) { return false; }
290 updateTabs: function(ev) {
291 document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
292 var menu = pane.parentNode.previousElementSibling,
293 tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
294 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
296 if (!pane.firstElementChild) {
297 tab.style.display = 'none';
298 tab.classList.remove('flash');
300 else if (tab.style.display === 'none') {
301 tab.style.display = '';
302 requestAnimationFrame(function() { tab.classList.add('flash') });
306 tab.setAttribute('data-errors', n_errors);
307 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
308 tab.setAttribute('data-tooltip-style', 'error');
311 tab.removeAttribute('data-errors');
312 tab.removeAttribute('data-tooltip');
317 switchTab: function(ev) {
318 var tab = ev.target.parentNode,
319 name = tab.getAttribute('data-tab'),
320 menu = tab.parentNode,
321 group = menu.nextElementSibling,
322 groupId = +group.getAttribute('data-tab-group'),
327 if (!tab.classList.contains('cbi-tab-disabled'))
330 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
331 tab.classList.remove('cbi-tab');
332 tab.classList.remove('cbi-tab-disabled');
334 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
337 group.childNodes.forEach(function(pane) {
338 if (L.dom.matches(pane, '[data-tab]')) {
339 if (pane.getAttribute('data-tab') === name) {
340 pane.setAttribute('data-tab-active', 'true');
341 L.tabs.setActiveTabId(groupId, index);
344 pane.setAttribute('data-tab-active', 'false');
353 /* DOM manipulation */
354 LuCI.prototype.dom = {
356 return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
363 domParser = domParser || new DOMParser();
364 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
370 dummyElem = dummyElem || document.createElement('div');
371 dummyElem.innerHTML = s;
372 elem = dummyElem.firstChild;
380 matches: function(node, selector) {
381 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
382 return m ? m.call(node, selector) : false;
385 parent: function(node, selector) {
386 if (this.elem(node) && node.closest)
387 return node.closest(selector);
389 while (this.elem(node))
390 if (this.matches(node, selector))
393 node = node.parentNode;
398 append: function(node, children) {
399 if (!this.elem(node))
402 if (Array.isArray(children)) {
403 for (var i = 0; i < children.length; i++)
404 if (this.elem(children[i]))
405 node.appendChild(children[i]);
406 else if (children !== null && children !== undefined)
407 node.appendChild(document.createTextNode('' + children[i]));
409 return node.lastChild;
411 else if (typeof(children) === 'function') {
412 return this.append(node, children(node));
414 else if (this.elem(children)) {
415 return node.appendChild(children);
417 else if (children !== null && children !== undefined) {
418 node.innerHTML = '' + children;
419 return node.lastChild;
425 content: function(node, children) {
426 if (!this.elem(node))
429 while (node.firstChild)
430 node.removeChild(node.firstChild);
432 return this.append(node, children);
435 attr: function(node, key, val) {
436 if (!this.elem(node))
441 if (typeof(key) === 'object' && key !== null)
443 else if (typeof(key) === 'string')
444 attr = {}, attr[key] = val;
447 if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
450 switch (typeof(attr[key])) {
452 node.addEventListener(key, attr[key]);
456 node.setAttribute(key, JSON.stringify(attr[key]));
460 node.setAttribute(key, attr[key]);
466 var html = arguments[0],
467 attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
468 data = attr ? arguments[2] : arguments[1],
473 else if (html.charCodeAt(0) === 60)
474 elem = this.parse(html);
476 elem = document.createElement(html);
481 this.attr(elem, attr);
482 this.append(elem, data);
489 LuCI.prototype.setupDOM = function(ev) {
496 modalDiv = document.body.appendChild(
497 this.dom.create('div', { id: 'modal_overlay' },
498 this.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
500 tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' }));
502 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
503 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
504 document.addEventListener('focus', this.showTooltip.bind(this), true);
505 document.addEventListener('blur', this.hideTooltip.bind(this), true);
507 document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
511 })(window, document);