luci-base: luci.js: split ui helper functions into external ui.js
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / ui.js
1 var modalDiv = null,
2     tooltipDiv = null,
3     tooltipTimeout = null;
4
5 return L.Class.extend({
6         __init__: function() {
7                 modalDiv = document.body.appendChild(
8                         L.dom.create('div', { id: 'modal_overlay' },
9                                 L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
10
11                 tooltipDiv = document.body.appendChild(
12                         L.dom.create('div', { class: 'cbi-tooltip' }));
13
14                 /* setup old aliases */
15                 L.showModal = this.showModal;
16                 L.hideModal = this.hideModal;
17                 L.showTooltip = this.showTooltip;
18                 L.hideTooltip = this.hideTooltip;
19                 L.itemlist = this.itemlist;
20
21                 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
22                 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
23                 document.addEventListener('focus', this.showTooltip.bind(this), true);
24                 document.addEventListener('blur', this.hideTooltip.bind(this), true);
25
26                 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
27         },
28
29         /* Modal dialog */
30         showModal: function(title, children) {
31                 var dlg = modalDiv.firstElementChild;
32
33                 dlg.setAttribute('class', 'modal');
34
35                 L.dom.content(dlg, L.dom.create('h4', {}, title));
36                 L.dom.append(dlg, children);
37
38                 document.body.classList.add('modal-overlay-active');
39
40                 return dlg;
41         },
42
43         hideModal: function() {
44                 document.body.classList.remove('modal-overlay-active');
45         },
46
47         /* Tooltip */
48         showTooltip: function(ev) {
49                 var target = findParent(ev.target, '[data-tooltip]');
50
51                 if (!target)
52                         return;
53
54                 if (tooltipTimeout !== null) {
55                         window.clearTimeout(tooltipTimeout);
56                         tooltipTimeout = null;
57                 }
58
59                 var rect = target.getBoundingClientRect(),
60                     x = rect.left              + window.pageXOffset,
61                     y = rect.top + rect.height + window.pageYOffset;
62
63                 tooltipDiv.className = 'cbi-tooltip';
64                 tooltipDiv.innerHTML = '▲ ';
65                 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
66
67                 if (target.hasAttribute('data-tooltip-style'))
68                         tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
69
70                 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
71                         y -= (tooltipDiv.offsetHeight + target.offsetHeight);
72                         tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
73                 }
74
75                 tooltipDiv.style.top = y + 'px';
76                 tooltipDiv.style.left = x + 'px';
77                 tooltipDiv.style.opacity = 1;
78
79                 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
80                         bubbles: true,
81                         detail: { target: target }
82                 }));
83         },
84
85         hideTooltip: function(ev) {
86                 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
87                     tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
88                         return;
89
90                 if (tooltipTimeout !== null) {
91                         window.clearTimeout(tooltipTimeout);
92                         tooltipTimeout = null;
93                 }
94
95                 tooltipDiv.style.opacity = 0;
96                 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
97
98                 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
99         },
100
101         /* Widget helper */
102         itemlist: function(node, items, separators) {
103                 var children = [];
104
105                 if (!Array.isArray(separators))
106                         separators = [ separators || E('br') ];
107
108                 for (var i = 0; i < items.length; i += 2) {
109                         if (items[i+1] !== null && items[i+1] !== undefined) {
110                                 var sep = separators[(i/2) % separators.length],
111                                     cld = [];
112
113                                 children.push(E('span', { class: 'nowrap' }, [
114                                         items[i] ? E('strong', items[i] + ': ') : '',
115                                         items[i+1]
116                                 ]));
117
118                                 if ((i+2) < items.length)
119                                         children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
120                         }
121                 }
122
123                 L.dom.content(node, children);
124
125                 return node;
126         },
127
128         /* Tabs */
129         tabs: L.Class.singleton({
130                 init: function() {
131                         var groups = [], prevGroup = null, currGroup = null;
132
133                         document.querySelectorAll('[data-tab]').forEach(function(tab) {
134                                 var parent = tab.parentNode;
135
136                                 if (!parent.hasAttribute('data-tab-group'))
137                                         parent.setAttribute('data-tab-group', groups.length);
138
139                                 currGroup = +parent.getAttribute('data-tab-group');
140
141                                 if (currGroup !== prevGroup) {
142                                         prevGroup = currGroup;
143
144                                         if (!groups[currGroup])
145                                                 groups[currGroup] = [];
146                                 }
147
148                                 groups[currGroup].push(tab);
149                         });
150
151                         for (var i = 0; i < groups.length; i++)
152                                 this.initTabGroup(groups[i]);
153
154                         document.addEventListener('dependency-update', this.updateTabs.bind(this));
155
156                         this.updateTabs();
157
158                         if (!groups.length)
159                                 this.setActiveTabId(-1, -1);
160                 },
161
162                 initTabGroup: function(panes) {
163                         if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
164                                 return;
165
166                         var menu = E('ul', { 'class': 'cbi-tabmenu' }),
167                             group = panes[0].parentNode,
168                             groupId = +group.getAttribute('data-tab-group'),
169                             selected = null;
170
171                         for (var i = 0, pane; pane = panes[i]; i++) {
172                                 var name = pane.getAttribute('data-tab'),
173                                     title = pane.getAttribute('data-tab-title'),
174                                     active = pane.getAttribute('data-tab-active') === 'true';
175
176                                 menu.appendChild(E('li', {
177                                         'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
178                                         'data-tab': name
179                                 }, E('a', {
180                                         'href': '#',
181                                         'click': this.switchTab.bind(this)
182                                 }, title)));
183
184                                 if (active)
185                                         selected = i;
186                         }
187
188                         group.parentNode.insertBefore(menu, group);
189
190                         if (selected === null) {
191                                 selected = this.getActiveTabId(groupId);
192
193                                 if (selected < 0 || selected >= panes.length)
194                                         selected = 0;
195
196                                 menu.childNodes[selected].classList.add('cbi-tab');
197                                 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
198                                 panes[selected].setAttribute('data-tab-active', 'true');
199
200                                 this.setActiveTabId(groupId, selected);
201                         }
202                 },
203
204                 getActiveTabState: function() {
205                         var page = document.body.getAttribute('data-page');
206
207                         try {
208                                 var val = JSON.parse(window.sessionStorage.getItem('tab'));
209                                 if (val.page === page && Array.isArray(val.groups))
210                                         return val;
211                         }
212                         catch(e) {}
213
214                         window.sessionStorage.removeItem('tab');
215                         return { page: page, groups: [] };
216                 },
217
218                 getActiveTabId: function(groupId) {
219                         return +this.getActiveTabState().groups[groupId] || 0;
220                 },
221
222                 setActiveTabId: function(groupId, tabIndex) {
223                         try {
224                                 var state = this.getActiveTabState();
225                                     state.groups[groupId] = tabIndex;
226
227                             window.sessionStorage.setItem('tab', JSON.stringify(state));
228                         }
229                         catch (e) { return false; }
230
231                         return true;
232                 },
233
234                 updateTabs: function(ev) {
235                         document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
236                                 var menu = pane.parentNode.previousElementSibling,
237                                     tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
238                                     n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
239
240                                 if (!pane.firstElementChild) {
241                                         tab.style.display = 'none';
242                                         tab.classList.remove('flash');
243                                 }
244                                 else if (tab.style.display === 'none') {
245                                         tab.style.display = '';
246                                         requestAnimationFrame(function() { tab.classList.add('flash') });
247                                 }
248
249                                 if (n_errors) {
250                                         tab.setAttribute('data-errors', n_errors);
251                                         tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
252                                         tab.setAttribute('data-tooltip-style', 'error');
253                                 }
254                                 else {
255                                         tab.removeAttribute('data-errors');
256                                         tab.removeAttribute('data-tooltip');
257                                 }
258                         });
259                 },
260
261                 switchTab: function(ev) {
262                         var tab = ev.target.parentNode,
263                             name = tab.getAttribute('data-tab'),
264                             menu = tab.parentNode,
265                             group = menu.nextElementSibling,
266                             groupId = +group.getAttribute('data-tab-group'),
267                             index = 0;
268
269                         ev.preventDefault();
270
271                         if (!tab.classList.contains('cbi-tab-disabled'))
272                                 return;
273
274                         menu.querySelectorAll('[data-tab]').forEach(function(tab) {
275                                 tab.classList.remove('cbi-tab');
276                                 tab.classList.remove('cbi-tab-disabled');
277                                 tab.classList.add(
278                                         tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
279                         });
280
281                         group.childNodes.forEach(function(pane) {
282                                 if (L.dom.matches(pane, '[data-tab]')) {
283                                         if (pane.getAttribute('data-tab') === name) {
284                                                 pane.setAttribute('data-tab-active', 'true');
285                                                 L.ui.tabs.setActiveTabId(groupId, index);
286                                         }
287                                         else {
288                                                 pane.setAttribute('data-tab-active', 'false');
289                                         }
290
291                                         index++;
292                                 }
293                         });
294                 }
295         })
296 });