From: Jo-Philipp Wich <jo@mein.io>
Date: Wed, 21 Nov 2018 17:44:59 +0000 (+0100)
Subject: luci-base: introduce common JavaScript api
X-Git-Url: http://git.cdn.openwrt.org/?a=commitdiff_plain;h=706c6836e40046ca82efc3355fc018fd37654d88;p=project%2Fluci.git

luci-base: introduce common JavaScript api

Introduce a new script file luci.js which is included by default and
intended to be the common location of functions currently scattered
in cbi.js and xhr.js.

The luci.js file provides a LuCI() class which - among other things -
implements helpers to construct URL paths and making HTTP requests.

A singleton instance of the class is instantiated as window.L upon
load and preset with the necessary environment information.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
---

diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
new file mode 100644
index 0000000000..cbf22460e4
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -0,0 +1,165 @@
+(function(window, document) {
+	var modalDiv = null,
+	    tooltipDiv = null,
+	    tooltipTimeout = 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);
+		},
+
+
+		/* Modal dialog */
+		showModal: function(title, children) {
+			var dlg = modalDiv.firstElementChild;
+
+			while (dlg.firstChild)
+				dlg.removeChild(dlg.firstChild);
+
+			dlg.setAttribute('class', 'modal');
+			dlg.appendChild(E('h4', {}, title));
+
+			if (!Array.isArray(children))
+				children = [ children ];
+
+			for (var i = 0; i < children.length; i++)
+				if (isElem(children[i]))
+					dlg.appendChild(children[i]);
+				else
+					dlg.appendChild(document.createTextNode('' + children[i]));
+
+			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;
+		},
+
+		hideTooltip: function(ev) {
+			if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv)
+				return;
+
+			if (tooltipTimeout !== null) {
+				window.clearTimeout(tooltipTimeout);
+				tooltipTimeout = null;
+			}
+
+			tooltipDiv.style.opacity = 0;
+			tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
+		}
+	};
+
+	function LuCI(env) {
+		this.env = env;
+
+		modalDiv = document.body.appendChild(E('div', { id: 'modal_overlay' }, E('div', { class: 'modal' })));
+		tooltipDiv = document.body.appendChild(E('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);
diff --git a/modules/luci-base/luasrc/view/header.htm b/modules/luci-base/luasrc/view/header.htm
index f6e20c9a40..2813c4d943 100644
--- a/modules/luci-base/luasrc/view/header.htm
+++ b/modules/luci-base/luasrc/view/header.htm
@@ -10,3 +10,14 @@
 		luci.dispatcher.context.template_header_sent = true
 	end
 %>
+
+<script type="text/javascript" src="<%=resource%>/luci.js"></script>
+<script type="text/javascript">
+	L = new LuCI(<%= luci.http.write_json({
+		token       = token,
+		resource    = resource,
+		scriptname  = luci.http.getenv("SCRIPT_NAME"),
+		pathinfo    = luci.http.getenv("PATH_INFO"),
+		requestpath = luci.dispatcher.context.requestpath
+	}) %>);
+</script>