The new `tooltip-open` and `tooltip-close` events allow other code to hook into the tooltip div rendering, e.g. to populate it with custom markup. Also ignore tooltip events originating from descendant elements while we're at it. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
335 lines
8 KiB
JavaScript
335 lines
8 KiB
JavaScript
(function(window, document, undefined) {
|
|
var modalDiv = null,
|
|
tooltipDiv = null,
|
|
tooltipTimeout = null,
|
|
dummyElem = null,
|
|
domParser = null;
|
|
|
|
LuCI.prototype = {
|
|
/* URL construction helpers */
|
|
path: function(prefix, parts) {
|
|
var url = [ prefix || '' ];
|
|
|
|
for (var i = 0; i < parts.length; i++)
|
|
if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
|
|
url.push('/', parts[i]);
|
|
|
|
if (url.length === 1)
|
|
url.push('/');
|
|
|
|
return url.join('');
|
|
},
|
|
|
|
url: function() {
|
|
return this.path(this.env.scriptname, arguments);
|
|
},
|
|
|
|
resource: function() {
|
|
return this.path(this.env.resource, arguments);
|
|
},
|
|
|
|
location: function() {
|
|
return this.path(this.env.scriptname, this.env.requestpath);
|
|
},
|
|
|
|
|
|
/* HTTP resource fetching */
|
|
get: function(url, args, cb) {
|
|
return this.poll(0, url, args, cb, false);
|
|
},
|
|
|
|
post: function(url, args, cb) {
|
|
return this.poll(0, url, args, cb, true);
|
|
},
|
|
|
|
poll: function(interval, url, args, cb, post) {
|
|
var data = post ? { token: this.env.token } : null;
|
|
|
|
if (!/^(?:\/|\S+:\/\/)/.test(url))
|
|
url = this.url(url);
|
|
|
|
if (typeof(args) === 'object' && args !== null) {
|
|
data = data || {};
|
|
|
|
for (var key in args)
|
|
if (args.hasOwnProperty(key))
|
|
switch (typeof(args[key])) {
|
|
case 'string':
|
|
case 'number':
|
|
case 'boolean':
|
|
data[key] = args[key];
|
|
break;
|
|
|
|
case 'object':
|
|
data[key] = JSON.stringify(args[key]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (interval > 0)
|
|
return XHR.poll(interval, url, data, cb, post);
|
|
else if (post)
|
|
return XHR.post(url, data, cb);
|
|
else
|
|
return XHR.get(url, data, cb);
|
|
},
|
|
|
|
stop: function(entry) { XHR.stop(entry) },
|
|
halt: function() { XHR.halt() },
|
|
run: function() { XHR.run() },
|
|
|
|
|
|
/* 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;
|
|
}
|
|
};
|
|
|
|
/* DOM manipulation */
|
|
LuCI.prototype.dom = {
|
|
elem: function(e) {
|
|
return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
|
|
},
|
|
|
|
parse: function(s) {
|
|
var elem;
|
|
|
|
try {
|
|
domParser = domParser || new DOMParser();
|
|
elem = domParser.parseFromString(s, 'text/html').body.firstChild;
|
|
}
|
|
catch(e) {}
|
|
|
|
if (!elem) {
|
|
try {
|
|
dummyElem = dummyElem || document.createElement('div');
|
|
dummyElem.innerHTML = s;
|
|
elem = dummyElem.firstChild;
|
|
}
|
|
catch (e) {}
|
|
}
|
|
|
|
return elem || null;
|
|
},
|
|
|
|
matches: function(node, selector) {
|
|
var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
|
|
return m ? m.call(node, selector) : false;
|
|
},
|
|
|
|
parent: function(node, selector) {
|
|
if (this.elem(node) && node.closest)
|
|
return node.closest(selector);
|
|
|
|
while (this.elem(node))
|
|
if (this.matches(node, selector))
|
|
return node;
|
|
else
|
|
node = node.parentNode;
|
|
|
|
return null;
|
|
},
|
|
|
|
append: function(node, children) {
|
|
if (!this.elem(node))
|
|
return null;
|
|
|
|
if (Array.isArray(children)) {
|
|
for (var i = 0; i < children.length; i++)
|
|
if (this.elem(children[i]))
|
|
node.appendChild(children[i]);
|
|
else if (children !== null && children !== undefined)
|
|
node.appendChild(document.createTextNode('' + children[i]));
|
|
|
|
return node.lastChild;
|
|
}
|
|
else if (typeof(children) === 'function') {
|
|
return this.append(node, children(node));
|
|
}
|
|
else if (this.elem(children)) {
|
|
return node.appendChild(children);
|
|
}
|
|
else if (children !== null && children !== undefined) {
|
|
node.innerHTML = '' + children;
|
|
return node.lastChild;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
content: function(node, children) {
|
|
if (!this.elem(node))
|
|
return null;
|
|
|
|
while (node.firstChild)
|
|
node.removeChild(node.firstChild);
|
|
|
|
return this.append(node, children);
|
|
},
|
|
|
|
attr: function(node, key, val) {
|
|
if (!this.elem(node))
|
|
return null;
|
|
|
|
var attr = null;
|
|
|
|
if (typeof(key) === 'object' && key !== null)
|
|
attr = key;
|
|
else if (typeof(key) === 'string')
|
|
attr = {}, attr[key] = val;
|
|
|
|
for (key in attr) {
|
|
if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
|
|
continue;
|
|
|
|
switch (typeof(attr[key])) {
|
|
case 'function':
|
|
node.addEventListener(key, attr[key]);
|
|
break;
|
|
|
|
case 'object':
|
|
node.setAttribute(key, JSON.stringify(attr[key]));
|
|
break;
|
|
|
|
default:
|
|
node.setAttribute(key, attr[key]);
|
|
}
|
|
}
|
|
},
|
|
|
|
create: function() {
|
|
var html = arguments[0],
|
|
attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
|
|
data = attr ? arguments[2] : arguments[1],
|
|
elem;
|
|
|
|
if (this.elem(html))
|
|
elem = html;
|
|
else if (html.charCodeAt(0) === 60)
|
|
elem = this.parse(html);
|
|
else
|
|
elem = document.createElement(html);
|
|
|
|
if (!elem)
|
|
return null;
|
|
|
|
this.attr(elem, attr);
|
|
this.append(elem, data);
|
|
|
|
return elem;
|
|
}
|
|
};
|
|
|
|
function LuCI(env) {
|
|
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);
|
|
}
|
|
|
|
window.LuCI = LuCI;
|
|
})(window, document);
|