luci-base: modal accessibility fix, wrap XHR.stop()
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / luci.js
1 (function(window, document, undefined) {
2         var modalDiv = null,
3             tooltipDiv = null,
4             tooltipTimeout = null,
5             dummyElem = null,
6             domParser = null;
7
8         LuCI.prototype = {
9                 /* URL construction helpers */
10                 path: function(prefix, parts) {
11                         var url = [ prefix || '' ];
12
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]);
16
17                         if (url.length === 1)
18                                 url.push('/');
19
20                         return url.join('');
21                 },
22
23                 url: function() {
24                         return this.path(this.env.scriptname, arguments);
25                 },
26
27                 resource: function() {
28                         return this.path(this.env.resource, arguments);
29                 },
30
31                 location: function() {
32                         return this.path(this.env.scriptname, this.env.requestpath);
33                 },
34
35
36                 /* HTTP resource fetching */
37                 get: function(url, args, cb) {
38                         return this.poll(0, url, args, cb, false);
39                 },
40
41                 post: function(url, args, cb) {
42                         return this.poll(0, url, args, cb, true);
43                 },
44
45                 poll: function(interval, url, args, cb, post) {
46                         var data = post ? { token: this.env.token } : null;
47
48                         if (!/^(?:\/|\S+:\/\/)/.test(url))
49                                 url = this.url(url);
50
51                         if (typeof(args) === 'object' && args !== null) {
52                                 data = data || {};
53
54                                 for (var key in args)
55                                         if (args.hasOwnProperty(key))
56                                                 switch (typeof(args[key])) {
57                                                 case 'string':
58                                                 case 'number':
59                                                 case 'boolean':
60                                                         data[key] = args[key];
61                                                         break;
62
63                                                 case 'object':
64                                                         data[key] = JSON.stringify(args[key]);
65                                                         break;
66                                                 }
67                         }
68
69                         if (interval > 0)
70                                 return XHR.poll(interval, url, data, cb, post);
71                         else if (post)
72                                 return XHR.post(url, data, cb);
73                         else
74                                 return XHR.get(url, data, cb);
75                 },
76
77                 stop: function(entry) { XHR.stop(entry) },
78                 halt: function() { XHR.halt() },
79                 run: function() { XHR.run() },
80
81
82                 /* Modal dialog */
83                 showModal: function(title, children) {
84                         var dlg = modalDiv.firstElementChild;
85
86                         dlg.setAttribute('class', 'modal');
87
88                         this.dom.content(dlg, this.dom.create('h4', {}, title));
89                         this.dom.append(dlg, children);
90
91                         document.body.classList.add('modal-overlay-active');
92
93                         return dlg;
94                 },
95
96                 hideModal: function() {
97                         document.body.classList.remove('modal-overlay-active');
98                 },
99
100
101                 /* Tooltip */
102                 showTooltip: function(ev) {
103                         var target = findParent(ev.target, '[data-tooltip]');
104
105                         if (!target)
106                                 return;
107
108                         if (tooltipTimeout !== null) {
109                                 window.clearTimeout(tooltipTimeout);
110                                 tooltipTimeout = null;
111                         }
112
113                         var rect = target.getBoundingClientRect(),
114                             x = rect.left              + window.pageXOffset,
115                             y = rect.top + rect.height + window.pageYOffset;
116
117                         tooltipDiv.className = 'cbi-tooltip';
118                         tooltipDiv.innerHTML = '▲ ';
119                         tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
120
121                         if (target.hasAttribute('data-tooltip-style'))
122                                 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
123
124                         if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
125                                 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
126                                 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
127                         }
128
129                         tooltipDiv.style.top = y + 'px';
130                         tooltipDiv.style.left = x + 'px';
131                         tooltipDiv.style.opacity = 1;
132                 },
133
134                 hideTooltip: function(ev) {
135                         if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv)
136                                 return;
137
138                         if (tooltipTimeout !== null) {
139                                 window.clearTimeout(tooltipTimeout);
140                                 tooltipTimeout = null;
141                         }
142
143                         tooltipDiv.style.opacity = 0;
144                         tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
145                 },
146
147
148                 /* Widget helper */
149                 itemlist: function(node, items, separators) {
150                         var children = [];
151
152                         if (!Array.isArray(separators))
153                                 separators = [ separators || E('br') ];
154
155                         for (var i = 0; i < items.length; i += 2) {
156                                 if (items[i+1] !== null && items[i+1] !== undefined) {
157                                         var sep = separators[(i/2) % separators.length],
158                                             cld = [];
159
160                                         children.push(E('span', { class: 'nowrap' }, [
161                                                 items[i] ? E('strong', items[i] + ': ') : '',
162                                                 items[i+1]
163                                         ]));
164
165                                         if ((i+2) < items.length)
166                                                 children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep);
167                                 }
168                         }
169
170                         this.dom.content(node, children);
171
172                         return node;
173                 }
174         };
175
176         /* DOM manipulation */
177         LuCI.prototype.dom = {
178                 elem: function(e) {
179                         return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
180                 },
181
182                 parse: function(s) {
183                         var elem;
184
185                         try {
186                                 domParser = domParser || new DOMParser();
187                                 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
188                         }
189                         catch(e) {}
190
191                         if (!elem) {
192                                 try {
193                                         dummyElem = dummyElem || document.createElement('div');
194                                         dummyElem.innerHTML = s;
195                                         elem = dummyElem.firstChild;
196                                 }
197                                 catch (e) {}
198                         }
199
200                         return elem || null;
201                 },
202
203                 matches: function(node, selector) {
204                         var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
205                         return m ? m.call(node, selector) : false;
206                 },
207
208                 parent: function(node, selector) {
209                         if (this.elem(node) && node.closest)
210                                 return node.closest(selector);
211
212                         while (this.elem(node))
213                                 if (this.matches(node, selector))
214                                         return node;
215                                 else
216                                         node = node.parentNode;
217
218                         return null;
219                 },
220
221                 append: function(node, children) {
222                         if (!this.elem(node))
223                                 return null;
224
225                         if (Array.isArray(children)) {
226                                 for (var i = 0; i < children.length; i++)
227                                         if (this.elem(children[i]))
228                                                 node.appendChild(children[i]);
229                                         else if (children !== null && children !== undefined)
230                                                 node.appendChild(document.createTextNode('' + children[i]));
231
232                                 return node.lastChild;
233                         }
234                         else if (typeof(children) === 'function') {
235                                 return this.append(node, children(node));
236                         }
237                         else if (this.elem(children)) {
238                                 return node.appendChild(children);
239                         }
240                         else if (children !== null && children !== undefined) {
241                                 node.innerHTML = '' + children;
242                                 return node.lastChild;
243                         }
244
245                         return null;
246                 },
247
248                 content: function(node, children) {
249                         if (!this.elem(node))
250                                 return null;
251
252                         while (node.firstChild)
253                                 node.removeChild(node.firstChild);
254
255                         return this.append(node, children);
256                 },
257
258                 attr: function(node, key, val) {
259                         if (!this.elem(node))
260                                 return null;
261
262                         var attr = null;
263
264                         if (typeof(key) === 'object' && key !== null)
265                                 attr = key;
266                         else if (typeof(key) === 'string')
267                                 attr = {}, attr[key] = val;
268
269                         for (key in attr) {
270                                 if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
271                                         continue;
272
273                                 switch (typeof(attr[key])) {
274                                 case 'function':
275                                         node.addEventListener(key, attr[key]);
276                                         break;
277
278                                 case 'object':
279                                         node.setAttribute(key, JSON.stringify(attr[key]));
280                                         break;
281
282                                 default:
283                                         node.setAttribute(key, attr[key]);
284                                 }
285                         }
286                 },
287
288                 create: function() {
289                         var html = arguments[0],
290                             attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
291                             data = attr ? arguments[2] : arguments[1],
292                             elem;
293
294                         if (this.elem(html))
295                                 elem = html;
296                         else if (html.charCodeAt(0) === 60)
297                                 elem = this.parse(html);
298                         else
299                                 elem = document.createElement(html);
300
301                         if (!elem)
302                                 return null;
303
304                         this.attr(elem, attr);
305                         this.append(elem, data);
306
307                         return elem;
308                 }
309         };
310
311         function LuCI(env) {
312                 this.env = env;
313
314                 modalDiv = document.body.appendChild(
315                         this.dom.create('div', { id: 'modal_overlay' },
316                                 this.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
317
318                 tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' }));
319
320                 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
321                 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
322                 document.addEventListener('focus', this.showTooltip.bind(this), true);
323                 document.addEventListener('blur', this.hideTooltip.bind(this), true);
324         }
325
326         window.LuCI = LuCI;
327 })(window, document);