dcda941f7bfab8f84ff04ea5ff07bc03b5a2ee9d
[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                 halt: function() { XHR.halt() },
78                 run: function() { XHR.run() },
79
80
81                 /* Modal dialog */
82                 showModal: function(title, children) {
83                         var dlg = modalDiv.firstElementChild;
84
85                         dlg.setAttribute('class', 'modal');
86
87                         this.dom.content(dlg, this.dom.create('h4', {}, title));
88                         this.dom.append(dlg, children);
89
90                         document.body.classList.add('modal-overlay-active');
91
92                         return dlg;
93                 },
94
95                 hideModal: function() {
96                         document.body.classList.remove('modal-overlay-active');
97                 },
98
99
100                 /* Tooltip */
101                 showTooltip: function(ev) {
102                         var target = findParent(ev.target, '[data-tooltip]');
103
104                         if (!target)
105                                 return;
106
107                         if (tooltipTimeout !== null) {
108                                 window.clearTimeout(tooltipTimeout);
109                                 tooltipTimeout = null;
110                         }
111
112                         var rect = target.getBoundingClientRect(),
113                             x = rect.left              + window.pageXOffset,
114                             y = rect.top + rect.height + window.pageYOffset;
115
116                         tooltipDiv.className = 'cbi-tooltip';
117                         tooltipDiv.innerHTML = '▲ ';
118                         tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
119
120                         if (target.hasAttribute('data-tooltip-style'))
121                                 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
122
123                         if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
124                                 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
125                                 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
126                         }
127
128                         tooltipDiv.style.top = y + 'px';
129                         tooltipDiv.style.left = x + 'px';
130                         tooltipDiv.style.opacity = 1;
131                 },
132
133                 hideTooltip: function(ev) {
134                         if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv)
135                                 return;
136
137                         if (tooltipTimeout !== null) {
138                                 window.clearTimeout(tooltipTimeout);
139                                 tooltipTimeout = null;
140                         }
141
142                         tooltipDiv.style.opacity = 0;
143                         tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
144                 },
145
146
147                 /* Widget helper */
148                 itemlist: function(node, items, separators) {
149                         var children = [];
150
151                         if (!Array.isArray(separators))
152                                 separators = [ separators || E('br') ];
153
154                         for (var i = 0; i < items.length; i += 2) {
155                                 if (items[i+1] !== null && items[i+1] !== undefined) {
156                                         var sep = separators[(i/2) % separators.length],
157                                             cld = [];
158
159                                         children.push(E('span', { class: 'nowrap' }, [
160                                                 items[i] ? E('strong', items[i] + ': ') : '',
161                                                 items[i+1]
162                                         ]));
163
164                                         if ((i+2) < items.length)
165                                                 children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep);
166                                 }
167                         }
168
169                         this.dom.content(node, children);
170
171                         return node;
172                 }
173         };
174
175         /* DOM manipulation */
176         LuCI.prototype.dom = {
177                 elem: function(e) {
178                         return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
179                 },
180
181                 parse: function(s) {
182                         var elem;
183
184                         try {
185                                 domParser = domParser || new DOMParser();
186                                 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
187                         }
188                         catch(e) {}
189
190                         if (!elem) {
191                                 try {
192                                         dummyElem = dummyElem || document.createElement('div');
193                                         dummyElem.innerHTML = s;
194                                         elem = dummyElem.firstChild;
195                                 }
196                                 catch (e) {}
197                         }
198
199                         return elem || null;
200                 },
201
202                 matches: function(node, selector) {
203                         var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
204                         return m ? m.call(node, selector) : false;
205                 },
206
207                 parent: function(node, selector) {
208                         if (this.elem(node) && node.closest)
209                                 return node.closest(selector);
210
211                         while (this.elem(node))
212                                 if (this.matches(node, selector))
213                                         return node;
214                                 else
215                                         node = node.parentNode;
216
217                         return null;
218                 },
219
220                 append: function(node, children) {
221                         if (!this.elem(node))
222                                 return null;
223
224                         if (Array.isArray(children)) {
225                                 for (var i = 0; i < children.length; i++)
226                                         if (this.elem(children[i]))
227                                                 node.appendChild(children[i]);
228                                         else if (children !== null && children !== undefined)
229                                                 node.appendChild(document.createTextNode('' + children[i]));
230
231                                 return node.lastChild;
232                         }
233                         else if (typeof(children) === 'function') {
234                                 return this.append(node, children(node));
235                         }
236                         else if (this.elem(children)) {
237                                 return node.appendChild(children);
238                         }
239                         else if (children !== null && children !== undefined) {
240                                 node.innerHTML = '' + children;
241                                 return node.lastChild;
242                         }
243
244                         return null;
245                 },
246
247                 content: function(node, children) {
248                         if (!this.elem(node))
249                                 return null;
250
251                         while (node.firstChild)
252                                 node.removeChild(node.firstChild);
253
254                         return this.append(node, children);
255                 },
256
257                 attr: function(node, key, val) {
258                         if (!this.elem(node))
259                                 return null;
260
261                         var attr = null;
262
263                         if (typeof(key) === 'object' && key !== null)
264                                 attr = key;
265                         else if (typeof(key) === 'string')
266                                 attr = {}, attr[key] = val;
267
268                         for (key in attr) {
269                                 if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
270                                         continue;
271
272                                 switch (typeof(attr[key])) {
273                                 case 'function':
274                                         node.addEventListener(key, attr[key]);
275                                         break;
276
277                                 case 'object':
278                                         node.setAttribute(key, JSON.stringify(attr[key]));
279                                         break;
280
281                                 default:
282                                         node.setAttribute(key, attr[key]);
283                                 }
284                         }
285                 },
286
287                 create: function() {
288                         var html = arguments[0],
289                             attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
290                             data = attr ? arguments[2] : arguments[1],
291                             elem;
292
293                         if (this.elem(html))
294                                 elem = html;
295                         else if (html.charCodeAt(0) === 60)
296                                 elem = this.parse(html);
297                         else
298                                 elem = document.createElement(html);
299
300                         if (!elem)
301                                 return null;
302
303                         this.attr(elem, attr);
304                         this.append(elem, data);
305
306                         return elem;
307                 }
308         };
309
310         function LuCI(env) {
311                 this.env = env;
312
313                 modalDiv = document.body.appendChild(this.dom.create('div', { id: 'modal_overlay' }, this.dom.create('div', { class: 'modal' })));
314                 tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' }));
315
316                 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
317                 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
318                 document.addEventListener('focus', this.showTooltip.bind(this), true);
319                 document.addEventListener('blur', this.hideTooltip.bind(this), true);
320         }
321
322         window.LuCI = LuCI;
323 })(window, document);