Commit | Line | Data |
---|---|---|
4bbf6ff1 AE |
1 | /** |
2 | * Modifies the interface to provide a better usability for mobile devices. | |
50aa3a01 | 3 | * |
9ada5b42 AE |
4 | * @author Alexander Ebert |
5 | * @copyright 2001-2019 WoltLab GmbH | |
6 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
4bbf6ff1 | 7 | */ |
7f4a2311 | 8 | define(["require", "exports", "tslib", "focus-trap", "../Core", "../Dom/Change/Listener", "../Dom/Util", "../Environment", "./Alignment", "./CloseOverlay", "./Dropdown/Reusable", "./Page/Menu/Main", "./Page/Menu/User", "./Screen", "../Language"], function (require, exports, tslib_1, focus_trap_1, Core, Listener_1, Util_1, Environment, UiAlignment, CloseOverlay_1, UiDropdownReusable, Main_1, User_1, UiScreen, Language) { |
50aa3a01 | 9 | "use strict"; |
9ada5b42 | 10 | Object.defineProperty(exports, "__esModule", { value: true }); |
054537bf AE |
11 | exports.setup = setup; |
12 | exports.enable = enable; | |
13 | exports.enableShadow = enableShadow; | |
14 | exports.disable = disable; | |
15 | exports.disableShadow = disableShadow; | |
16 | exports.rebuildShadow = rebuildShadow; | |
17 | exports.removeShadow = removeShadow; | |
716617cf TD |
18 | Core = tslib_1.__importStar(Core); |
19 | Listener_1 = tslib_1.__importDefault(Listener_1); | |
1efd0b8b | 20 | Util_1 = tslib_1.__importDefault(Util_1); |
716617cf | 21 | Environment = tslib_1.__importStar(Environment); |
716617cf TD |
22 | UiAlignment = tslib_1.__importStar(UiAlignment); |
23 | CloseOverlay_1 = tslib_1.__importDefault(CloseOverlay_1); | |
24 | UiDropdownReusable = tslib_1.__importStar(UiDropdownReusable); | |
716617cf | 25 | UiScreen = tslib_1.__importStar(UiScreen); |
1efd0b8b | 26 | Language = tslib_1.__importStar(Language); |
9ada5b42 AE |
27 | let _dropdownMenu = null; |
28 | let _dropdownMenuMessage = null; | |
29 | let _enabled = false; | |
30 | let _enabledLGTouchNavigation = false; | |
31 | let _enableMobileMenu = false; | |
7f4a2311 | 32 | let _focusTrap = undefined; |
9ada5b42 AE |
33 | const _knownMessages = new WeakSet(); |
34 | let _mobileSidebarEnabled = false; | |
ab82d20b | 35 | let _pageMenuMain; |
8d3dce1a | 36 | let _pageMenuUser = undefined; |
9ada5b42 | 37 | let _messageGroups = null; |
c7bacb86 | 38 | let _pageMenuMainProvider; |
9ada5b42 | 39 | const _sidebars = []; |
6817cc5d | 40 | function init() { |
9ada5b42 | 41 | _enabled = true; |
6817cc5d AE |
42 | initButtonGroupNavigation(); |
43 | initMessages(); | |
44 | initMobileMenu(); | |
45 | CloseOverlay_1.default.add("WoltLabSuite/Core/Ui/Mobile", closeAllMenus); | |
9ada5b42 | 46 | Listener_1.default.add("WoltLabSuite/Core/Ui/Mobile", () => { |
6817cc5d AE |
47 | initButtonGroupNavigation(); |
48 | initMessages(); | |
9ada5b42 | 49 | }); |
b91d3b98 | 50 | document.addEventListener("scroll", () => closeDropdown(), { passive: true }); |
9ada5b42 | 51 | } |
6817cc5d | 52 | function initButtonGroupNavigation() { |
9ada5b42 AE |
53 | document.querySelectorAll(".buttonGroupNavigation").forEach((navigation) => { |
54 | if (navigation.classList.contains("jsMobileButtonGroupNavigation")) { | |
55 | return; | |
50aa3a01 | 56 | } |
9ada5b42 AE |
57 | else { |
58 | navigation.classList.add("jsMobileButtonGroupNavigation"); | |
50aa3a01 | 59 | } |
9ada5b42 AE |
60 | const list = navigation.querySelector(".buttonList"); |
61 | if (list.childElementCount === 0) { | |
62 | // ignore objects without options | |
63 | return; | |
64 | } | |
65 | navigation.parentElement.classList.add("hasMobileNavigation"); | |
13136ab4 | 66 | const button = document.createElement("button"); |
e7f175fb | 67 | button.type = "button"; |
13136ab4 AE |
68 | button.innerHTML = '<fa-icon size="24" name="ellipsis-vertical"></fa-icon>'; |
69 | button.classList.add("dropdownLabel"); | |
9ada5b42 AE |
70 | button.addEventListener("click", (event) => { |
71 | event.preventDefault(); | |
72 | event.stopPropagation(); | |
7e574eaa | 73 | closeAllMenus(); |
9ada5b42 | 74 | navigation.classList.toggle("open"); |
50aa3a01 | 75 | }); |
9ada5b42 AE |
76 | list.addEventListener("click", function (event) { |
77 | event.stopPropagation(); | |
78 | navigation.classList.remove("open"); | |
50aa3a01 | 79 | }); |
9ada5b42 AE |
80 | navigation.insertBefore(button, navigation.firstChild); |
81 | }); | |
82 | } | |
6817cc5d | 83 | function initMessages() { |
9ada5b42 AE |
84 | document.querySelectorAll(".message").forEach((message) => { |
85 | if (_knownMessages.has(message)) { | |
86 | return; | |
50aa3a01 | 87 | } |
9ada5b42 AE |
88 | const navigation = message.querySelector(".jsMobileNavigation"); |
89 | if (navigation) { | |
90 | navigation.addEventListener("click", (event) => { | |
91 | event.stopPropagation(); | |
92 | // mimic dropdown behavior | |
93 | window.setTimeout(() => { | |
94 | navigation.classList.remove("open"); | |
95 | }, 10); | |
96 | }); | |
97 | const quickOptions = message.querySelector(".messageQuickOptions"); | |
98 | if (quickOptions && navigation.childElementCount) { | |
99 | quickOptions.classList.add("active"); | |
ad845963 AE |
100 | let buttonWrapper = quickOptions.querySelector(".messageQuickOptionsMobile"); |
101 | if (buttonWrapper === null) { | |
102 | buttonWrapper = document.createElement("li"); | |
103 | buttonWrapper.innerHTML = ` | |
58d5a1b5 | 104 | <button type="button" aria-label="${Language.get("wcf.global.button.more")}"> |
ab43711a | 105 | <fa-icon name="ellipsis-vertical"></fa-icon> |
ad845963 AE |
106 | </button> |
107 | `; | |
108 | buttonWrapper.classList.add("messageQuickOptionsMobile"); | |
109 | quickOptions.append(buttonWrapper); | |
7f4a2311 | 110 | } |
ad845963 AE |
111 | const button = buttonWrapper.querySelector("button"); |
112 | button.addEventListener("click", (event) => { | |
113 | event.stopPropagation(); | |
114 | toggleMobileNavigation(message, quickOptions, navigation); | |
115 | }); | |
50aa3a01 | 116 | } |
50aa3a01 | 117 | } |
9ada5b42 AE |
118 | _knownMessages.add(message); |
119 | }); | |
120 | } | |
6817cc5d | 121 | function initMobileMenu() { |
9ada5b42 | 122 | if (_enableMobileMenu) { |
c7bacb86 | 123 | _pageMenuMain = new Main_1.PageMenuMain(_pageMenuMainProvider); |
ab82d20b | 124 | _pageMenuMain.enable(); |
77e970f4 AE |
125 | if ((0, User_1.hasValidUserMenu)()) { |
126 | _pageMenuUser = new User_1.PageMenuUser(); | |
127 | _pageMenuUser.enable(); | |
8d3dce1a | 128 | } |
9ada5b42 AE |
129 | } |
130 | } | |
6817cc5d | 131 | function closeAllMenus() { |
9ada5b42 AE |
132 | document.querySelectorAll(".jsMobileButtonGroupNavigation.open, .jsMobileNavigation.open").forEach((menu) => { |
133 | menu.classList.remove("open"); | |
134 | }); | |
135 | if (_enabled && _dropdownMenu) { | |
136 | closeDropdown(); | |
137 | } | |
138 | } | |
6817cc5d | 139 | function enableMobileSidebar() { |
9ada5b42 AE |
140 | _mobileSidebarEnabled = true; |
141 | } | |
6817cc5d | 142 | function disableMobileSidebar() { |
9ada5b42 AE |
143 | _mobileSidebarEnabled = false; |
144 | _sidebars.forEach(function (sidebar) { | |
145 | sidebar.classList.remove("open"); | |
146 | }); | |
147 | } | |
6817cc5d | 148 | function setupMobileSidebar() { |
9ada5b42 AE |
149 | _sidebars.forEach(function (sidebar) { |
150 | sidebar.addEventListener("mousedown", function (event) { | |
151 | if (_mobileSidebarEnabled && event.target === sidebar) { | |
152 | event.preventDefault(); | |
153 | sidebar.classList.toggle("open"); | |
50aa3a01 | 154 | } |
50aa3a01 | 155 | }); |
9ada5b42 AE |
156 | }); |
157 | _mobileSidebarEnabled = true; | |
158 | } | |
159 | function closeDropdown() { | |
2b2ad7a7 | 160 | _dropdownMenu?.classList.remove("dropdownOpen"); |
9ada5b42 | 161 | } |
6817cc5d | 162 | function toggleMobileNavigation(message, quickOptions, navigation) { |
9ada5b42 AE |
163 | if (_dropdownMenu === null) { |
164 | _dropdownMenu = document.createElement("ul"); | |
165 | _dropdownMenu.className = "dropdownMenu"; | |
166 | UiDropdownReusable.init("com.woltlab.wcf.jsMobileNavigation", _dropdownMenu); | |
167 | } | |
168 | else if (_dropdownMenu.classList.contains("dropdownOpen")) { | |
169 | closeDropdown(); | |
7f4a2311 AE |
170 | _focusTrap.deactivate(); |
171 | _focusTrap = undefined; | |
9ada5b42 AE |
172 | if (_dropdownMenuMessage === message) { |
173 | // toggle behavior | |
174 | return; | |
50aa3a01 | 175 | } |
9ada5b42 AE |
176 | } |
177 | _dropdownMenu.innerHTML = ""; | |
178 | CloseOverlay_1.default.execute(); | |
6817cc5d | 179 | rebuildMobileNavigation(navigation); |
9ada5b42 AE |
180 | const previousNavigation = navigation.previousElementSibling; |
181 | if (previousNavigation && previousNavigation.classList.contains("messageFooterButtonsExtra")) { | |
182 | const divider = document.createElement("li"); | |
183 | divider.className = "dropdownDivider"; | |
184 | _dropdownMenu.appendChild(divider); | |
6817cc5d | 185 | rebuildMobileNavigation(previousNavigation); |
9ada5b42 AE |
186 | } |
187 | UiAlignment.set(_dropdownMenu, quickOptions, { | |
188 | horizontal: "right", | |
189 | allowFlip: "vertical", | |
190 | }); | |
191 | _dropdownMenu.classList.add("dropdownOpen"); | |
192 | _dropdownMenuMessage = message; | |
7f4a2311 AE |
193 | _focusTrap = (0, focus_trap_1.createFocusTrap)(_dropdownMenu, { |
194 | allowOutsideClick: true, | |
195 | escapeDeactivates() { | |
196 | toggleMobileNavigation(message, quickOptions, navigation); | |
197 | return false; | |
198 | }, | |
a4d21c3e | 199 | setReturnFocus: quickOptions, |
7f4a2311 AE |
200 | }); |
201 | _focusTrap.activate(); | |
9ada5b42 | 202 | } |
6817cc5d | 203 | function setupLGTouchNavigation() { |
9ada5b42 AE |
204 | _enabledLGTouchNavigation = true; |
205 | document.querySelectorAll(".boxMenuHasChildren > a").forEach((element) => { | |
04653e2e | 206 | element.addEventListener("touchstart", (event) => { |
9ada5b42 AE |
207 | if (_enabledLGTouchNavigation && element.getAttribute("aria-expanded") === "false") { |
208 | event.preventDefault(); | |
209 | element.setAttribute("aria-expanded", "true"); | |
210 | // Register an new event listener after the touch ended, which is triggered once when an | |
211 | // element on the page is pressed. This allows us to reset the touch status of the navigation | |
212 | // entry when the entry is no longer open, so that it does not redirect to the page when you | |
213 | // click it again. | |
214 | element.addEventListener("touchend", () => { | |
215 | document.body.addEventListener("touchstart", () => { | |
216 | document.body.addEventListener("touchend", (event) => { | |
217 | const parent = element.parentElement; | |
218 | const target = event.target; | |
219 | if (!parent.contains(target) && target !== parent) { | |
220 | element.setAttribute("aria-expanded", "false"); | |
221 | } | |
50aa3a01 | 222 | }, { |
9ada5b42 | 223 | once: true, |
50aa3a01 TD |
224 | }); |
225 | }, { | |
9ada5b42 | 226 | once: true, |
50aa3a01 | 227 | }); |
9ada5b42 AE |
228 | }, { once: true }); |
229 | } | |
04653e2e | 230 | }, { passive: false }); |
9ada5b42 AE |
231 | }); |
232 | } | |
6817cc5d | 233 | function enableLGTouchNavigation() { |
9ada5b42 AE |
234 | _enabledLGTouchNavigation = true; |
235 | } | |
6817cc5d | 236 | function disableLGTouchNavigation() { |
9ada5b42 AE |
237 | _enabledLGTouchNavigation = false; |
238 | } | |
6817cc5d | 239 | function rebuildMobileNavigation(navigation) { |
9ada5b42 | 240 | navigation.querySelectorAll(".button").forEach((button) => { |
a894bd6e AE |
241 | if (button.classList.contains("ignoreMobileNavigation") || button.classList.contains("reactButton")) { |
242 | return; | |
9ada5b42 AE |
243 | } |
244 | const item = document.createElement("li"); | |
245 | if (button.classList.contains("active")) { | |
246 | item.className = "active"; | |
247 | } | |
248 | const label = button.querySelector("span:not(.icon)"); | |
249 | item.innerHTML = `<a href="#">${label.textContent}</a>`; | |
250 | item.children[0].addEventListener("click", function (event) { | |
251 | event.preventDefault(); | |
252 | event.stopPropagation(); | |
253 | if (button.nodeName === "A") { | |
254 | button.click(); | |
255 | } | |
256 | else { | |
257 | Core.triggerEvent(button, "click"); | |
258 | } | |
259 | closeDropdown(); | |
260 | }); | |
261 | _dropdownMenu.appendChild(item); | |
262 | }); | |
263 | } | |
264 | /** | |
265 | * Initializes the mobile UI. | |
266 | */ | |
c7bacb86 | 267 | function setup(enableMobileMenu, pageMenuMainProvider) { |
9ada5b42 | 268 | _enableMobileMenu = enableMobileMenu; |
c7bacb86 | 269 | _pageMenuMainProvider = pageMenuMainProvider; |
c00bdb06 | 270 | document.querySelectorAll(".boxesSidebarLeft").forEach((sidebar) => { |
9ada5b42 AE |
271 | _sidebars.push(sidebar); |
272 | }); | |
273 | if (Environment.touch()) { | |
274 | document.documentElement.classList.add("touch"); | |
275 | } | |
276 | if (Environment.platform() !== "desktop") { | |
277 | document.documentElement.classList.add("mobile"); | |
278 | } | |
279 | const messageGroupList = document.querySelector(".messageGroupList"); | |
280 | if (messageGroupList) { | |
281 | _messageGroups = messageGroupList.getElementsByClassName("messageGroup"); | |
282 | } | |
283 | UiScreen.on("screen-md-down", { | |
284 | match: enable, | |
285 | unmatch: disable, | |
6817cc5d | 286 | setup: init, |
9ada5b42 AE |
287 | }); |
288 | UiScreen.on("screen-sm-down", { | |
289 | match: enableShadow, | |
290 | unmatch: disableShadow, | |
291 | setup: enableShadow, | |
292 | }); | |
293 | UiScreen.on("screen-md-down", { | |
6817cc5d AE |
294 | match: enableMobileSidebar, |
295 | unmatch: disableMobileSidebar, | |
296 | setup: setupMobileSidebar, | |
9ada5b42 AE |
297 | }); |
298 | // On the large tablets (e.g. iPad Pro) the navigation is not usable, because there is not the mobile | |
299 | // layout displayed, but the normal one for the desktop. The navigation reacts to a hover status if a | |
300 | // menu item has several submenu items. Logically, this cannot be created with the tablet, so that we | |
301 | // display the submenu here after a single click and only follow the link after another click. | |
302 | if (Environment.touch() && (Environment.platform() === "ios" || Environment.platform() === "android")) { | |
303 | UiScreen.on("screen-lg", { | |
6817cc5d AE |
304 | match: enableLGTouchNavigation, |
305 | unmatch: disableLGTouchNavigation, | |
306 | setup: setupLGTouchNavigation, | |
50aa3a01 TD |
307 | }); |
308 | } | |
9ada5b42 | 309 | } |
9ada5b42 AE |
310 | /** |
311 | * Enables the mobile UI. | |
312 | */ | |
313 | function enable() { | |
6d44b5e7 | 314 | CloseOverlay_1.default.execute(); |
9ada5b42 AE |
315 | _enabled = true; |
316 | if (_enableMobileMenu) { | |
ab82d20b | 317 | _pageMenuMain.enable(); |
2b2ad7a7 | 318 | _pageMenuUser?.enable(); |
9ada5b42 AE |
319 | } |
320 | } | |
9ada5b42 AE |
321 | /** |
322 | * Enables shadow links for larger click areas on messages. | |
323 | */ | |
324 | function enableShadow() { | |
325 | if (_messageGroups) { | |
326 | rebuildShadow(_messageGroups, ".messageGroupLink"); | |
327 | } | |
328 | } | |
9ada5b42 AE |
329 | /** |
330 | * Disables the mobile UI. | |
331 | */ | |
332 | function disable() { | |
6d44b5e7 | 333 | CloseOverlay_1.default.execute(); |
9ada5b42 AE |
334 | _enabled = false; |
335 | if (_enableMobileMenu) { | |
ab82d20b | 336 | _pageMenuMain.disable(); |
2b2ad7a7 | 337 | _pageMenuUser?.disable(); |
9ada5b42 AE |
338 | } |
339 | } | |
9ada5b42 AE |
340 | /** |
341 | * Disables shadow links. | |
342 | */ | |
343 | function disableShadow() { | |
344 | if (_messageGroups) { | |
345 | removeShadow(_messageGroups); | |
346 | } | |
347 | if (_dropdownMenu) { | |
348 | closeDropdown(); | |
349 | } | |
350 | } | |
9ada5b42 AE |
351 | function rebuildShadow(elements, linkSelector) { |
352 | Array.from(elements).forEach((element) => { | |
353 | const parent = element.parentElement; | |
354 | let shadow = parent.querySelector(".mobileLinkShadow"); | |
355 | if (shadow === null) { | |
356 | const link = element.querySelector(linkSelector); | |
357 | if (link.href) { | |
358 | shadow = document.createElement("a"); | |
359 | shadow.className = "mobileLinkShadow"; | |
360 | shadow.href = link.href; | |
c86d00dc | 361 | shadow.setAttribute("aria-labelledby", Util_1.default.identify(link)); |
9ada5b42 AE |
362 | parent.appendChild(shadow); |
363 | parent.classList.add("mobileLinkShadowContainer"); | |
364 | } | |
365 | } | |
366 | }); | |
367 | } | |
9ada5b42 AE |
368 | function removeShadow(elements) { |
369 | Array.from(elements).forEach((element) => { | |
370 | const parent = element.parentElement; | |
371 | if (parent.classList.contains("mobileLinkShadowContainer")) { | |
372 | const shadow = parent.querySelector(".mobileLinkShadow"); | |
373 | if (shadow !== null) { | |
374 | shadow.remove(); | |
375 | } | |
376 | parent.classList.remove("mobileLinkShadowContainer"); | |
377 | } | |
378 | }); | |
379 | } | |
4bbf6ff1 | 380 | }); |