Merge pull request #6006 from WoltLab/file-processor-can-adopt
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Ui / Mobile.js
CommitLineData
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 8define(["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});