Diff: STRATO-apps/wordpress_03/app/wp-content/themes/blocksy/static/js/frontend/header/menu.js

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + import { isTouchDevice } from '../helpers/is-touch-device'
2 + import { isIosDevice } from '../helpers/is-ios-device'
3 +
4 + export const isMegaMenuCustomWidthCentered = (el) => {
5 + return (
6 + el.classList.contains('ct-mega-menu-custom-width') &&
7 + el.classList.contains('ct-mega-menu-centered')
8 + )
9 + }
10 +
11 + const isRtl = () => document.querySelector('html').dir === 'rtl'
12 +
13 + const isEligibleForSubmenu = (el) => {
14 + if (!el.className.includes('animated-submenu')) {
15 + return false
16 + }
17 +
18 + if (el.className.includes('ct-mega-menu')) {
19 + // If not custom width, it should not apply data-submenu attribute
20 + // at all.
21 + if (!el.classList.contains('ct-mega-menu-custom-width')) {
22 + return false
23 + }
24 +
25 + // Don't treat centered custom width mega menus as regular sub-menus.
26 + if (isMegaMenuCustomWidthCentered(el)) {
27 + return false
28 + }
29 +
30 + return true
31 + }
32 +
33 + return true
34 + }
35 +
36 + const getAllParents = (a) => {
37 + var els = []
38 +
39 + while (a) {
40 + els.unshift(a)
41 + a = a.parentNode
42 + }
43 +
44 + return els
45 + }
46 +
47 + function furthest(el, s) {
48 + var nodes = []
49 +
50 + while (el.parentNode) {
51 + if (
52 + el.parentNode &&
53 + el.parentNode.matches &&
54 + el.parentNode.matches(s)
55 + ) {
56 + nodes.push(el.parentNode)
57 + }
58 +
59 + el = el.parentNode
60 + }
61 +
62 + return nodes[nodes.length - 1]
63 + }
64 +
65 + const getAllParentsUntil = (el, parent) => {
66 + const parents = []
67 +
68 + while (el.parentNode !== parent) {
69 + parents.push(el.parentNode)
70 + el = el.parentNode
71 + }
72 +
73 + return parents
74 + }
75 +
76 + const reversePlacementIfNeeded = (placement) => {
77 + if (!isRtl()) {
78 + return placement
79 + }
80 +
81 + if (placement === 'left') {
82 + return 'right'
83 + }
84 +
85 + if (placement === 'right') {
86 + return 'left'
87 + }
88 +
89 + return placement
90 + }
91 +
92 + const getPreferedPlacementFor = (el) => {
93 + let farmost = furthest(el, 'li.menu-item')
94 +
95 + if (!farmost) {
96 + return reversePlacementIfNeeded('right')
97 + }
98 +
99 + const submenusWithParents = [...farmost.querySelectorAll('.sub-menu')].map(
100 + (el) => {
101 + return { el, parents: getAllParentsUntil(el, farmost) }
102 + }
103 + )
104 +
105 + if (submenusWithParents.length === 0) {
106 + return reversePlacementIfNeeded('right')
107 + }
108 +
109 + const submenusWithParentsSorted = submenusWithParents
110 + .sort((a, b) => a.parents.length - b.parents.length)
111 + .reverse()
112 +
113 + const submenuWithMostParents = submenusWithParentsSorted[0]
114 +
115 + const allSubmenus = [
116 + ...submenuWithMostParents.parents.filter((el) =>
117 + el.matches('.sub-menu')
118 + ),
119 +
120 + ...[submenuWithMostParents.el]
121 + ]
122 +
123 + const allSubmenusAlignedWidth = allSubmenus.reduce((acc, el, index) => {
124 + const style = getComputedStyle(el)
125 +
126 + return (
127 + acc +
128 + el.getBoundingClientRect().width +
129 + (index === 0
130 + ? 0
131 + : parseFloat(
132 + style.getPropertyValue(
133 + '--dropdown-horizontal-offset'
134 + ) || '5px'
135 + ))
136 + )
137 + }, 0)
138 +
139 + const farmostRect = farmost.getBoundingClientRect()
140 +
141 + let willItFitToTheLeft = allSubmenusAlignedWidth < farmostRect.right
142 +
143 + let willItFitToTheRight =
144 + innerWidth - farmostRect.left > allSubmenusAlignedWidth
145 +
146 + if (farmost.matches('.animated-submenu-inline')) {
147 + willItFitToTheRight =
148 + innerWidth - farmostRect.left - farmostRect.width >
149 + allSubmenusAlignedWidth
150 + }
151 +
152 + if (isRtl()) {
153 + if (!willItFitToTheLeft && !willItFitToTheRight) {
154 + return 'left'
155 + }
156 +
157 + return willItFitToTheLeft ? 'left' : 'right'
158 + }
159 +
160 + if (!willItFitToTheLeft && !willItFitToTheRight) {
161 + return 'right'
162 + }
163 +
164 + return willItFitToTheRight ? 'right' : 'left'
165 + }
166 +
167 + const computeItemSubmenuFor = (
168 + reference,
169 + {
170 + // left -- 1st level menu items
171 + // end -- submenus
172 + startPosition = 'end'
173 + }
174 + ) => {
175 + const menu = reference.querySelector('.sub-menu')
176 + const placement = getPreferedPlacementFor(menu)
177 +
178 + const { left, width, right } = menu.getBoundingClientRect()
179 +
180 + let futurePlacement = placement
181 + let referenceRect = reference.getBoundingClientRect()
182 +
183 + if (placement === 'left') {
184 + let referencePoint =
185 + startPosition === 'end' ? referenceRect.left : referenceRect.right
186 +
187 + if (referencePoint - width < 0) {
188 + futurePlacement = 'right'
189 + }
190 + }
191 +
192 + if (placement === 'right') {
193 + let referencePoint =
194 + startPosition === 'end' ? referenceRect.right : referenceRect.left
195 +
196 + if (referencePoint + width > innerWidth) {
197 + futurePlacement = 'left'
198 + }
199 + }
200 +
201 + reference.dataset.submenu = futurePlacement
202 +
203 + reference.addEventListener('click', () => {})
204 + }
205 +
206 + const openSubmenu = (e) => {
207 + const li = e.target.closest('li')
208 +
209 + if (li.__closeSubmenuTimer__) {
210 + clearTimeout(li.__closeSubmenuTimer__)
211 + li.__closeSubmenuTimer__ = null
212 + }
213 +
214 + li.classList.add('ct-active')
215 +
216 + let childIndicator = [...li.children].find((el) =>
217 + el.matches('.ct-toggle-dropdown-desktop-ghost')
218 + )
219 +
220 + if (!childIndicator) {
221 + childIndicator = li.firstElementChild
222 + }
223 +
224 + if (childIndicator) {
225 + childIndicator.setAttribute('aria-expanded', 'true')
226 + if (childIndicator.tagName.toLowerCase() === 'button') {
227 + childIndicator.setAttribute(
228 + 'aria-label',
229 + ct_localizations.collapse_submenu
230 + )
231 + }
232 + }
233 +
234 + if (!isEligibleForSubmenu(li)) {
235 + return
236 + }
237 +
238 + const menu = li.querySelector('.sub-menu')
239 +
240 + mountMenuLevel(menu)
241 +
242 + if (menu.closest('[data-interaction="hover"]')) {
243 + menu.parentNode.addEventListener(
244 + 'mouseleave',
245 + () => {
246 + ;[...menu.children]
247 + .filter((el) => isEligibleForSubmenu(el))
248 + .map((el) => el.removeAttribute('data-submenu'))
249 + },
250 + { once: true }
251 + )
252 + }
253 + }
254 +
255 + const closeSubmenu = (e) => {
256 + if (!e.target) {
257 + return
258 + }
259 +
260 + const li = e.target.closest('li')
261 + li.classList.remove('ct-active')
262 +
263 + let childIndicator = [...li.children].find((el) =>
264 + el.matches('.ct-toggle-dropdown-desktop-ghost')
265 + )
266 +
267 + if (!childIndicator) {
268 + childIndicator = li.firstElementChild
269 + }
270 +
271 + if (childIndicator) {
272 + childIndicator.setAttribute('aria-expanded', 'false')
273 +
274 + if (childIndicator.tagName.toLowerCase() === 'button') {
275 + childIndicator.setAttribute(
276 + 'aria-label',
277 + ct_localizations.expand_submenu
278 + )
279 + }
280 +
281 + if (e.focusOnIndicator) {
282 + childIndicator.focus({
283 + focusVisible: true
284 + })
285 + }
286 + }
287 +
288 + li.__closeSubmenuTimer__ = setTimeout(() => {
289 + li.__closeSubmenuTimer__ = null
290 + ;[...li.querySelectorAll('[data-submenu]')].map((el) => {
291 + el.removeAttribute('data-submenu')
292 + })
293 + ;[...li.querySelectorAll('.ct-active')].map((el) => {
294 + el.classList.remove('ct-active')
295 + })
296 + }, 30)
297 + }
298 +
299 + const mountMenuForElement = (el, args = {}) => {
300 + if (isMegaMenuCustomWidthCentered(el)) {
301 + const menu = el.querySelector('.sub-menu')
302 +
303 + const elRect = el.getBoundingClientRect()
304 + const menuRect = menu.getBoundingClientRect()
305 +
306 + let centerFits =
307 + elRect.left + elRect.width / 2 > menuRect.width / 2 &&
308 + innerWidth - (elRect.left + elRect.width / 2) > menuRect.width / 2
309 +
310 + if (!centerFits) {
311 + const placement = getPreferedPlacementFor(menu)
312 +
313 + let offset = 0
314 +
315 + let edgeOffset = 15
316 +
317 + if (placement === 'right') {
318 + offset = `${
319 + Math.round(
320 + elRect.left - (innerWidth - menuRect.width) + edgeOffset
321 + ) * -1
322 + }px`
323 +
324 + if (!(elRect.left + elRect.width / 2 > menuRect.width / 2)) {
325 + offset = `${Math.round(elRect.left - edgeOffset) * -1}px`
326 + }
327 + }
328 +
329 + if (placement === 'left') {
330 + offset = `${
331 + Math.round(innerWidth - elRect.right - edgeOffset) * -1
332 + }px`
333 + }
334 +
335 + el.dataset.submenu = placement
336 +
337 + menu.style.setProperty('--theme-submenu-inline-offset', offset)
338 + }
339 + }
340 +
341 + if (isEligibleForSubmenu(el)) {
342 + computeItemSubmenuFor(el, args)
343 + }
344 +
345 + if (el.hasSubmenuEventListeners) {
346 + return
347 + }
348 +
349 + el.hasSubmenuEventListeners = true
350 +
351 + let hasClickInteraction = el.matches('[data-interaction*="click"] *')
352 +
353 + el.addEventListener('keydown', function (e) {
354 + if (e.keyCode == 27) {
355 + closeSubmenu({
356 + target: el.firstElementChild,
357 + focusOnIndicator: true
358 + })
359 + }
360 + })
361 +
362 + el.addEventListener('focusout', (evt) => {
363 + if (el.contains(evt.relatedTarget)) {
364 + return
365 + }
366 +
367 + closeSubmenu({
368 + target: el.firstElementChild
369 + })
370 + })
371 +
372 + if (!hasClickInteraction) {
373 + const handleMouseEnter = () => {
374 + // So that mouseenter event is catched before the open itself
375 + if (isIosDevice()) {
376 + openSubmenu({ target: el.firstElementChild })
377 + } else {
378 + requestAnimationFrame(() => {
379 + openSubmenu({ target: el.firstElementChild })
380 + })
381 + }
382 +
383 + // If first level
384 + if (!el.parentNode.classList.contains('.sub-menu')) {
385 + ;[...el.parentNode.children]
386 + .filter((firstLevelEl) => firstLevelEl !== el)
387 + .map((firstLevelEl) => {
388 + closeSubmenu({
389 + target: firstLevelEl.firstElementChild
390 + })
391 + })
392 + }
393 +
394 + el.addEventListener(
395 + 'mouseleave',
396 + () => {
397 + closeSubmenu({ target: el.firstElementChild })
398 + },
399 + { once: true }
400 + )
401 + }
402 +
403 + el.addEventListener('mouseenter', handleMouseEnter)
404 +
405 + if (el.matches(':hover')) {
406 + handleMouseEnter()
407 + }
408 +
409 + // On Android devices, allow only 2nd click to open the link.
410 + // First click will ensure the submenu is opened
411 + //
412 + // iOS has this behaviour out of the box.
413 + //
414 + // Important: only perform this for touch devices so that keyboard
415 + // users are not affected.
416 + if (isTouchDevice()) {
417 + el.addEventListener('click', (e) => {
418 + if (!el.classList.contains('ct-active')) {
419 + e.preventDefault()
420 + }
421 + })
422 + }
423 + }
424 +
425 + if (hasClickInteraction) {
426 + let itemTarget = el.matches('[data-interaction*="item"] *')
427 + ? el.firstElementChild
428 + : el.firstElementChild.querySelector('.ct-toggle-dropdown-desktop')
429 +
430 + itemTarget.addEventListener('click', (e) => {
431 + e.preventDefault()
432 +
433 + if (e.target.closest('li').classList.contains('ct-active')) {
434 + closeSubmenu(e)
435 + } else {
436 + openSubmenu(e)
437 +
438 + if (isIosDevice()) {
439 + e.target.closest('li').addEventListener(
440 + 'mouseleave',
441 + () => {
442 + closeSubmenu({
443 + target: el.firstElementChild
444 + })
445 + },
446 + { once: true }
447 + )
448 + }
449 +
450 + if (!e.target.hasDocumentListener) {
451 + e.target.hasDocumentListener = true
452 + // Add the event a bit later
453 + setTimeout(() => {
454 + document.addEventListener('click', (evt) => {
455 + if (!e.target.closest('li').contains(evt.target)) {
456 + closeSubmenu(e)
457 + }
458 + })
459 + })
460 + }
461 + }
462 + })
463 + }
464 +
465 + let childIndicator = [...el.children].find((el) =>
466 + el.matches('.ct-toggle-dropdown-desktop-ghost')
467 + )
468 +
469 + if (childIndicator) {
470 + childIndicator.addEventListener('click', (e) => {
471 + if (e.target.closest('li').classList.contains('ct-active')) {
472 + closeSubmenu(e)
473 + } else {
474 + openSubmenu(e)
475 + }
476 + })
477 + }
478 + }
479 +
480 + export const mountMenuLevel = (menuLevel, args = {}) => {
481 + ;[...menuLevel.children]
482 + .filter((el) =>
483 + el.matches('.menu-item-has-children, .page_item_has_children')
484 + )
485 + .map((el) => {
486 + mountMenuForElement(el, args)
487 + })
488 + }
489 +