Fix broken repository link in target/makeccs
[librecmc/librecmc.git] / package / luci / 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                         tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
134                                 bubbles: true,
135                                 detail: { target: target }
136                         }));
137                 },
138
139                 hideTooltip: function(ev) {
140                         if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
141                             tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
142                                 return;
143
144                         if (tooltipTimeout !== null) {
145                                 window.clearTimeout(tooltipTimeout);
146                                 tooltipTimeout = null;
147                         }
148
149                         tooltipDiv.style.opacity = 0;
150                         tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
151
152                         tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
153                 },
154
155
156                 /* Widget helper */
157                 itemlist: function(node, items, separators) {
158                         var children = [];
159
160                         if (!Array.isArray(separators))
161                                 separators = [ separators || E('br') ];
162
163                         for (var i = 0; i < items.length; i += 2) {
164                                 if (items[i+1] !== null && items[i+1] !== undefined) {
165                                         var sep = separators[(i/2) % separators.length],
166                                             cld = [];
167
168                                         children.push(E('span', { class: 'nowrap' }, [
169                                                 items[i] ? E('strong', items[i] + ': ') : '',
170                                                 items[i+1]
171                                         ]));
172
173                                         if ((i+2) < items.length)
174                                                 children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep);
175                                 }
176                         }
177
178                         this.dom.content(node, children);
179
180                         return node;
181                 }
182         };
183
184         /* Tabs */
185         LuCI.prototype.tabs = {
186                 init: function() {
187                         var groups = [], prevGroup = null, currGroup = null;
188
189                         document.querySelectorAll('[data-tab]').forEach(function(tab) {
190                                 var parent = tab.parentNode;
191
192                                 if (!parent.hasAttribute('data-tab-group'))
193                                         parent.setAttribute('data-tab-group', groups.length);
194
195                                 currGroup = +parent.getAttribute('data-tab-group');
196
197                                 if (currGroup !== prevGroup) {
198                                         prevGroup = currGroup;
199
200                                         if (!groups[currGroup])
201                                                 groups[currGroup] = [];
202                                 }
203
204                                 groups[currGroup].push(tab);
205                         });
206
207                         for (var i = 0; i < groups.length; i++)
208                                 this.initTabGroup(groups[i]);
209
210                         document.addEventListener('dependency-update', this.updateTabs.bind(this));
211
212                         this.updateTabs();
213
214                         if (!groups.length)
215                                 this.setActiveTabId(-1, -1);
216                 },
217
218                 initTabGroup: function(panes) {
219                         if (!Array.isArray(panes) || panes.length === 0)
220                                 return;
221
222                         var menu = E('ul', { 'class': 'cbi-tabmenu' }),
223                             group = panes[0].parentNode,
224                             groupId = +group.getAttribute('data-tab-group'),
225                             selected = null;
226
227                         for (var i = 0, pane; pane = panes[i]; i++) {
228                                 var name = pane.getAttribute('data-tab'),
229                                     title = pane.getAttribute('data-tab-title'),
230                                     active = pane.getAttribute('data-tab-active') === 'true';
231
232                                 menu.appendChild(E('li', {
233                                         'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
234                                         'data-tab': name
235                                 }, E('a', {
236                                         'href': '#',
237                                         'click': this.switchTab.bind(this)
238                                 }, title)));
239
240                                 if (active)
241                                         selected = i;
242                         }
243
244                         group.parentNode.insertBefore(menu, group);
245
246                         if (selected === null) {
247                                 selected = this.getActiveTabId(groupId);
248
249                                 if (selected < 0 || selected >= panes.length)
250                                         selected = 0;
251
252                                 menu.childNodes[selected].classList.add('cbi-tab');
253                                 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
254                                 panes[selected].setAttribute('data-tab-active', 'true');
255
256                                 this.setActiveTabId(groupId, selected);
257                         }
258                 },
259
260                 getActiveTabState: function() {
261                         var page = document.body.getAttribute('data-page');
262
263                         try {
264                                 var val = JSON.parse(window.sessionStorage.getItem('tab'));
265                                 if (val.page === page && Array.isArray(val.groups))
266                                         return val;
267                         }
268                         catch(e) {}
269
270                         window.sessionStorage.removeItem('tab');
271                         return { page: page, groups: [] };
272                 },
273
274                 getActiveTabId: function(groupId) {
275                         return +this.getActiveTabState().groups[groupId] || 0;
276                 },
277
278                 setActiveTabId: function(groupId, tabIndex) {
279                         try {
280                                 var state = this.getActiveTabState();
281                                     state.groups[groupId] = tabIndex;
282
283                             window.sessionStorage.setItem('tab', JSON.stringify(state));
284                         }
285                         catch (e) { return false; }
286
287                         return true;
288                 },
289
290                 updateTabs: function(ev) {
291                         document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
292                                 var menu = pane.parentNode.previousElementSibling,
293                                     tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
294                                     n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
295
296                                 if (!pane.firstElementChild) {
297                                         tab.style.display = 'none';
298                                         tab.classList.remove('flash');
299                                 }
300                                 else if (tab.style.display === 'none') {
301                                         tab.style.display = '';
302                                         requestAnimationFrame(function() { tab.classList.add('flash') });
303                                 }
304
305                                 if (n_errors) {
306                                         tab.setAttribute('data-errors', n_errors);
307                                         tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
308                                         tab.setAttribute('data-tooltip-style', 'error');
309                                 }
310                                 else {
311                                         tab.removeAttribute('data-errors');
312                                         tab.removeAttribute('data-tooltip');
313                                 }
314                         });
315                 },
316
317                 switchTab: function(ev) {
318                         var tab = ev.target.parentNode,
319                             name = tab.getAttribute('data-tab'),
320                             menu = tab.parentNode,
321                             group = menu.nextElementSibling,
322                             groupId = +group.getAttribute('data-tab-group'),
323                             index = 0;
324
325                         ev.preventDefault();
326
327                         if (!tab.classList.contains('cbi-tab-disabled'))
328                                 return;
329
330                         menu.querySelectorAll('[data-tab]').forEach(function(tab) {
331                                 tab.classList.remove('cbi-tab');
332                                 tab.classList.remove('cbi-tab-disabled');
333                                 tab.classList.add(
334                                         tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
335                         });
336
337                         group.childNodes.forEach(function(pane) {
338                                 if (L.dom.matches(pane, '[data-tab]')) {
339                                         if (pane.getAttribute('data-tab') === name) {
340                                                 pane.setAttribute('data-tab-active', 'true');
341                                                 L.tabs.setActiveTabId(groupId, index);
342                                         }
343                                         else {
344                                                 pane.setAttribute('data-tab-active', 'false');
345                                         }
346
347                                         index++;
348                                 }
349                         });
350                 }
351         };
352
353         /* DOM manipulation */
354         LuCI.prototype.dom = {
355                 elem: function(e) {
356                         return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
357                 },
358
359                 parse: function(s) {
360                         var elem;
361
362                         try {
363                                 domParser = domParser || new DOMParser();
364                                 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
365                         }
366                         catch(e) {}
367
368                         if (!elem) {
369                                 try {
370                                         dummyElem = dummyElem || document.createElement('div');
371                                         dummyElem.innerHTML = s;
372                                         elem = dummyElem.firstChild;
373                                 }
374                                 catch (e) {}
375                         }
376
377                         return elem || null;
378                 },
379
380                 matches: function(node, selector) {
381                         var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
382                         return m ? m.call(node, selector) : false;
383                 },
384
385                 parent: function(node, selector) {
386                         if (this.elem(node) && node.closest)
387                                 return node.closest(selector);
388
389                         while (this.elem(node))
390                                 if (this.matches(node, selector))
391                                         return node;
392                                 else
393                                         node = node.parentNode;
394
395                         return null;
396                 },
397
398                 append: function(node, children) {
399                         if (!this.elem(node))
400                                 return null;
401
402                         if (Array.isArray(children)) {
403                                 for (var i = 0; i < children.length; i++)
404                                         if (this.elem(children[i]))
405                                                 node.appendChild(children[i]);
406                                         else if (children !== null && children !== undefined)
407                                                 node.appendChild(document.createTextNode('' + children[i]));
408
409                                 return node.lastChild;
410                         }
411                         else if (typeof(children) === 'function') {
412                                 return this.append(node, children(node));
413                         }
414                         else if (this.elem(children)) {
415                                 return node.appendChild(children);
416                         }
417                         else if (children !== null && children !== undefined) {
418                                 node.innerHTML = '' + children;
419                                 return node.lastChild;
420                         }
421
422                         return null;
423                 },
424
425                 content: function(node, children) {
426                         if (!this.elem(node))
427                                 return null;
428
429                         while (node.firstChild)
430                                 node.removeChild(node.firstChild);
431
432                         return this.append(node, children);
433                 },
434
435                 attr: function(node, key, val) {
436                         if (!this.elem(node))
437                                 return null;
438
439                         var attr = null;
440
441                         if (typeof(key) === 'object' && key !== null)
442                                 attr = key;
443                         else if (typeof(key) === 'string')
444                                 attr = {}, attr[key] = val;
445
446                         for (key in attr) {
447                                 if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
448                                         continue;
449
450                                 switch (typeof(attr[key])) {
451                                 case 'function':
452                                         node.addEventListener(key, attr[key]);
453                                         break;
454
455                                 case 'object':
456                                         node.setAttribute(key, JSON.stringify(attr[key]));
457                                         break;
458
459                                 default:
460                                         node.setAttribute(key, attr[key]);
461                                 }
462                         }
463                 },
464
465                 create: function() {
466                         var html = arguments[0],
467                             attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
468                             data = attr ? arguments[2] : arguments[1],
469                             elem;
470
471                         if (this.elem(html))
472                                 elem = html;
473                         else if (html.charCodeAt(0) === 60)
474                                 elem = this.parse(html);
475                         else
476                                 elem = document.createElement(html);
477
478                         if (!elem)
479                                 return null;
480
481                         this.attr(elem, attr);
482                         this.append(elem, data);
483
484                         return elem;
485                 }
486         };
487
488         /* Setup */
489         LuCI.prototype.setupDOM = function(ev) {
490                 this.tabs.init();
491         };
492
493         function LuCI(env) {
494                 this.env = env;
495
496                 modalDiv = document.body.appendChild(
497                         this.dom.create('div', { id: 'modal_overlay' },
498                                 this.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
499
500                 tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' }));
501
502                 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
503                 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
504                 document.addEventListener('focus', this.showTooltip.bind(this), true);
505                 document.addEventListener('blur', this.hideTooltip.bind(this), true);
506
507                 document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
508         }
509
510         window.LuCI = LuCI;
511 })(window, document);