cab400d7930ba97932fdddf6b23e3b96463dfaa6
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLab / WCF / Ui / Page / Menu / Abstract.js
1 /**
2 * Provides a touch-friendly fullscreen menu.
3 *
4 * @author Alexander Ebert
5 * @copyright 2001-2016 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLab/WCF/Ui/Page/Menu/Abstract
8 */
9 define(['Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', 'Ui/Screen'], function(Environment, EventHandler, ObjectMap, DomTraverse, DomUtil, UiScreen) {
10 "use strict";
11
12 var _pageContainer = elById('pageContainer');
13
14 /**
15 * @param {string} eventIdentifier event namespace
16 * @param {string} elementId menu element id
17 * @param {string} buttonSelector CSS selector for toggle button
18 * @constructor
19 */
20 function UiPageMenuAbstract(eventIdentifier, elementId, buttonSelector) { this.init(eventIdentifier, elementId, buttonSelector); }
21 UiPageMenuAbstract.prototype = {
22 /**
23 * Initializes a touch-friendly fullscreen menu.
24 *
25 * @param {string} eventIdentifier event namespace
26 * @param {string} elementId menu element id
27 * @param {string} buttonSelector CSS selector for toggle button
28 */
29 init: function(eventIdentifier, elementId, buttonSelector) {
30 this._activeList = [];
31 this._depth = 0;
32 this._enabled = true;
33 this._eventIdentifier = eventIdentifier;
34 this._items = new ObjectMap();
35 this._menu = elById(elementId);
36 this._removeActiveList = false;
37
38 var callbackOpen = this.open.bind(this);
39 var button = elBySel(buttonSelector);
40 button.addEventListener(WCF_CLICK_EVENT, callbackOpen);
41
42 this._initItems();
43 this._initHeader();
44
45 EventHandler.add(this._eventIdentifier, 'open', callbackOpen);
46 EventHandler.add(this._eventIdentifier, 'close', this.close.bind(this));
47
48 var itemList, itemLists = elByClass('menuOverlayItemList', this._menu);
49 this._menu.addEventListener('animationend', (function() {
50 if (!this._menu.classList.contains('open')) {
51 for (var i = 0, length = itemLists.length; i < length; i++) {
52 itemList = itemLists[i];
53
54 // force the main list to be displayed
55 itemList.classList.remove('active');
56 itemList.classList.remove('hidden');
57 }
58 }
59 }).bind(this));
60
61 this._menu.children[0].addEventListener('transitionend', (function() {
62 this._menu.classList.add('allowScroll');
63
64 if (this._removeActiveList) {
65 this._removeActiveList = false;
66
67 var list = this._activeList.pop();
68 if (list) {
69 list.classList.remove('activeList');
70 }
71 }
72 }).bind(this));
73
74 var backdrop = elCreate('div');
75 backdrop.className = 'menuOverlayMobileBackdrop';
76 backdrop.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
77
78 DomUtil.insertAfter(backdrop, this._menu);
79 },
80
81 /**
82 * Opens the menu.
83 *
84 * @param {Event} event event object
85 */
86 open: function(event) {
87 if (!this._enabled) {
88 return;
89 }
90
91 if (event instanceof Event) {
92 event.preventDefault();
93 }
94
95 this._menu.classList.add('open');
96 this._menu.classList.add('allowScroll');
97 this._menu.children[0].classList.add('activeList');
98
99 UiScreen.scrollDisable();
100
101 _pageContainer.classList.add('menuOverlay-' + this._menu.id);
102
103 document.documentElement.classList.add('pageOverlayActive');
104 },
105
106 /**
107 * Closes the menu.
108 *
109 * @param {(Event|boolean)} event event object or boolean true to force close the menu
110 */
111 close: function(event) {
112 if (event instanceof Event) {
113 event.preventDefault();
114 }
115
116 if (this._menu.classList.contains('open')) {
117 this._menu.classList.remove('open');
118
119 UiScreen.scrollEnable();
120
121 _pageContainer.classList.remove('menuOverlay-' + this._menu.id);
122
123 document.documentElement.classList.remove('pageOverlayActive');
124 }
125 },
126
127 /**
128 * Enables the touch menu.
129 */
130 enable: function() {
131 this._enabled = true;
132 },
133
134 /**
135 * Disables the touch menu.
136 */
137 disable: function() {
138 this._enabled = false;
139
140 this.close(true);
141 },
142
143 /**
144 * Initializes all menu items.
145 *
146 * @protected
147 */
148 _initItems: function() {
149 elBySelAll('.menuOverlayItemLink', this._menu, this._initItem.bind(this));
150 },
151
152 /**
153 * Initializes a single menu item.
154 *
155 * @param {Element} item menu item
156 * @protected
157 */
158 _initItem: function(item) {
159 // check if it should contain a 'more' link w/ an external callback
160 var parent = item.parentNode;
161 var more = elData(parent, 'more');
162 if (more) {
163 item.addEventListener(WCF_CLICK_EVENT, (function(event) {
164 event.preventDefault();
165 event.stopPropagation();
166
167 EventHandler.fire(this._eventIdentifier, 'more', {
168 handler: this,
169 identifier: more,
170 item: item,
171 parent: parent
172 });
173 }).bind(this));
174
175 return;
176 }
177
178 var itemList = item.nextElementSibling, wrapper;
179 if (itemList === null) {
180 return;
181 }
182
183 // handle static items with an icon-type button next to it (acp menu)
184 if (itemList.nodeName !== 'OL' && itemList.classList.contains('menuOverlayItemLinkIcon')) {
185 // add wrapper
186 wrapper = elCreate('span');
187 wrapper.className = 'menuOverlayItemWrapper';
188 parent.insertBefore(wrapper, item);
189 wrapper.appendChild(item);
190
191 while (wrapper.nextElementSibling) {
192 wrapper.appendChild(wrapper.nextElementSibling);
193 }
194
195 return;
196 }
197
198 var isLink = (elAttr(item, 'href') !== '#');
199 var parentItemList = parent.parentNode;
200 var itemTitle = elData(itemList, 'title');
201
202 this._items.set(item, {
203 itemList: itemList,
204 parentItemList: parentItemList
205 });
206
207 if (itemTitle === '') {
208 itemTitle = DomTraverse.childByClass(item, 'menuOverlayItemTitle').textContent;
209 elData(itemList, 'title', itemTitle);
210 }
211
212 var callbackLink = this._showItemList.bind(this, item);
213 if (isLink) {
214 wrapper = elCreate('span');
215 wrapper.className = 'menuOverlayItemWrapper';
216 parent.insertBefore(wrapper, item);
217 wrapper.appendChild(item);
218
219 var moreLink = elCreate('a');
220 elAttr(moreLink, 'href', '#');
221 moreLink.className = 'menuOverlayItemLinkIcon' + (item.classList.contains('active') ? ' active' : '');
222 moreLink.innerHTML = '<span class="icon icon24 fa-angle-right"></span>';
223 moreLink.addEventListener(WCF_CLICK_EVENT, callbackLink);
224 wrapper.appendChild(moreLink);
225 }
226 else {
227 item.classList.add('menuOverlayItemLinkMore');
228 item.addEventListener(WCF_CLICK_EVENT, callbackLink);
229 }
230
231 var backLinkItem = elCreate('li');
232 backLinkItem.className = 'menuOverlayHeader';
233
234 wrapper = elCreate('span');
235 wrapper.className = 'menuOverlayItemWrapper';
236
237 var backLink = elCreate('a');
238 elAttr(backLink, 'href', '#');
239 backLink.className = 'menuOverlayItemLink menuOverlayBackLink';
240 backLink.textContent = elData(parentItemList, 'title');
241 backLink.addEventListener(WCF_CLICK_EVENT, this._hideItemList.bind(this, item));
242
243 var closeLink = elCreate('a');
244 elAttr(closeLink, 'href', '#');
245 closeLink.className = 'menuOverlayItemLinkIcon';
246 closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
247 closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
248
249 wrapper.appendChild(backLink);
250 wrapper.appendChild(closeLink);
251 backLinkItem.appendChild(wrapper);
252
253 itemList.insertBefore(backLinkItem, itemList.firstElementChild);
254
255 if (!backLinkItem.nextElementSibling.classList.contains('menuOverlayTitle')) {
256 var titleItem = elCreate('li');
257 titleItem.className = 'menuOverlayTitle';
258 var title = elCreate('span');
259 title.textContent = itemTitle;
260 titleItem.appendChild(title);
261
262 itemList.insertBefore(titleItem, backLinkItem.nextElementSibling);
263 }
264 },
265
266 /**
267 * Renders the menu item list header.
268 *
269 * @protected
270 */
271 _initHeader: function() {
272 var listItem = elCreate('li');
273 listItem.className = 'menuOverlayHeader';
274
275 var wrapper = elCreate('span');
276 wrapper.className = 'menuOverlayItemWrapper';
277 listItem.appendChild(wrapper);
278
279 var logoWrapper = elCreate('span');
280 logoWrapper.className = 'menuOverlayLogoWrapper';
281 wrapper.appendChild(logoWrapper);
282
283 var logo = elCreate('span');
284 logo.className = 'menuOverlayLogo';
285 logo.style.setProperty('background-image', 'url("' + elData(this._menu, 'page-logo') + '")', '');
286 logoWrapper.appendChild(logo);
287
288 var closeLink = elCreate('a');
289 elAttr(closeLink, 'href', '#');
290 closeLink.className = 'menuOverlayItemLinkIcon';
291 closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
292 closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
293 wrapper.appendChild(closeLink);
294
295 var list = DomTraverse.childByClass(this._menu, 'menuOverlayItemList');
296 list.insertBefore(listItem, list.firstElementChild);
297 },
298
299 /**
300 * Hides an item list, return to the parent item list.
301 *
302 * @param {Element} item menu item
303 * @param {Event} event event object
304 * @protected
305 */
306 _hideItemList: function(item, event) {
307 if (event instanceof Event) {
308 event.preventDefault();
309 }
310
311 this._menu.classList.remove('allowScroll');
312 this._removeActiveList = true;
313
314 var data = this._items.get(item);
315 data.parentItemList.classList.remove('hidden');
316
317 this._updateDepth(false);
318 },
319
320 /**
321 * Shows the child item list.
322 *
323 * @param {Element} item menu item
324 * @param event
325 * @private
326 */
327 _showItemList: function(item, event) {
328 if (event instanceof Event) {
329 event.preventDefault();
330 }
331
332 var data = this._items.get(item);
333
334 var load = elData(data.itemList, 'load');
335 if (load) {
336 if (!elDataBool(item, 'loaded')) {
337 var icon = event.currentTarget.firstElementChild;
338 if (icon.classList.contains('fa-angle-right')) {
339 icon.classList.remove('fa-angle-right');
340 icon.classList.add('fa-spinner');
341 }
342
343 EventHandler.fire(this._eventIdentifier, 'load_' + load);
344
345 return;
346 }
347 }
348
349 this._menu.classList.remove('allowScroll');
350
351 data.itemList.classList.add('activeList');
352 data.parentItemList.classList.add('hidden');
353
354 this._activeList.push(data.itemList);
355
356 this._updateDepth(true);
357 },
358
359 _updateDepth: function(increase) {
360 this._depth += (increase) ? 1 : -1;
361
362 this._menu.children[0].style.setProperty('transform', 'translateX(' + (this._depth * -100) + '%)', '')
363 }
364 };
365
366 return UiPageMenuAbstract;
367 });