luci-base: introduce common JavaScript api
authorJo-Philipp Wich <jo@mein.io>
Wed, 21 Nov 2018 17:44:59 +0000 (18:44 +0100)
committerJo-Philipp Wich <jo@mein.io>
Thu, 22 Nov 2018 11:49:14 +0000 (12:49 +0100)
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>
modules/luci-base/htdocs/luci-static/resources/luci.js [new file with mode: 0644]
modules/luci-base/luasrc/view/header.htm

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 (file)
index 0000000..cbf2246
--- /dev/null
@@ -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);
index f6e20c9a40b709ed737a88751ef27d4547ce8c66..2813c4d943e3af991903a932c55ad37152689a23 100644 (file)
                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>