luci-base: move DOM manipulation functions to luci.js
[project/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);