UNPKG

123 kBJavaScriptView Raw
1/*!
2 * Xtend UI (https://xtendui.com/)
3 * @copyright (c) 2017 - 2021 Riccardo Caroli
4 * @license MIT (https://github.com/xtendui/xtendui/blob/master/LICENSE.txt)
5 */
6
7import { Xt } from './xt.mjs'
8import JSON5 from 'json5'
9import * as focusTrap from 'focus-trap'
10Xt.JSON5 = JSON5
11Xt.focusTrap = focusTrap
12
13/**
14 * Toggle
15 */
16class Toggle {
17 /**
18 * fields
19 */
20 _optionsCustom
21 _optionsDefault
22 _optionsInitial
23 _componentNs
24 _mode
25 _classes = []
26 _classesIn = []
27 _classesOut = []
28 _classesDone = []
29 _classesInitial = []
30 _classesBefore = []
31 _classesAfter = []
32 _initialCurrents = []
33 _destroyElements
34 _containerElements
35 _containerTargets
36 _oldIndex
37 _inverse
38 _queueIn = []
39 _queueOut = []
40 _autoblock
41 _disabledManual
42 _hasHash
43 _autorunning
44 _observer
45 _focusTrap
46 _hasContainer
47 _search = ''
48 componentName
49 ns
50 options
51 initial
52 disabled = false
53 container
54 elements = []
55 targets = []
56 index
57 direction
58
59 // slider
60 _usedWidth
61 _wrap
62 _keepHeight
63 _autoHeight
64 _groups
65 drag = {}
66 pags
67
68 /**
69 * constructor
70 * @param {Node|HTMLElement|EventTarget|Window} object Base node
71 * @param {Object} optionsCustom User options
72 * @constructor
73 */
74 constructor(object, optionsCustom = {}) {
75 const self = this
76 self.container = object
77 self._optionsCustom = optionsCustom
78 self.componentName = self.constructor.componentName
79 self._componentNs = self.componentName.replace('-', '.')
80 // set self
81 Xt._set({ name: self.componentName, el: self.container, self })
82 // init
83 self._init()
84 }
85
86 //
87 // init
88 //
89
90 /**
91 * init
92 */
93 _init() {
94 const self = this
95 // init
96 self._initVars()
97 self._initLogic()
98 }
99
100 /**
101 * init vars
102 */
103 _initVars() {
104 const self = this
105 // options
106 self._optionsDefault = Xt.merge([self.constructor.optionsDefaultSuper, self.constructor.optionsDefault])
107 self._optionsDefault = Xt.merge([self._optionsDefault, Xt.options[self.componentName]])
108 self._optionsInitial = self.options = Xt.merge([self._optionsDefault, self._optionsCustom])
109 // classes
110 const options = self.options
111 self._classes = options.class ? [...options.class.split(' ')] : []
112 self._classesIn = options.classIn ? [...options.classIn.split(' ')] : []
113 self._classesOut = options.classOut ? [...options.classOut.split(' ')] : []
114 self._classesDone = options.classDone ? [...options.classDone.split(' ')] : []
115 self._classesInitial = options.classInitial ? [...options.classInitial.split(' ')] : []
116 self._classesBefore = options.classBefore ? [...options.classBefore.split(' ')] : []
117 self._classesAfter = options.classAfter ? [...options.classAfter.split(' ')] : []
118 }
119
120 /**
121 * init logic
122 * @param {Object} params
123 * @param {Boolean} params.save Save currents
124 */
125 _initLogic({ save = true } = {}) {
126 const self = this
127 // vars
128 self._destroyElements = [document, window, self.container]
129 // enable first for proper initial activation
130 self.enable()
131 // init
132 self._initSetup()
133 Xt._initMatches({ self, optionsInitial: self._optionsInitial })
134 self._initScope()
135 self._initEvents()
136 self._initA11y()
137 self._initStart({ save })
138 // disable last for proper options.disableDeactivate
139 if (self.options.disabled || self._disabledManual) {
140 self.disable()
141 }
142 }
143
144 /**
145 * init setup
146 */
147 _initSetup() {
148 const self = this
149 const options = self.options
150 // mode
151 self._containerTargets = self.container
152 if (options.targets && options.targets.indexOf('#') !== -1) {
153 self._mode = 'unique'
154 self._containerTargets = document.documentElement
155 self.ns = `${self.componentName}-${options.targets.toString()}-${self._classes.toString()}`
156 } else {
157 self._mode = 'multiple'
158 self.ns = self.ns ?? Xt.uniqueId()
159 }
160 // final namespace
161 self.ns = self.ns.replace(/^[^a-z]+|[ ,#_:.-]+/gi, '')
162 // namespace
163 self._addNamespace()
164 // currents array based on namespace (so shared between Xt objects)
165 self._setCurrents([])
166 }
167
168 /**
169 * init elements, targets and currents
170 */
171 _initScope() {
172 const self = this
173 // elements
174 self._initScopeElements()
175 // targets
176 self._initScopeTargets()
177 }
178
179 /**
180 * init elements
181 */
182 _initScopeElements() {
183 const self = this
184 const options = self.options
185 // elements
186 self._containerElements = self.container
187 if (options.elements) {
188 if (options.elements.indexOf('#') !== -1) {
189 self._containerElements = document.documentElement
190 }
191 let arr = Array.from(self._containerElements.querySelectorAll(options.elements))
192 if (options.exclude) {
193 arr = arr.filter(x => !x.matches(options.exclude))
194 }
195 self.elements = arr
196 self._destroyElements.push(...self.elements)
197 }
198 // object if no elements
199 if (!self.elements.length) {
200 self.elements = [self.container]
201 }
202 // elementsInner
203 if (options.elementsInner) {
204 for (const el of self.elements) {
205 const elements = Xt.queryAll({ els: el, query: options.elementsInner })
206 Xt.dataStorage.set(el, `elementsInner/${self.ns}`, elements)
207 }
208 }
209 }
210
211 /**
212 * init targets
213 */
214 _initScopeTargets() {
215 const self = this
216 const options = self.options
217 // targets
218 if (options.targets) {
219 let arr = Array.from(self._containerTargets.querySelectorAll(options.targets))
220 if (options.exclude) {
221 arr = arr.filter(x => !x.matches(options.exclude))
222 }
223 self.targets = arr
224 self._destroyElements.push(...self.targets)
225 // elementsInner
226 if (options.targetsInner) {
227 for (const tr of self.targets) {
228 const elements = Xt.queryAll({ els: tr, query: options.targetsInner })
229 Xt.dataStorage.set(tr, `targetsInner/${self.ns}`, elements)
230 }
231 }
232 }
233 }
234
235 /**
236 * init start
237 * @param {Object} params
238 * @param {Boolean} params.save Save currents
239 */
240 _initStart({ save = false } = {}) {
241 const self = this
242 const options = self.options
243 // currents
244 self._setCurrents([])
245 // vars
246 let currents = 0
247 self.initial = true
248 self.index = null
249 self._oldIndex = null
250 Xt._running[self.ns] = []
251 // INSTANT ACTIVATION because we need activation classes right away (e.g.: slider inside demos toggle must be visible to get values)
252 // check initial activation
253 currents = self._initActivate({ save })
254 // if currents < min
255 const todo = options.min - currents
256 const start = 0
257 if (todo > 0) {
258 // initial
259 currents += todo
260 }
261 // todo
262 if (todo > 0) {
263 for (let i = start; i < todo; i++) {
264 const el = self.elements[i]
265 if (el) {
266 // toggle event if present because of custom listeners
267 if (options.on) {
268 const event = options.on.split(' ')[0]
269 const elEvent = self._getEventParent({ el, event })
270 elEvent.dispatchEvent(new CustomEvent(event, { detail: { force: true } }))
271 } else {
272 self._eventOn({ el, force: true })
273 }
274 }
275 }
276 }
277 // currents
278 if (save) {
279 self._initialCurrents = self._getCurrents().slice(0) // copy array with slice(0)
280 }
281 // no currents
282 if (currents === 0) {
283 // init
284 // needs frameDouble after ondone
285 Xt.frameDouble({
286 el: self.container,
287 ns: `${self.ns}Init`,
288 func: () => {
289 // fix before _initScope or slider absolute has multiple active and bugs initial calculations
290 self.container.setAttribute(`data-${self.componentName}-init`, '')
291 // dispatch event
292 self.container.dispatchEvent(new CustomEvent(`init.${self._componentNs}`))
293 // fix autostart after self.initial or it gives error on reinitialization (demos fullscreen)
294 self._eventAutostart()
295 // initial after autostart
296 self.initial = false
297 // debug
298 if (options.debug) {
299 // eslint-disable-next-line no-console
300 console.debug(`${self.componentName} init`, self)
301 }
302 },
303 })
304 }
305 }
306
307 /**
308 * init activate
309 * @param {Object} params
310 * @param {Boolean} params.save Save currents
311 * @return {Number} currents count
312 */
313 _initActivate({ save = false } = {}) {
314 const self = this
315 const options = self.options
316 // check
317 const checkClass = el => {
318 for (const c of self._classes) {
319 if (el.classList.contains(c) || el.checked) {
320 return true
321 }
322 }
323 return false
324 }
325 // check hash
326 const obj = self._hashChange({ save })
327 let currents = obj.currents ?? 0
328 // check class
329 for (const el of self.getElementsGroups()) {
330 let activated = false
331 // check if activated
332 if (save) {
333 if (options.classSkip !== true && !options.classSkip.elements) {
334 activated = checkClass(el)
335 }
336 } else if (self._initialCurrents.includes(el)) {
337 activated = true
338 }
339 // check if activated
340 // fix check options.max for currents of _hashChange current reset if hash has current
341 // fix check obj.arr has element already activated
342 if ((activated && currents < options.max) || obj.arr.includes(el)) {
343 // instant animation
344 el.classList.add(...self._classes)
345 el.classList.add(...self._classesIn)
346 el.classList.add(...self._classesInitial)
347 } else {
348 // reset classes
349 if (options.classSkip !== true && !options.classSkip.elements) {
350 const elsSame = self.getElements({ el })
351 for (const elSame of elsSame) {
352 elSame.classList.remove(
353 ...self._classes,
354 ...self._classesIn,
355 ...self._classesOut,
356 ...self._classesDone,
357 ...self._classesInitial,
358 ...self._classesBefore,
359 ...self._classesAfter
360 )
361 }
362 }
363 if (options.elementsInner) {
364 if (options.classSkip !== true && !options.classSkip.elementsInner) {
365 const elementsInner = Xt.dataStorage.get(el, `elementsInner/${self.ns}`)
366 for (const elementInner of elementsInner) {
367 elementInner.classList.remove(
368 ...self._classes,
369 ...self._classesIn,
370 ...self._classesOut,
371 ...self._classesDone,
372 ...self._classesInitial,
373 ...self._classesBefore,
374 ...self._classesAfter
375 )
376 }
377 }
378 }
379 }
380 // check targets
381 const targets = self.getTargets({ el })
382 for (const tr of targets) {
383 // check if activated
384 if (save && !activated) {
385 if (options.classSkip !== true && !options.classSkip.targets) {
386 activated = checkClass(tr)
387 }
388 }
389 // check if activated
390 // fix check options.max for currents of _hashChange current reset if hash has current
391 // fix check tr with same activation
392 const els = self.getElements({ el: tr, same: true })
393 if ((activated && currents < options.max) || obj.arr.some(x => els.includes(x))) {
394 // instant animation
395 tr.classList.add(...self._classes)
396 tr.classList.add(...self._classesIn)
397 tr.classList.add(...self._classesInitial)
398 } else {
399 // reset classes
400 if (options.classSkip !== true && !options.classSkip.targets) {
401 tr.classList.remove(
402 ...self._classes,
403 ...self._classesIn,
404 ...self._classesOut,
405 ...self._classesDone,
406 ...self._classesInitial,
407 ...self._classesBefore,
408 ...self._classesAfter
409 )
410 }
411 if (options.targetsInner) {
412 if (options.classSkip !== true && !options.classSkip.targetsInner) {
413 const targetsInner = Xt.dataStorage.get(tr, `targetsInner/${self.ns}`)
414 for (const targetInner of targetsInner) {
415 targetInner.classList.remove(
416 ...self._classes,
417 ...self._classesIn,
418 ...self._classesOut,
419 ...self._classesDone,
420 ...self._classesInitial,
421 ...self._classesBefore,
422 ...self._classesAfter
423 )
424 }
425 }
426 }
427 }
428 }
429 // activate
430 if (activated && currents < options.max) {
431 // initial
432 currents++
433 // fix check tr with same activation
434 obj.arr.push(el)
435 // toggle event if present because of custom listeners
436 if (options.on) {
437 const event = options.on.split(' ')[0]
438 const elEvent = self._getEventParent({ el, event })
439 elEvent.dispatchEvent(new CustomEvent(event, { detail: { force: true } }))
440 } else {
441 self._eventOn({ el, force: true })
442 }
443 }
444 }
445 // return
446 return currents
447 }
448
449 /**
450 * init events
451 */
452 _initEvents() {
453 const self = this
454 const options = self.options
455 // remove events
456 self._removeEvents()
457 // elements
458 for (const el of self.elements) {
459 // event on
460 const onHandlerCustom = Xt.dataStorage.put(
461 el,
462 `${options.on}/oncustom/${self.ns}`,
463 self._eventOnHandler.bind(self, { el, force: true })
464 )
465 el.addEventListener(`on.trigger.${self._componentNs}`, onHandlerCustom)
466 if (options.on) {
467 const events = [...options.on.split(' ')]
468 for (const event of events) {
469 const elEvent = self._getEventParent({ el, event })
470 if (elEvent !== el) {
471 self._destroyElements.push(elEvent)
472 }
473 const onHandler = Xt.dataStorage.put(
474 elEvent,
475 `${options.on}/on/${self.ns}`,
476 self._eventOnHandler.bind(self, { el })
477 )
478 elEvent.addEventListener(event, onHandler)
479 }
480 }
481 // event off
482 const offHandlerCustom = Xt.dataStorage.put(
483 el,
484 `${options.off}/offcustom/${self.ns}`,
485 self._eventOffHandler.bind(self, { el, force: true })
486 )
487 el.addEventListener(`off.trigger.${self._componentNs}`, offHandlerCustom)
488 if (options.off) {
489 const events = [...options.off.split(' ')]
490 for (const event of events) {
491 // same event for on and off same namespace
492 if (![...options.on.split(' ')].includes(event)) {
493 const elEvent = self._getEventParent({ el, event })
494 if (elEvent !== el) {
495 self._destroyElements.push(elEvent)
496 }
497 const offHandler = Xt.dataStorage.put(
498 elEvent,
499 `${options.off}/off/${self.ns}`,
500 self._eventOffHandler.bind(self, { el })
501 )
502 elEvent.addEventListener(event, offHandler)
503 }
504 }
505 }
506 // preventEvent
507 if (options.on) {
508 if (options.preventEvent) {
509 const events = [...options.on.split(' ')]
510 if (events.includes('click') || events.includes('mouseenter') || events.includes('mousehover')) {
511 // prevent touch links
512 const preventeventStartHandler = Xt.dataStorage.put(
513 el,
514 `touchend/preventevent/${self.ns}`,
515 self._eventPreventeventStartHandler.bind(self, { el })
516 )
517 el.addEventListener('touchend', preventeventStartHandler)
518 }
519 if (events.includes('click')) {
520 // prevent click links
521 const preventeventStartHandler = Xt.dataStorage.put(
522 el,
523 `mouseup keyup/preventevent/${self.ns}`,
524 self._eventPreventeventStartHandler.bind(self, { el })
525 )
526 el.addEventListener('mouseup', preventeventStartHandler)
527 el.addEventListener('keyup', preventeventStartHandler)
528 }
529 }
530 Xt.dataStorage.put(el, `active/preventevent/${self.ns}`, self.hasCurrent({ el }))
531 }
532 }
533 // targets
534 for (const tr of self.targets) {
535 // event on
536 const onHandlerCustom = Xt.dataStorage.put(
537 tr,
538 `${options.on}/oncustom/${self.ns}`,
539 self._eventOnHandler.bind(self, { el: tr, force: true })
540 )
541 tr.addEventListener(`on.trigger.${self._componentNs}`, onHandlerCustom)
542 // event off
543 const offHandlerCustom = Xt.dataStorage.put(
544 tr,
545 `${options.off}/offcustom/${self.ns}`,
546 self._eventOffHandler.bind(self, { el: tr, force: true })
547 )
548 tr.addEventListener(`off.trigger.${self._componentNs}`, offHandlerCustom)
549 }
550 // auto
551 if (options.auto && options.auto.time) {
552 const autostartHandler = Xt.dataStorage.put(
553 self.container,
554 `autostart/${self.ns}`,
555 self._eventAutostart.bind(self)
556 )
557 const autostopHandler = Xt.dataStorage.put(self.container, `autostop/${self.ns}`, self._eventAutostop.bind(self))
558 // focus
559 // Xt.dataStorage.set with window to fix unique mode same self.ns
560 const focusHandler = Xt.dataStorage.set(window, `focus/auto/${self.ns}`, autostartHandler)
561 addEventListener('focus', focusHandler)
562 // blur
563 // Xt.dataStorage.set with window to fix unique mode same self.ns
564 const blurHandler = Xt.dataStorage.set(window, `blur/auto/${self.ns}`, autostopHandler)
565 addEventListener('blur', blurHandler)
566 // event
567 self.container.addEventListener(`autostart.trigger.${self._componentNs}`, autostartHandler)
568 self.container.addEventListener(`autostop.trigger.${self._componentNs}`, autostopHandler)
569 // autopause
570 if (options.auto.pause) {
571 const autopauseEls = self.container.querySelectorAll(options.auto.pause)
572 if (autopauseEls.length) {
573 self._destroyElements.push(...autopauseEls)
574 for (const el of autopauseEls) {
575 // pause
576 const autopauseOnHandler = Xt.dataStorage.put(
577 el,
578 `mouseenter focus/auto/${self.ns}`,
579 self._eventAutostop.bind(self)
580 )
581 const eventsPause = ['mouseenter', 'focus']
582 for (const event of eventsPause) {
583 el.addEventListener(event, autopauseOnHandler)
584 }
585 // resume
586 const autoresumeOnHandler = Xt.dataStorage.put(
587 el,
588 `mouseleave blur/auto/${self.ns}`,
589 self._eventAutostart.bind(self)
590 )
591 const eventsResume = ['mouseleave', 'blur']
592 for (const event of eventsResume) {
593 el.addEventListener(event, autoresumeOnHandler)
594 }
595 }
596 }
597 }
598 }
599 // hash
600 if (options.hash) {
601 for (const el of self.elements) {
602 if (el.getAttribute(options.hash)) {
603 self._hasHash = true
604 break
605 }
606 }
607 if (!self._hasHash) {
608 for (const tr of self.targets) {
609 if (tr.getAttribute(options.hash)) {
610 self._hasHash = true
611 break
612 }
613 }
614 }
615 }
616 if (self._hasHash) {
617 // hash
618 const hashHandler = Xt.dataStorage.put(
619 window,
620 `popstate/${self.ns}`,
621 self._hashChange.bind(self).bind(self, { save: true })
622 )
623 addEventListener('popstate', hashHandler)
624 }
625 // jump
626 if (options.jump) {
627 for (const el of self.targets) {
628 const jumpHandler = Xt.dataStorage.put(
629 el,
630 `click/jump/${self.ns}`,
631 self._eventJumpHandler.bind(self).bind(self, { el })
632 )
633 el.addEventListener('click', jumpHandler, true) // fix elements inside targets (slider pagination)
634 // jump
635 if (!self.disabled) {
636 el.classList.add('xt-jump')
637 }
638 }
639 }
640 // navigation
641 if (options.navigation) {
642 self.navs = self.container.querySelectorAll(options.navigation)
643 if (self.navs.length) {
644 self._destroyElements.push(...self.navs)
645 for (const el of self.navs) {
646 const navHandler = Xt.dataStorage.put(
647 el,
648 `click/nav/${self.ns}`,
649 self._eventNavHandler.bind(self).bind(self, { el })
650 )
651 el.addEventListener('click', navHandler)
652 }
653 }
654 }
655 // closeauto
656 if (options.closeauto) {
657 // Xt.dataStorage.set with window to fix unique mode same self.ns
658 const closeautoHandler = Xt.dataStorage.set(
659 window,
660 `closeauto.trigger.xt/${self.ns}`,
661 self._eventCloseautoHandler.bind(self)
662 )
663 addEventListener('closeauto.trigger.xt', closeautoHandler, true) // useCapture event propagation
664 }
665 if (options.openauto) {
666 // Xt.dataStorage.set with window to fix unique mode same self.ns
667 const openautoHandler = Xt.dataStorage.set(
668 window,
669 `openauto.trigger.xt/${self.ns}`,
670 self._eventOpenautoHandler.bind(self)
671 )
672 addEventListener('openauto.trigger.xt', openautoHandler, true) // useCapture event propagation
673 }
674 // mediaLoaded
675 if (options.mediaLoaded || options.mediaLoadedReinit) {
676 for (const el of self.elements) {
677 const imgs = Array.from(el.querySelectorAll('img'))
678 self._destroyElements.push(...imgs)
679 for (const img of imgs) {
680 if (!Xt.dataStorage.get(img, `${self.ns}MedialoadedDone`)) {
681 Xt.dataStorage.set(img, `${self.ns}MedialoadedDone`, true) // useCapture event propagation
682 if (!img.complete) {
683 const medialoadedHandler = Xt.dataStorage.put(
684 img,
685 `load/media/${self.ns}`,
686 self._eventMedialoadedHandler.bind(self).bind(self, { img, el, deferred: true })
687 )
688 img.addEventListener('load', medialoadedHandler)
689 } else {
690 self._eventMedialoadedHandler({ img, el })
691 }
692 }
693 }
694 }
695 for (const tr of self.targets) {
696 const imgs = Array.from(tr.querySelectorAll('img'))
697 self._destroyElements.push(...imgs)
698 for (const img of imgs) {
699 if (!Xt.dataStorage.get(img, `${self.ns}MedialoadedDone`)) {
700 if (!img.complete) {
701 const medialoadedHandler = Xt.dataStorage.put(
702 img,
703 `load/media/${self.ns}`,
704 self._eventMedialoadedHandler.bind(self).bind(self, { img, el: tr, deferred: true, reinit: true })
705 )
706 img.addEventListener('load', medialoadedHandler)
707 } else {
708 self._eventMedialoadedHandler({ img, el: tr })
709 }
710 }
711 }
712 }
713 }
714 // visibleReinit
715 if (options.visibleReinit) {
716 if (!Xt.visible({ el: self.container })) {
717 // intersection observer
718 self._observer = new IntersectionObserver(
719 (entries, observer) => {
720 for (const entry of entries) {
721 if (entry.intersectionRatio > 0) {
722 self._eventVisibleReinit()
723 observer.disconnect()
724 self._observer = null
725 }
726 }
727 },
728 { root: null }
729 )
730 self._observer.observe(self.container)
731 }
732 }
733 }
734
735 //
736 // handler
737 //
738
739 /**
740 * element on handler
741 * @param {Object} params
742 * @param {Node|HTMLElement|EventTarget|Window} params.el
743 * @param {Boolean} params.force
744 * @param {Event} e
745 */
746 _eventOnHandler({ el, force = false }, e) {
747 const self = this
748 const options = self.options
749 force = force ? force : e?.detail?.force
750 // fix groupElements and targets
751 el = options.groupElements || self.targets.includes(el) ? self.getElements({ el })[0] : el
752 // handler
753 if (!force && options.eventLimit) {
754 const eventLimit = self._containerElements.querySelectorAll(options.eventLimit)
755 if (self._containerElements.matches(options.eventLimit)) {
756 return
757 } else if (eventLimit.length) {
758 if (Xt.contains({ els: eventLimit, tr: e.target })) {
759 return
760 }
761 }
762 }
763 self._eventOn({ el, force }, e)
764 }
765
766 /**
767 * element off handler
768 * @param {Object} params
769 * @param {Node|HTMLElement|EventTarget|Window} params.el
770 * @param {Boolean} params.force
771 * @param {Event} e
772 */
773 _eventOffHandler({ el, force = false }, e) {
774 const self = this
775 const options = self.options
776 force = force ? force : e?.detail?.force
777 // fix groupElements and targets
778 el = options.groupElements || self.targets.includes(el) ? self.getElements({ el })[0] : el
779 // handler
780 if (!force && options.eventLimit) {
781 const eventLimit = self._containerElements.querySelectorAll(options.eventLimit)
782 if (self._containerElements.matches(options.eventLimit)) {
783 return
784 } else if (eventLimit.length) {
785 if (Xt.contains({ els: eventLimit, tr: e.target })) {
786 return
787 }
788 }
789 }
790 self._eventOff({ el, force }, e)
791 }
792
793 /**
794 * init prevents click on touch until clicked two times
795 * @param {Object} params
796 * @param {Node|HTMLElement|EventTarget|Window} params.el
797 */
798 _eventPreventeventStartHandler({ el } = {}) {
799 const self = this
800 // active
801 Xt.dataStorage.put(el, `active/preventevent/${self.ns}`, self.hasCurrent({ el }))
802 // prevent link but execute on.xt because added before
803 const preventeventHandler = Xt.dataStorage.put(
804 el,
805 `click keypress/preventevent/${self.ns}`,
806 self._eventPreventeventHandler.bind(self, { el })
807 )
808 el.addEventListener('click', preventeventHandler)
809 el.addEventListener('keypress', preventeventHandler)
810 // reset prevent event
811 const preventeventResetHandler = Xt.dataStorage.put(
812 el,
813 `off/preventevent/${self.ns}`,
814 self._eventPreventeventResetHandler.bind(self, { el })
815 )
816 el.addEventListener(`off.${self._componentNs}`, preventeventResetHandler)
817 }
818
819 /**
820 * remove prevents click on touch until clicked two times
821 * @param {Object} params
822 * @param {Node|HTMLElement|EventTarget|Window} params.el
823 */
824 _eventPreventeventEndHandler({ el } = {}) {
825 const self = this
826 // event link
827 const preventeventHandler = Xt.dataStorage.get(el, `click/preventevent/${self.ns}`)
828 el.removeEventListener('click', preventeventHandler)
829 // event reset
830 const preventeventResetHandler = Xt.dataStorage.get(el, `off/preventevent/${self.ns}`)
831 el.removeEventListener(`off.${self._componentNs}`, preventeventResetHandler)
832 }
833
834 /**
835 * prevents click on touch until clicked two times
836 * @param {Object} params
837 * @param {Node|HTMLElement|EventTarget|Window} params.el
838 * @param {Event} e
839 */
840 _eventPreventeventHandler({ el }, e) {
841 const self = this
842 const active = Xt.dataStorage.get(el, `active/preventevent/${self.ns}`)
843 // only no key or key enter
844 if (e.key && e.key !== 'Enter') {
845 return
846 }
847 // logic
848 if (!active && !Xt.dataStorage.get(el, `${self.ns}PreventeventDone`)) {
849 Xt.dataStorage.set(el, `${self.ns}PreventeventDone`, true)
850 // prevent default
851 e.preventDefault()
852 } else {
853 self._eventPreventeventEndHandler({ el })
854 Xt.dataStorage.remove(el, `${self.ns}PreventeventDone`)
855 Xt.dataStorage.remove(el, `active/preventevent/${self.ns}`)
856 }
857 }
858
859 /**
860 * reset prevents click on touch until clicked two times
861 * @param {Object} params
862 * @param {Node|HTMLElement|EventTarget|Window} params.el
863 */
864 _eventPreventeventResetHandler({ el } = {}) {
865 const self = this
866 self._eventPreventeventEndHandler({ el })
867 Xt.dataStorage.remove(el, `${self.ns}PreventeventDone`)
868 Xt.dataStorage.remove(el, `active/preventevent/${self.ns}`)
869 }
870
871 /**
872 * hash change
873 * @param {Object} params
874 * @param {Boolean} params.save Save currents
875 * @return {Object} return
876 * @return {Number} return.currents
877 * @return {Array} return.arr
878 */
879 _hashChange({ save = false } = {}) {
880 const self = this
881 const options = self.options
882 // vars
883 let currents = 0
884 const arr = []
885 // disabled
886 if (self.disabled) {
887 return { currents, arr }
888 }
889 // logic
890 if (self._hasHash) {
891 if (!Xt.dataStorage.get(self.container, `${self.ns}HashSkip`)) {
892 const hash = decodeURI(location.hash.split('#')[1])
893 if (hash) {
894 // check
895 const checkHash = (el, hash) => {
896 if (el.getAttribute(options.hash) === hash) {
897 return true
898 }
899 return false
900 }
901 // check hash
902 for (const el of self.elements) {
903 let activated = false
904 // check if activated
905 if (save) {
906 activated = checkHash(el, hash)
907 }
908 // check targets
909 const targets = self.getTargets({ el })
910 for (const tr of targets) {
911 // check if activated
912 if (save && !activated) {
913 activated = checkHash(tr, hash)
914 }
915 }
916 // activate
917 if (activated && currents < options.max) {
918 // initial
919 currents++
920 arr.push(el)
921 // toggle event if present because of custom listeners
922 Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, true)
923 if (options.on) {
924 const event = options.on.split(' ')[0]
925 el.dispatchEvent(new CustomEvent(event, { detail: { force: true } }))
926 } else {
927 self._eventOn({ el, force: true })
928 }
929 Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, false)
930 }
931 }
932 }
933 }
934 }
935 // return
936 return { currents, arr }
937 }
938
939 /**
940 * jump handler
941 * @param {Object} params
942 * @param {Node|HTMLElement|EventTarget|Window} params.el
943 * @param {Event} e
944 */
945 _eventJumpHandler({ el }, e) {
946 const self = this
947 // disabled
948 if (self.disabled) {
949 return
950 }
951 // useCapture event propagation check
952 if (self.targets.includes(el)) {
953 // handler
954 self._eventJump({ el }, e)
955 }
956 }
957
958 /**
959 * nav handler
960 * @param {Object} params
961 * @param {Node|HTMLElement|EventTarget|Window} params.el
962 * @param {Event} e
963 */
964 _eventNavHandler({ el }, e) {
965 const self = this
966 // handler
967 self._eventNav({ el }, e)
968 }
969
970 /**
971 * closeauto handler
972 * @param {Event} e
973 */
974 _eventCloseautoHandler(e) {
975 const self = this
976 // triggering e.detail.container
977 if (!e?.detail?.container || e?.detail?.container.contains(self.container)) {
978 // handler
979 const currents = self._getCurrents()
980 for (const current of currents) {
981 self._eventOff({ el: current, force: true }, e)
982 }
983 }
984 }
985
986 /**
987 * openauto handler
988 * @param {Event} e
989 */
990 _eventOpenautoHandler(e) {
991 const self = this
992 // handler
993 let found
994 for (const el of Array.from(self.elements).filter(x => x.contains(e.target))) {
995 found = el
996 break
997 }
998 if (!found) {
999 for (const tr of Array.from(self.targets).filter(x => x.contains(e.target))) {
1000 found = tr
1001 break
1002 }
1003 }
1004 if (found) {
1005 self._eventOn({ el: found }, e)
1006 }
1007 }
1008
1009 /**
1010 * medialoaded
1011 * @param {Object} params
1012 * @param {Node|HTMLElement|EventTarget|Window} params.img
1013 * @param {Node|HTMLElement|EventTarget|Window} params.el
1014 * @param {Boolean} params.deferred
1015 * @param {Boolean} params.reinit
1016 */
1017 _eventMedialoadedHandler({ img, el, deferred = false, reinit = false } = {}) {
1018 const self = this
1019 const options = self.options
1020 // fix multiple calls
1021 Xt.dataStorage.set(img, `${self.ns}MedialoadedDone`, true)
1022 // mediaLoadedReinit
1023 if (options.mediaLoadedReinit && deferred && reinit) {
1024 clearTimeout(Xt.dataStorage.get(self.container, `${self.ns}MedialoadedTimeout`))
1025 Xt.dataStorage.set(
1026 self.container,
1027 `${self.ns}MedialoadedTimeout`,
1028 setTimeout(() => {
1029 self._eventMediaLoadedReinit()
1030 }, Xt.medialoadedDelay)
1031 )
1032 }
1033 // mediaLoaded
1034 if (options.mediaLoaded) {
1035 el.classList.add('xt-medialoaded')
1036 }
1037 // dispatch event
1038 el.dispatchEvent(
1039 new CustomEvent(`medialoaded.${self._componentNs}`, {
1040 detail: { deferred: deferred },
1041 })
1042 )
1043 }
1044
1045 //
1046 // event util
1047 //
1048
1049 /**
1050 * Get all elements from element or target
1051 * @return {Array} array of elements
1052 */
1053 getElementsGroups() {
1054 const self = this
1055 // groups
1056 const groups = []
1057 for (const el of self.elements) {
1058 // choose element by group
1059 const group = el.getAttribute('data-xt-group')
1060 if (group) {
1061 const alreadyFound = groups.filter(x => x.getAttribute('data-xt-group') === group)
1062 if (!alreadyFound.length) {
1063 groups.push(el)
1064 }
1065 } else {
1066 groups.push(el)
1067 }
1068 }
1069 return groups
1070 }
1071
1072 /**
1073 * filter elements or targets array with groups array
1074 * @param {Object} params
1075 * @param {Array} params.els Elements or Targets
1076 * @param {String} params.attr Groups attribute
1077 * @param {Boolean} params.some Filter also if some in Groups attribute
1078 * @param {Boolean} params.same Use also data-xt-group-same
1079 * @return {Array} Filtered array
1080 */
1081 _groupFilter({ els, attr, some = false, same = false } = {}) {
1082 const self = this
1083 const options = self.options
1084 // logic
1085 const found = []
1086 for (const el of els) {
1087 let currentAttr = el.getAttribute('data-xt-group')
1088 if (same) {
1089 const currentAttrSame = el.getAttribute('data-xt-group-same')
1090 if (currentAttrSame) {
1091 currentAttr += options.groupSeparator + currentAttrSame
1092 }
1093 }
1094 // if same attr
1095 if (currentAttr === attr) {
1096 found.push(el)
1097 continue
1098 }
1099 // if some in attr
1100 if (some) {
1101 const groups = attr?.split(options.groupSeparator).filter(x => x) // filter out nullish
1102 const currentGroups = currentAttr?.split(options.groupSeparator).filter(x => x) // filter out nullish
1103 if (currentGroups && groups && currentGroups.some(x => groups.includes(x))) {
1104 found.push(el)
1105 }
1106 }
1107 }
1108 return found
1109 }
1110
1111 /**
1112 * get elements from element or target
1113 * @param {Object} params
1114 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
1115 * @param {Boolean} params.same Use also data-xt-group-same
1116 * @return {Array} The first element is the one on getElementsGroups()
1117 */
1118 getElements({ el, same = false } = {}) {
1119 const self = this
1120 const options = self.options
1121 // getElements
1122 if (!self.elements || !self.elements.length) {
1123 return []
1124 }
1125 if (!el) {
1126 return []
1127 } else if (self._mode === 'unique') {
1128 // xtNamespace linked components
1129 const final = []
1130 const selfs = Xt.dataStorage.get(self.ns, 'xtNamespace')
1131 if (selfs) {
1132 for (const s of selfs) {
1133 // choose element by group
1134 final.push(...s.elements)
1135 }
1136 return final
1137 }
1138 return []
1139 } else if (self._mode === 'multiple') {
1140 // choose element by group
1141 let final
1142 let attr = el.getAttribute('data-xt-group')
1143 if (same) {
1144 const attrSame = el.getAttribute('data-xt-group-same')
1145 if (attrSame) {
1146 attr += options.groupSeparator + attrSame
1147 }
1148 }
1149 const some = self.elements.includes(el) ? false : true // data-xt-group some only if finding elements from targets
1150 const groupElements = self._groupFilter({ els: self.elements, attr, some, same })
1151 const groupTargets = self._groupFilter({ els: self.targets, attr, some, same })
1152 if (attr) {
1153 // if group all group targets
1154 final = groupElements
1155 } else {
1156 // not group targets by index
1157 if (Array.from(self.elements).includes(el)) {
1158 final = [el].filter(x => x) // filter out nullish
1159 } else {
1160 // groupElements and groupTargets are elements and targets without data-xt-group here
1161 const index = groupTargets.findIndex(x => x === el)
1162 final = [groupElements[index]].filter(x => x) // filter out nullish
1163 }
1164 }
1165 return final
1166 }
1167 }
1168
1169 /**
1170 * Get all targets from element or target
1171 * @param {Object} params
1172 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
1173 * @param {Boolean} params.same Use also data-xt-group-same
1174 * @return {Array}
1175 */
1176 getTargets({ el, same = false } = {}) {
1177 const self = this
1178 const options = self.options
1179 // getTargets
1180 if (!self.targets || !self.targets.length) {
1181 return []
1182 }
1183 if (!el) {
1184 return []
1185 } else if (self._mode === 'unique') {
1186 // xtNamespace linked components
1187 const final = self.targets
1188 return final
1189 } else if (self._mode === 'multiple') {
1190 // choose only target by group
1191 let final
1192 let attr = el.getAttribute('data-xt-group')
1193 if (same) {
1194 const attrSame = el.getAttribute('data-xt-group-same')
1195 if (attrSame) {
1196 attr += options.groupSeparator + attrSame
1197 }
1198 }
1199 const some = self.targets.includes(el) ? false : true // data-xt-group some only if finding targets from elements
1200 const groupElements = self._groupFilter({ els: self.elements, attr, some, same })
1201 const groupTargets = self._groupFilter({ els: self.targets, attr, some, same })
1202 if (attr) {
1203 // if group all group targets
1204 final = groupTargets
1205 } else {
1206 // not group targets by index
1207 if (Array.from(self.targets).includes(el)) {
1208 final = [el].filter(x => x) // filter out nullish
1209 } else {
1210 // groupElements and groupTargets are elements and targets without data-xt-group here
1211 const index = groupElements.findIndex(x => x === el)
1212 final = [groupTargets[index]].filter(x => x) // filter out nullish
1213 }
1214 }
1215 return final
1216 }
1217 }
1218
1219 /**
1220 * Get elements inner
1221 * @param {Object} params
1222 * @param {Node|HTMLElement|EventTarget|Window} params.els Elements
1223 * @return {Array}
1224 */
1225 _getElementsInner({ els } = {}) {
1226 const self = this
1227 const options = self.options
1228 // inners
1229 let inners = []
1230 if (options.elementsInner) {
1231 for (const el of els) {
1232 const inner = Xt.dataStorage.get(el, `elementsInner/${self.ns}`)
1233 if (inner.length) {
1234 inners = inners.concat(inner)
1235 }
1236 }
1237 }
1238 return inners
1239 }
1240
1241 /**
1242 * Get targets inner
1243 * @param {Object} params
1244 * @param {Node|HTMLElement|EventTarget|Window} params.els Elements
1245 * @return {Array}
1246 */
1247 _getTargetsInner({ els } = {}) {
1248 const self = this
1249 const options = self.options
1250 // inners
1251 let inners = []
1252 if (options.targetsInner) {
1253 for (const el of els) {
1254 const inner = Xt.dataStorage.get(el, `targetsInner/${self.ns}`)
1255 if (inner.length) {
1256 inners = inners.concat(inner)
1257 }
1258 }
1259 }
1260 return inners
1261 }
1262
1263 /**
1264 * get event parent
1265 * @param {Object} params
1266 * @param {Node|HTMLElement|EventTarget|Window} params.el
1267 * @param {String} params.event
1268 * @return {Node|HTMLElement|EventTarget|Window}
1269 */
1270 _getEventParent({ el, event } = {}) {
1271 const self = this
1272 const options = self.options
1273 // getCurrents
1274 if (options.mouseParent) {
1275 if (['mouseenter', 'mouseleave', 'mousehover', 'mouseout'].includes(event)) {
1276 if (typeof options.mouseParent === 'string') {
1277 return el.closest(options.mouseParent)
1278 } else {
1279 return el.parentNode
1280 }
1281 }
1282 }
1283 return el
1284 }
1285 /**
1286 * get currents based on namespace (so shared between Xt objects)
1287 * @return {Array}
1288 */
1289 _getCurrents() {
1290 const self = this
1291 // getCurrents
1292 return Xt._currents[self.ns]
1293 }
1294
1295 /**
1296 * set currents based on namespace (so shared between Xt objects)
1297 * @param {Array} arr
1298 */
1299 _setCurrents(arr) {
1300 const self = this
1301 // setCurrents
1302 Xt._currents[self.ns] = arr
1303 }
1304
1305 /**
1306 * add current based on namespace (so shared between Xt objects)
1307 * @param {Object} params
1308 * @param {Node|HTMLElement|EventTarget|Window} params.el
1309 * @param {Boolean} params.running Running currents
1310 */
1311 _addCurrent({ el, running = false } = {}) {
1312 const self = this
1313 // addCurrent
1314 if (!self.hasCurrent({ el, running })) {
1315 const arr = running ? Xt._running : Xt._currents
1316 arr[self.ns].push(el)
1317 }
1318 }
1319
1320 /**
1321 * remove currents based on namespace (so shared between Xt objects)
1322 * @param {Object} params
1323 * @param {Node|HTMLElement|EventTarget|Window} params.el To be removed
1324 * @param {Boolean} params.running Running currents
1325 */
1326 _removeCurrent({ el, running = false } = {}) {
1327 const self = this
1328 // removeCurrent
1329 const arr = running ? Xt._running : Xt._currents
1330 arr[self.ns] = arr[self.ns].filter(x => x !== el)
1331 }
1332
1333 /**
1334 * Check if element or target is activated
1335 * @param {Object} params
1336 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
1337 * @param {Boolean} params.same Use also data-xt-group-same
1338 * @param {Boolean} params.running Running currents
1339 */
1340 hasCurrent({ el, same = false, running = false } = {}) {
1341 const self = this
1342 const options = self.options
1343 // fix groupElements and targets
1344 const elements = options.groupElements || self.targets.includes(el) ? self.getElements({ el, same }) : [el]
1345 // hasCurrent
1346 const arr = running ? Xt._running : Xt._currents
1347 return arr[self.ns].filter(x => elements.includes(x)).length
1348 }
1349
1350 /**
1351 * check element on
1352 * @param {Object} params
1353 * @param {Node|HTMLElement|EventTarget|Window} params.el To be checked
1354 * @return {Boolean} If elements can activate
1355 */
1356 _checkOn({ el } = {}) {
1357 const self = this
1358 // check
1359 return !self.hasCurrent({ el })
1360 }
1361
1362 /**
1363 * check element off
1364 * @param {Object} params
1365 * @param {Node|HTMLElement|EventTarget|Window} params.el To be checked
1366 * @return {Boolean} If elements can deactivate
1367 */
1368 _checkOff({ el } = {}) {
1369 const self = this
1370 const options = self.options
1371 // skip if min >= currents
1372 if (options.min - self._getCurrents().length >= 0) {
1373 return false
1374 }
1375 // check
1376 return self.hasCurrent({ el })
1377 }
1378
1379 /**
1380 * check element on
1381 * @param {Object} params
1382 * @param {Object} params.obj Queue object to end
1383 * @return {Boolean} If elements can activate
1384 */
1385 _checkOnRunning({ obj } = {}) {
1386 const self = this
1387 // running check to stop multiple activation/deactivation with delay
1388 const check = obj.elements.runningOn || !self.hasCurrent({ el: obj.elements.queueEls[0], running: true })
1389 obj.elements.runningOn = check
1390 return check
1391 }
1392
1393 /**
1394 * check element off running
1395 * @param {Object} params
1396 * @param {Object} params.obj Queue object to end
1397 * @return {Boolean} If elements can activate
1398 */
1399 _checkOffRunning({ obj } = {}) {
1400 const self = this
1401 // running check to stop multiple activation/deactivation with delay
1402 const check = obj.elements.runningOff || self.hasCurrent({ el: obj.elements.queueEls[0], running: true })
1403 obj.elements.runningOff = check
1404 return check
1405 }
1406
1407 /**
1408 * set index
1409 * @param {Object} params
1410 * @param {Node|HTMLElement|EventTarget|Window} params.el
1411 */
1412 _setIndex({ el } = {}) {
1413 const self = this
1414 // set index
1415 const index = self.getIndex({ el })
1416 self._oldIndex = self.index ?? index
1417 self.index = index
1418 }
1419
1420 /**
1421 * get index
1422 * @param {Object} params
1423 * @param {Node|HTMLElement|EventTarget|Window} params.el
1424 */
1425 getIndex({ el } = {}) {
1426 const self = this
1427 // fix groupElements and targets
1428 el = self.getElements({ el })[0]
1429 // set index
1430 let index = null
1431 for (const [i, element] of self.getElementsGroups().entries()) {
1432 if (el === element) {
1433 index = i
1434 break
1435 }
1436 }
1437 return index
1438 }
1439
1440 /**
1441 * set direction
1442 */
1443 _setDirection() {
1444 const self = this
1445 // set direction
1446 if (self.index === null || self.index === self._oldIndex) {
1447 // initial direction and same index direction
1448 self.direction = 0
1449 } else if (self._inverse !== null) {
1450 // forced value
1451 self.direction = self._inverse ? -1 : 1
1452 } else {
1453 self.direction = self.index < self._oldIndex ? -1 : 1
1454 }
1455 }
1456
1457 /**
1458 * activate element
1459 * @param {Object} params
1460 * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be activated
1461 * @param {String} params.type Type of element
1462 * @param {Boolean} params.skipSame If skip activation classes and events
1463 */
1464 _activate({ el, type, skipSame } = {}) {
1465 const self = this
1466 const options = self.options
1467 // activation
1468 if (!skipSame && options.classSkip !== true && !options.classSkip[type]) {
1469 // input
1470 el.checked = true
1471 // activation
1472 el.classList.add(...self._classes)
1473 el.classList.remove(...self._classesOut)
1474 // needs TWO raf or sequential off/on flickr (e.g. display)
1475 Xt.frameDouble({
1476 el,
1477 func: () => {
1478 el.classList.add(...self._classesIn)
1479 el.classList.remove(...self._classesDone)
1480 },
1481 })
1482 // direction
1483 el.classList.remove(...self._classesBefore, ...self._classesAfter)
1484 if (self.direction < 0) {
1485 el.classList.add(...self._classesBefore)
1486 } else if (self.direction > 0) {
1487 el.classList.add(...self._classesAfter)
1488 }
1489 }
1490 }
1491
1492 /**
1493 * activate element done
1494 * @param {Object} params
1495 * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated
1496 * @param {String} params.type Type of element
1497 * @param {Boolean} params.skipSame If skip activation classes and events
1498 */
1499 _activateDone({ el, type, skipSame } = {}) {
1500 const self = this
1501 const options = self.options
1502 // activation
1503 if (!skipSame && options.classSkip !== true && !options.classSkip[type]) {
1504 // fix need to repeat inside frameDouble in case we cancel
1505 Xt.frameDouble({ el })
1506 el.classList.add(...self._classesIn, ...self._classesDone)
1507 }
1508 }
1509
1510 /**
1511 * activate hash
1512 * @param {Object} params
1513 * @param {Object} params.obj Queue object
1514 * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be activated
1515 * @param {String} type Type of element
1516 */
1517 _activateHash({ obj, el, type } = {}) {
1518 const self = this
1519 const options = self.options
1520 // hash
1521 if (!Xt.dataStorage.get(self.container, `${self.ns}HashSkip`)) {
1522 if (self._hasHash && !self.initial) {
1523 // fix no data-xt-group-same
1524 const elMain = obj.elements.queueEls[0]
1525 if (
1526 (type === 'elements' && self.getElements({ el: elMain }).includes(el)) ||
1527 (type === 'targets' && self.getTargets({ el: elMain }).includes(el))
1528 ) {
1529 const attr = el.getAttribute(options.hash)
1530 if (attr) {
1531 // raf prevents hash on chained activations (e.g: multiple hash on elements with same activation)
1532 Xt.frame({
1533 el: window,
1534 ns: `${self.ns}Hash`,
1535 func: () => {
1536 Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, true)
1537 history.pushState({}, '', `#${encodeURIComponent(attr)}`)
1538 Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, false)
1539 },
1540 })
1541 }
1542 }
1543 }
1544 }
1545 }
1546
1547 /**
1548 * deactivate element
1549 * @param {Object} params
1550 * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated
1551 * @param {String} params.type Type of element
1552 * @param {Boolean} params.skipSame If skip activation classes and events
1553 */
1554 _deactivate({ el, type, skipSame } = {}) {
1555 const self = this
1556 const options = self.options
1557 // activation
1558 if (!skipSame && options.classSkip !== true && !options.classSkip[type]) {
1559 // input
1560 el.checked = false
1561 // must be outside inside raf or page jumps (e.g. noqueue, done outside for toggle inverse)
1562 el.classList.remove(...self._classes)
1563 // needs TWO raf or sequential off/on flickr (e.g. backdrop megamenu)
1564 Xt.frameDouble({
1565 el,
1566 func: () => {
1567 el.classList.remove(...self._classesIn, ...self._classesDone)
1568 el.classList.add(...self._classesOut)
1569 },
1570 })
1571 // direction
1572 el.classList.remove(...self._classesBefore, ...self._classesAfter)
1573 if (self.direction < 0) {
1574 el.classList.add(...self._classesBefore)
1575 } else if (self.direction > 0) {
1576 el.classList.add(...self._classesAfter)
1577 }
1578 }
1579 }
1580
1581 /**
1582 * deactivate element done
1583 * @param {Object} params
1584 * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated
1585 * @param {String} params.type Type of element
1586 * @param {Boolean} params.skipSame If skip activation classes and events
1587 */
1588 _deactivateDone({ el, type, skipSame } = {}) {
1589 const self = this
1590 const options = self.options
1591 // activation
1592 if (!skipSame && options.classSkip !== true && !options.classSkip[type]) {
1593 // fix need to repeat inside frameDouble in case we cancel
1594 Xt.frameDouble({ el })
1595 el.classList.remove(...self._classesIn, ...self._classesOut)
1596 }
1597 }
1598
1599 /**
1600 * deactivate hash
1601 * @param {Object} params
1602 * @param {Object} params.obj Queue object
1603 * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated
1604 * @param {String} params.type Type of element
1605 */
1606 _deactivateHash({ obj, el, type } = {}) {
1607 const self = this
1608 const options = self.options
1609 // hash
1610 if (!Xt.dataStorage.get(self.container, `${self.ns}HashSkip`)) {
1611 if (options.hash && self._hasHash && !self.initial) {
1612 // fix no data-xt-group-same
1613 const elMain = obj.elements.queueEls[0]
1614 if (
1615 (type === 'elements' && self.getElements({ el: elMain }).includes(el)) ||
1616 (type === 'targets' && self.getTargets({ el: elMain }).includes(el))
1617 ) {
1618 const attr = el.getAttribute(options.hash)
1619 if (attr && attr === location.hash.split('#')[1]) {
1620 // raf prevents hash on chained activations (e.g: multiple hash on elements with same activation)
1621 Xt.frame({
1622 el: window,
1623 ns: `${self.ns}Hash`,
1624 func: () => {
1625 Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, true)
1626 history.pushState({}, '', '#')
1627 Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, false)
1628 },
1629 })
1630 }
1631 }
1632 }
1633 }
1634 }
1635
1636 //
1637 // event
1638 //
1639
1640 /**
1641 * element on
1642 * @param {Object} params
1643 * @param {Node|HTMLElement|EventTarget|Window} params.el To be activated
1644 * @param {Boolean} params.force
1645 * @param {Boolean} params.focus
1646 * @param {Event} e
1647 * @return {Boolean} If activated
1648 */
1649 _eventOn({ el, force = false, focus = false }, e) {
1650 const self = this
1651 const options = self.options
1652 force = force ? force : e?.detail?.force
1653 // disabled
1654 if (self.disabled && !force) {
1655 return false
1656 }
1657 // toggle
1658 if (force || self._checkOn({ el })) {
1659 // auto
1660 self._eventAutostop()
1661 // fix groupElements and targets
1662 const elements = options.groupElements || self.targets.includes(el) ? self.getElements({ el, same: true }) : [el]
1663 el = elements[0]
1664 // targets
1665 const targets = self.getTargets({ el, same: true })
1666 // inner
1667 const elementsInner = self._getElementsInner({ els: elements })
1668 const targetsInner = self._getTargetsInner({ els: targets })
1669 // on
1670 self._addCurrent({ el })
1671 self._setIndex({ el })
1672 self._setDirection()
1673 // queue obj
1674 const actionCurrent = 'In'
1675 const actionOther = 'Out'
1676 let obj = self._eventQueue({ elements, targets, elementsInner, targetsInner, force, e })
1677 // if currents > max
1678 const currents = self._getCurrents()
1679 if (currents.length > options.max) {
1680 // deactivate old
1681 const objFiltered = self._eventOff({ el: currents[0], objFilter: obj })
1682 // skip obj nodes that are in off and on (e.g. group same slider animation-js)
1683 if (!options.queue && objFiltered?.obj) {
1684 obj = objFiltered.obj
1685 }
1686 }
1687 // put in queue
1688 if (!options.queue) {
1689 self[`_queue${actionCurrent}`] = [obj]
1690 } else {
1691 self[`_queue${actionCurrent}`].unshift(obj)
1692 }
1693 // queue run
1694 for (const type in self[`_queue${actionCurrent}`][0]) {
1695 self._queueStart({ actionCurrent, actionOther, type, index: 0 })
1696 }
1697 // focus
1698 if (focus) {
1699 el = elementsInner[0] ?? el
1700 el.focus()
1701 }
1702 // return
1703 return true
1704 } else if (options.off && [...options.off.split(' ')].includes(e?.type)) {
1705 // fix same event for on and off same namespace
1706 self._eventOff({ el }, e)
1707 }
1708 // return
1709 return false
1710 }
1711
1712 /**
1713 * element off
1714 * @param {Object} params
1715 * @param {Node|HTMLElement|EventTarget|Window} params.el To be deactivated
1716 * @param {Boolean} params.force
1717 * @param {Boolean} params.focus
1718 * @param {Object} params.objFilter Object to filter from
1719 * @param {Event} e
1720 * @return {Boolean} If deactivated
1721 */
1722 _eventOff({ el, force = false, focus = false, objFilter } = {}, e) {
1723 const self = this
1724 const options = self.options
1725 force = force ? force : e?.detail?.force
1726 // disabled
1727 if (self.disabled && !force) {
1728 return false
1729 }
1730 // toggle
1731 if (force || self._checkOff({ el })) {
1732 // fix groupElements and targets
1733 const elements = options.groupElements || self.targets.includes(el) ? self.getElements({ el, same: true }) : [el]
1734 el = elements[0]
1735 // off
1736 self._removeCurrent({ el })
1737 // targets
1738 const targets = self.getTargets({ el, same: true })
1739 // inner
1740 const elementsInner = self._getElementsInner({ els: elements })
1741 const targetsInner = self._getTargetsInner({ els: targets })
1742 // auto
1743 if (!self._getCurrents().length) {
1744 self._eventAutostop()
1745 }
1746 // queue obj
1747 const actionCurrent = 'Out'
1748 const actionOther = 'In'
1749 const obj = self._eventQueue({ elements, targets, elementsInner, targetsInner, force, e })
1750 // fix groupSame do not deactivate/reactivate but do logic (e.g. group same slider animation-js and slider hash)
1751 if (options.groupSame && !options.queue && objFilter) {
1752 for (const key in obj) {
1753 const item = obj[key]
1754 if (item.queueEls) {
1755 const itemFilter = objFilter[key]
1756 const queueEls = item.queueEls.filter(x => !itemFilter.queueEls.includes(x))
1757 // activation object skip nodes that should deactivate/reactivate
1758 itemFilter.skipEls = itemFilter.queueEls.filter(x => item.queueEls.includes(x))
1759 // deactivation object remove nodes that should deactivate/reactivate
1760 item.queueEls = queueEls
1761 }
1762 }
1763 }
1764 // put in queue
1765 if (!options.queue) {
1766 self[`_queue${actionCurrent}`] = [obj]
1767 } else {
1768 self[`_queue${actionCurrent}`].unshift(obj)
1769 }
1770 // remove queue not started if queue too big
1771 if (self[`_queue${actionCurrent}`].length > options.max) {
1772 // remove queue and stop
1773 const removedOn = self[`_queue${actionOther}`].shift()
1774 self._queueStop({ actionCurrent: actionOther, actionOther: actionCurrent, obj: removedOn })
1775 // remove queue and stop
1776 const removedOff = self[`_queue${actionCurrent}`].shift()
1777 self._queueStop({ actionCurrent, actionOther, obj: removedOff })
1778 }
1779 // queue run
1780 for (const type in self[`_queue${actionCurrent}`][0]) {
1781 self._queueStart({ actionCurrent, actionOther, type, index: 0 })
1782 }
1783 // focus
1784 if (focus) {
1785 el = elementsInner[0] ?? el
1786 el.focus()
1787 }
1788 // return
1789 if (objFilter) {
1790 return { obj: objFilter }
1791 }
1792 return true
1793 }
1794 // return
1795 return false
1796 }
1797
1798 /**
1799 * element on
1800 * @param {Object} params
1801 * @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.elements
1802 * @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.targets
1803 * @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.elementsInner
1804 * @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.targetsInner
1805 * @param {Boolean} params.force
1806 * @param {Event} params.e
1807 */
1808 _eventQueue({ elements, targets, elementsInner, targetsInner, force, e } = {}) {
1809 // populate
1810 const obj = {}
1811 obj.elements = {
1812 queueEls: elements,
1813 force: force,
1814 e: e,
1815 }
1816 if (targets.length) {
1817 obj.targets = {
1818 queueEls: targets,
1819 }
1820 }
1821 if (elementsInner.length) {
1822 obj.elementsInner = {
1823 queueEls: elementsInner,
1824 }
1825 }
1826 if (targetsInner.length) {
1827 obj.targetsInner = {
1828 queueEls: targetsInner,
1829 }
1830 }
1831 return obj
1832 }
1833
1834 /**
1835 * auto
1836 */
1837 _eventAuto() {
1838 const self = this
1839 const options = self.options
1840 // disabled
1841 if (self.disabled) {
1842 return
1843 }
1844 // auto
1845 if (!self._autoblock && self._autorunning) {
1846 if (Xt.visible({ el: self.container })) {
1847 // not when disabled
1848 if (options.auto.inverse) {
1849 self.goToPrev({ amount: options.auto.step, loop: options.auto.loop })
1850 } else {
1851 self.goToNext({ amount: options.auto.step, loop: options.auto.loop })
1852 }
1853 }
1854 }
1855 }
1856
1857 /**
1858 * auto start
1859 */
1860 _eventAutostart() {
1861 const self = this
1862 const options = self.options
1863 // disabled
1864 if (self.disabled) {
1865 return
1866 }
1867 // start
1868 if (options.auto && options.auto.time && Xt.autoTimescale) {
1869 if (!self._autoblock && !self._autorunning) {
1870 // not when nothing activated
1871 if (self.index !== null && (!self.initial || options.auto.initial)) {
1872 // paused
1873 self._autorunning = true
1874 // clear
1875 clearTimeout(Xt.dataStorage.get(self.container, `${self.ns}AutoTimeout`))
1876 // auto
1877 const time = options.auto.time
1878 // disabled
1879 if (self.disabled) {
1880 return
1881 }
1882 // timeout
1883 Xt.dataStorage.set(
1884 self.container,
1885 `${self.ns}AutoTimeout`,
1886 setTimeout(() => {
1887 // auto
1888 self._eventAuto()
1889 }, time / Xt.autoTimescale)
1890 )
1891 // dispatch event
1892 self.container.dispatchEvent(new CustomEvent(`autostart.${self._componentNs}`))
1893 }
1894 }
1895 }
1896 }
1897
1898 /**
1899 * auto stop
1900 */
1901 _eventAutostop() {
1902 const self = this
1903 const options = self.options
1904 // stop
1905 if (options.auto && options.auto.time) {
1906 if (!self._autoblock && self._autorunning) {
1907 // paused
1908 self._autorunning = false
1909 // clear
1910 clearTimeout(Xt.dataStorage.get(self.container, `${self.ns}AutoTimeout`))
1911 // dispatch event
1912 self.container.dispatchEvent(new CustomEvent(`autostop.${self._componentNs}`))
1913 }
1914 }
1915 }
1916
1917 /**
1918 * jump
1919 * @param {Object} params
1920 * @param {Node|HTMLElement|EventTarget|Window} params.el
1921 * @param {Event} e
1922 */
1923 _eventJump({ el }, e) {
1924 const self = this
1925 // disabled
1926 if (self.disabled) {
1927 return
1928 }
1929 // check disabled
1930 if (el.classList.contains(...self._classes) || !el.classList.contains('xt-jump')) {
1931 return
1932 }
1933 // prevent default
1934 e.preventDefault()
1935 // jump
1936 if (self._checkOn({ el })) {
1937 self._eventOn({ el })
1938 }
1939 }
1940
1941 /**
1942 * nav
1943 * @param {Node|HTMLElement|EventTarget|Window} el
1944 */
1945 _eventNav({ el } = {}) {
1946 const self = this
1947 // disabled
1948 if (self.disabled) {
1949 return
1950 }
1951 // nav
1952 const step = parseFloat(el.getAttribute('data-xt-nav'))
1953 if (step < 0) {
1954 self.goToPrev({ amount: -step })
1955 } else {
1956 self.goToNext({ amount: step })
1957 }
1958 }
1959
1960 /**
1961 * medialoadedReinit
1962 */
1963 _eventMediaLoadedReinit() {
1964 const self = this
1965 // reinit
1966 self.reinit()
1967 }
1968
1969 /**
1970 * eventVisibleReinit
1971 */
1972 _eventVisibleReinit() {
1973 const self = this
1974 // reinit
1975 self.reinit()
1976 }
1977
1978 //
1979 // queue
1980 //
1981
1982 /**
1983 * queue start
1984 * @param {Object} params
1985 * @param {String} params.actionCurrent Current action
1986 * @param {String} params.actionOther Other action
1987 * @param {String} params.type Type of element
1988 * @param {Number} params.index Queue index
1989 */
1990 _queueStart({ actionCurrent, actionOther, type, index } = {}) {
1991 const self = this
1992 const options = self.options
1993 // queue start
1994 const obj = self[`_queue${actionCurrent}`][index]
1995 if (obj && obj[type] && !obj[type].done) {
1996 const queueOther = self[`_queue${actionOther}`]
1997 const objOther = queueOther[queueOther.length - 1]
1998 if (!objOther || !objOther[type] || objOther[type].done) {
1999 // fix if initial must be instant, fixes queue
2000 if (self.initial || !options.queue) {
2001 obj[type].instant = true
2002 } else if (options.queue && !options.queue[type]) {
2003 obj[type].instantType = true
2004 }
2005 // special
2006 self._specialClassBody({ actionCurrent, type })
2007 // start queue
2008 self._queueDelay({ actionCurrent, actionOther, obj, type })
2009 }
2010 }
2011 }
2012
2013 /**
2014 * queue stop
2015 * @param {Object} params
2016 * @param {String} params.actionCurrent Current action
2017 * @param {String} params.actionOther Other action
2018 * @param {Object} params.obj Queue object to end
2019 */
2020 _queueStop({ actionCurrent, actionOther, obj } = {}) {
2021 const self = this
2022 // stop type if done
2023 for (const type in obj) {
2024 if (obj[type].done) {
2025 for (const el of obj[type].queueEls) {
2026 // clear timeout and frame
2027 Xt.frameDouble({ el, ns: `${self.ns}CollapseHeightFrame` })
2028 Xt.frameDouble({ el, ns: `${self.ns}CollapseWidthFrame` })
2029 clearTimeout(Xt.dataStorage.get(el, `${self.ns + type}DelayTimeout`))
2030 clearTimeout(Xt.dataStorage.get(el, `${self.ns + type}AnimTimeout`))
2031 // done other queue
2032 self._queueDelayDone({
2033 actionCurrent: actionOther,
2034 actionOther: actionCurrent,
2035 obj,
2036 el,
2037 type,
2038 skipQueue: true,
2039 })
2040 self._queueAnimDone({
2041 actionCurrent: actionOther,
2042 actionOther: actionCurrent,
2043 obj,
2044 el,
2045 type,
2046 skipQueue: true,
2047 })
2048 }
2049 }
2050 }
2051 }
2052
2053 /**
2054 * queue delay
2055 * @param {Object} params
2056 * @param {String} params.actionCurrent Current action
2057 * @param {String} params.actionOther Other action
2058 * @param {Object} params.obj Queue object
2059 * @param {String} params.type Type of elements
2060 */
2061 _queueDelay({ actionCurrent, actionOther, obj, type } = {}) {
2062 const self = this
2063 const options = self.options
2064 // delay
2065 const els = obj[type].queueEls
2066 for (const el of els) {
2067 // fix groupSame do not deactivate/reactivate but do logic (e.g. group same slider animation-js and slider hash)
2068 const skipSame = obj[type].skipEls?.includes(el)
2069 // delay
2070 let delay =
2071 self.initial || self.disabled // off from disable/destroy
2072 ? false
2073 : Xt.delayTime({ el, duration: options.delay || options[`delay${actionCurrent}`], actionCurrent })
2074 if (delay) {
2075 if (typeof delay === 'function') {
2076 const count = Xt.dataStorage.get(el, `${self.ns + actionCurrent}Count`) || els.findIndex(x => x === el)
2077 const tot = Xt.dataStorage.get(el, `${self.ns + actionCurrent}Tot`) || els.length
2078 delay = delay({ current: count, total: tot - 1, el, self })
2079 }
2080 }
2081 // fnc
2082 clearTimeout(Xt.dataStorage.get(el, `${self.ns + type}DelayTimeout`))
2083 clearTimeout(Xt.dataStorage.get(el, `${self.ns + type}AnimTimeout`))
2084 if (!delay) {
2085 self._queueDelayDone({ actionCurrent, actionOther, obj, el, type, skipSame })
2086 } else if (delay === 'raf') {
2087 Xt.frameDouble({
2088 el,
2089 ns: `${self.ns + type}QueueDelayDone`,
2090 func: () => {
2091 self._queueDelayDone({ actionCurrent, actionOther, obj, el, type, skipSame })
2092 },
2093 })
2094 } else {
2095 Xt.dataStorage.set(
2096 el,
2097 `${self.ns + type}DelayTimeout`,
2098 setTimeout(() => {
2099 self._queueDelayDone({ actionCurrent, actionOther, obj, el, type, skipSame })
2100 }, delay)
2101 )
2102 }
2103 // queue done
2104 if (obj[type].instant) {
2105 // only if last element
2106 if (el === els[els.length - 1]) {
2107 self._queueDone({ actionCurrent, actionOther, obj, type })
2108 }
2109 }
2110 }
2111 // queue done
2112 if (!els.length) {
2113 self._queueDone({ actionCurrent, actionOther, obj, type })
2114 }
2115 }
2116
2117 /**
2118 * queue delay done
2119 * @param {Object} params
2120 * @param {String} params.actionCurrent Current action
2121 * @param {String} params.actionOther Other action
2122 * @param {Object} params.obj Queue object
2123 * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated
2124 * @param {String} params.type Type of elements
2125 * @param {Boolean} params.skipSame If skip activation classes and events
2126 * @param {Boolean} params.skipQueue If skip queue
2127 */
2128 _queueDelayDone({ actionCurrent, actionOther, obj, el, type, skipSame, skipQueue = false } = {}) {
2129 const self = this
2130 // check if not already running or if force
2131 if (actionCurrent === 'In' && (self._checkOnRunning({ obj }) || obj.elements.force)) {
2132 // only one time and if last element
2133 if (type === 'elements' && el === obj.elements.queueEls[0]) {
2134 self._addCurrent({ el, running: true })
2135 }
2136 // activation
2137 self._activate({ el, type })
2138 self._activateHash({ obj, el, type })
2139 // special
2140 self._specialZindex({ actionCurrent, obj, el, type })
2141 self._specialAppendto({ actionCurrent, el, type })
2142 self._specialClose({ actionCurrent, el, type, obj })
2143 if (!self.initial) {
2144 self._specialCollapse({ actionCurrent, el, type })
2145 } else {
2146 self._specialCollapse({ actionCurrent, el, type, reset: true })
2147 }
2148 // dispatch event
2149 if (!skipSame && type !== 'elementsInner' && type !== 'targetsInner') {
2150 // off from disable/destroy
2151 if (!self.disabled) {
2152 Xt.frame({
2153 el,
2154 ns: `${self.ns}${actionCurrent}DelayDone`,
2155 func: () => {
2156 el.dispatchEvent(
2157 new CustomEvent(`on.${self._componentNs}`, {
2158 detail: obj.elements.e,
2159 })
2160 )
2161 },
2162 })
2163 }
2164 }
2165 } else if (actionCurrent === 'Out' && (self._checkOffRunning({ obj }) || obj.elements.force)) {
2166 // only one time and if last element
2167 if (type === 'elements' && el === obj.elements.queueEls[0]) {
2168 self._removeCurrent({ el, running: true })
2169 // if no currents
2170 if (!self._getCurrents().length) {
2171 // reset index and direction
2172 self.index = null
2173 self._setDirection()
2174 }
2175 }
2176 // activation
2177 self._deactivate({ el, type })
2178 self._deactivateHash({ obj, el, type })
2179 // special
2180 self._specialCollapse({ actionCurrent, el, type })
2181 self._specialClose({ actionCurrent, el, type, obj })
2182 // dispatch event
2183 if (!skipSame && type !== 'elementsInner' && type !== 'targetsInner') {
2184 // off from disable/destroy
2185 if (!self.disabled) {
2186 Xt.frame({
2187 el,
2188 ns: `${self.ns}${actionCurrent}DelayDone`,
2189 func: () => {
2190 el.dispatchEvent(
2191 new CustomEvent(`off.${self._componentNs}`, {
2192 detail: obj.elements.e,
2193 })
2194 )
2195 },
2196 })
2197 }
2198 }
2199 }
2200 // queue
2201 if (!skipQueue) {
2202 // off from disable/destroy
2203 // must be instant on destroy (e.g. overlay mobile)
2204 if (self.disabled) {
2205 Xt.frame({
2206 el,
2207 ns: `${self.ns + type}QueueAnim`,
2208 })
2209 self._queueAnim({ actionCurrent, actionOther, obj, el, type, skipSame })
2210 } else {
2211 // needs ONE raf or sequential off/on flickr (e.g. toggle inverse)
2212 Xt.frame({
2213 el,
2214 ns: `${self.ns + type}QueueAnim`,
2215 func: () => {
2216 self._queueAnim({ actionCurrent, actionOther, obj, el, type, skipSame })
2217 },
2218 })
2219 }
2220 // queue done
2221 if (!obj[type].instant && obj[type].instantType) {
2222 const els = obj[type].queueEls
2223 // only if last element
2224 if (el === els[els.length - 1]) {
2225 self._queueDone({ actionCurrent, actionOther, obj, type })
2226 }
2227 }
2228 }
2229 }
2230
2231 /**
2232 * queue anim
2233 * @param {Object} params
2234 * @param {String} params.actionCurrent Current action
2235 * @param {String} params.actionOther Other action
2236 * @param {Object} params.obj Queue object
2237 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to be animated
2238 * @param {String} params.type Type of element
2239 * @param {Boolean} params.skipSame If skip activation classes and events
2240 */
2241 _queueAnim({ actionCurrent, actionOther, obj, el, type, skipSame } = {}) {
2242 const self = this
2243 const options = self.options
2244 // duration
2245 const els = obj[type].queueEls
2246 let duration =
2247 self.initial || self.disabled // off from disable/destroy
2248 ? false
2249 : Xt.animTime({ el, duration: options.duration || options[`duration${actionCurrent}`], actionCurrent })
2250 if (duration) {
2251 if (typeof duration === 'function') {
2252 const count = Xt.dataStorage.get(el, `${self.ns + actionCurrent}Count`) || els.findIndex(x => x === el)
2253 const tot = Xt.dataStorage.get(el, `${self.ns + actionCurrent}Tot`) || els.length
2254 duration = duration({ current: count, total: tot - 1, el, self })
2255 }
2256 }
2257 // fnc
2258 clearTimeout(Xt.dataStorage.get(el, `${self.ns + type}AnimTimeout`))
2259 if (!duration) {
2260 self._queueAnimDone({ actionCurrent, actionOther, obj, el, type, skipSame })
2261 } else if (duration === 'raf') {
2262 Xt.frameDouble({
2263 el,
2264 ns: `${self.ns + type}QueueAnimDone`,
2265 func: () => {
2266 self._queueAnimDone({ actionCurrent, actionOther, obj, el, type, skipSame })
2267 },
2268 })
2269 } else {
2270 Xt.dataStorage.set(
2271 el,
2272 `${self.ns + type}AnimTimeout`,
2273 setTimeout(() => {
2274 self._queueAnimDone({ actionCurrent, actionOther, obj, el, type, skipSame })
2275 }, duration)
2276 )
2277 }
2278 }
2279
2280 /**
2281 * queue anim done
2282 * @param {Object} params
2283 * @param {String} params.actionCurrent Current action
2284 * @param {String} params.actionOther Other action
2285 * @param {Object} params.obj Queue object
2286 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to be animated
2287 * @param {String} params.type Type of element
2288 * @param {Boolean} params.skipSame If skip activation classes and events
2289 * @param {Boolean} params.skipQueue If skip queue
2290 */
2291 _queueAnimDone({ actionCurrent, actionOther, obj, el, type, skipSame, skipQueue = false } = {}) {
2292 const self = this
2293 // special
2294 if (actionCurrent === 'In') {
2295 // only one time and if last element
2296 if (type === 'elements' && el === obj.elements.queueEls[0]) {
2297 // if no queueOut
2298 if (!self[`_queue${actionOther}`].length) {
2299 // reset all zIndex
2300 for (const type in obj) {
2301 self._specialZindex({ actionCurrent: actionOther, obj, type })
2302 }
2303 }
2304 }
2305 // activation
2306 self._activateDone({ el, type })
2307 // special
2308 self._specialCollapse({ actionCurrent, el, type, reset: true })
2309 self._specialScrollto({ actionCurrent, el, type, obj })
2310 // dispatch event
2311 if (!skipSame && type !== 'elementsInner' && type !== 'targetsInner') {
2312 // off from disable/destroy
2313 if (!self.disabled) {
2314 Xt.frame({
2315 el,
2316 ns: `${self.ns}${actionCurrent}AnimDone`,
2317 func: () => {
2318 el.dispatchEvent(
2319 new CustomEvent(`ondone.${self._componentNs}`, {
2320 detail: obj.elements.e,
2321 })
2322 )
2323 },
2324 })
2325 }
2326 }
2327 } else if (actionCurrent === 'Out') {
2328 // only one time and if last element
2329 if (type === 'elements' && el === obj.elements.queueEls[0]) {
2330 // if no currents
2331 if (!self._getCurrents().length) {
2332 // reset all zIndex
2333 for (const type in obj) {
2334 self._specialZindex({ actionCurrent, obj, type })
2335 }
2336 }
2337 }
2338 // activation
2339 self._deactivateDone({ el, type })
2340 // special
2341 self._specialAppendto({ actionCurrent, el, type })
2342 self._specialCollapse({ actionCurrent, el, type, reset: true })
2343 // dispatch event
2344 if (!skipSame && type !== 'elementsInner' && type !== 'targetsInner') {
2345 // off from disable/destroy
2346 if (!self.disabled) {
2347 Xt.frame({
2348 el,
2349 ns: `${self.ns}${actionCurrent}AnimDone`,
2350 func: () => {
2351 el.dispatchEvent(
2352 new CustomEvent(`offdone.${self._componentNs}`, {
2353 detail: obj.elements.e,
2354 })
2355 )
2356 },
2357 })
2358 }
2359 }
2360 }
2361 // queue
2362 if (!skipQueue) {
2363 // queue done
2364 if (!obj[type].instant && !obj[type].instantType) {
2365 const els = obj[type].queueEls
2366 // only if last element
2367 if (el === els[els.length - 1]) {
2368 self._queueDone({ actionCurrent, actionOther, obj, type })
2369 }
2370 }
2371 }
2372 }
2373
2374 /**
2375 * queue done
2376 * @param {Object} params
2377 * @param {String} params.actionCurrent Current action
2378 * @param {String} params.actionOther Other action
2379 * @param {Object} params.obj Queue object
2380 * @param {String} params.type Type of element
2381 */
2382 _queueDone({ actionCurrent, actionOther, obj, type } = {}) {
2383 const self = this
2384 // check
2385 if (obj[type]) {
2386 // type done
2387 obj[type].done = true
2388 // check done
2389 let done = 0
2390 for (const type in obj) {
2391 if (obj[type].done) {
2392 done++
2393 }
2394 }
2395 // all done
2396 if (done === Object.entries(obj).length) {
2397 // queue other when all done
2398 for (const type in obj) {
2399 self._queueStart({
2400 actionCurrent: actionOther,
2401 actionOther: actionCurrent,
2402 type,
2403 index: self[`_queue${actionOther}`].length - 1,
2404 })
2405 }
2406 // remove queue
2407 self[`_queue${actionCurrent}`].pop()
2408 // queue complete
2409 self._queueComplete({ actionCurrent, obj })
2410 }
2411 }
2412 }
2413
2414 /**
2415 * logic to execute on queue complete
2416 * @param {Object} params
2417 * @param {String} params.actionCurrent Current action
2418 * @param {Object} params.obj Queue object
2419 */
2420 _queueComplete({ actionCurrent, obj } = {}) {
2421 const self = this
2422 const options = self.options
2423 // logic
2424 if (actionCurrent === 'In') {
2425 // init
2426 // needs frameDouble after ondone
2427 Xt.frameDouble({
2428 el: self.container,
2429 ns: `${self.ns}Init`,
2430 func: () => {
2431 if (self.initial) {
2432 // fix before _initScope or slider absolute has multiple active and bugs initial calculations
2433 self.container.setAttribute(`data-${self.componentName}-init`, '')
2434 // dispatch event
2435 self.container.dispatchEvent(new CustomEvent(`init.${self._componentNs}`))
2436 // remove class initial
2437 for (const type in obj) {
2438 for (const el of obj[type].queueEls) {
2439 el.classList.remove(...self._classesInitial)
2440 }
2441 }
2442 // debug
2443 if (options.debug) {
2444 // eslint-disable-next-line no-console
2445 console.debug(`${self.componentName} init`, self)
2446 }
2447 }
2448 // fix autostart after self.initial or it gives error on reinitialization (demos fullscreen)
2449 self._eventAutostart()
2450 // initial after autostart
2451 if (self.initial) {
2452 self.initial = false
2453 }
2454 // reset
2455 self._inverse = null
2456 },
2457 })
2458 // focusLimit
2459 if (options.focusLimit && !self._focusTrap) {
2460 const trs = self.targets.length ? self.targets : self.elements
2461 self._focusTrap = focusTrap.createFocusTrap(trs, options.focusTrap)
2462 self._focusTrap.activate()
2463 }
2464 } else if (actionCurrent === 'Out') {
2465 // focusLimit
2466 if (options.focusLimit && self._focusTrap) {
2467 self._focusTrap.deactivate()
2468 self._focusTrap = null
2469 }
2470 }
2471 }
2472
2473 //
2474 // special
2475 //
2476
2477 /**
2478 * zindex on activation
2479 * @param {Object} params
2480 * @param {String} params.actionCurrent Current action
2481 * @param {Object} params.obj Queue object
2482 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to be animated
2483 * @param {String} params.type Type of element
2484 */
2485 _specialZindex({ actionCurrent, obj, el, type } = {}) {
2486 const self = this
2487 const options = self.options
2488 // fix when standalone !self.targets.length && type === 'elements'
2489 if (!self.targets.length && type === 'elements') {
2490 type = 'targets'
2491 }
2492 // set zIndex
2493 if (options.zIndex && options.zIndex[type]) {
2494 if (actionCurrent === 'In') {
2495 self.zIndex = self.zIndex ? self.zIndex : options.zIndex[type].start
2496 self.zIndex = self.zIndex + options.zIndex[type].factor
2497 el.style.zIndex = self.zIndex
2498 } else if (actionCurrent === 'Out') {
2499 self.zIndex = options.zIndex[type].start
2500 // check actionOther
2501 if (obj[type]) {
2502 for (const el of obj[type].queueEls) {
2503 el.style.zIndex = self.zIndex
2504 }
2505 }
2506 }
2507 }
2508 }
2509
2510 /**
2511 * add or remove html class
2512 * @param {Object} params
2513 * @param {String} params.actionCurrent Current action
2514 * @param {String} params.type Type of element
2515 */
2516 _specialClassBody({ actionCurrent, type } = {}) {
2517 const self = this
2518 const options = self.options
2519 if (options.classBody) {
2520 // fix when standalone !self.targets.length && type === 'elements'
2521 if (type === 'targets' || (!self.targets.length && type === 'elements')) {
2522 if (actionCurrent === 'In') {
2523 // raf because only one time on route update
2524 Xt.frame({
2525 el: self.container,
2526 ns: `${self.ns}ClassBodyFrame`,
2527 func: () => {
2528 for (const c of options.classBody.split(' ')) {
2529 // checks
2530 Xt._classBody.add({
2531 c: c,
2532 ns: self.ns,
2533 })
2534 // class on
2535 const container = document.documentElement.querySelector('body')
2536 container.classList.add(c)
2537 }
2538 },
2539 })
2540 } else if (actionCurrent === 'Out') {
2541 // raf because only one time on route update
2542 Xt.frame({
2543 el: self.container,
2544 ns: `${self.ns}ClassBodyFrame`,
2545 func: () => {
2546 for (const c of options.classBody.split(' ')) {
2547 // checks
2548 Xt._classBody.remove({
2549 c: c,
2550 ns: self.ns,
2551 })
2552 if (!Xt._classBody.get({ c: c }).length) {
2553 // class off
2554 const container = document.documentElement.querySelector('body')
2555 container.classList.remove(c)
2556 }
2557 }
2558 },
2559 })
2560 }
2561 }
2562 }
2563 }
2564
2565 /**
2566 * appendTo
2567 * @param {Object} params
2568 * @param {String} params.actionCurrent Current action
2569 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to be animated
2570 * @param {String} params.type Type of element
2571 */
2572 _specialAppendto({ actionCurrent, el, type } = {}) {
2573 const self = this
2574 const options = self.options
2575 if (options.appendTo) {
2576 // fix when standalone !self.targets.length && type === 'elements'
2577 if (type === 'targets' || (!self.targets.length && type === 'elements')) {
2578 if (actionCurrent === 'In') {
2579 // appendTo
2580 const appendToTarget = document.querySelector(options.appendTo)
2581 const appendOrigin = document.querySelector(`[data-xt-origin="${self.ns}"]`)
2582 if (!appendOrigin) {
2583 el.before(Xt.node({ str: `<div class="xt-ignore hidden" data-xt-origin="${self.ns}"></div>` }))
2584 }
2585 appendToTarget.append(el)
2586 } else if (actionCurrent === 'Out') {
2587 // appendTo
2588 const appendOrigin = document.querySelector(`[data-xt-origin="${self.ns}"]`)
2589 if (appendOrigin) {
2590 appendOrigin.before(el)
2591 appendOrigin.remove()
2592 } else {
2593 el.remove()
2594 }
2595 }
2596 }
2597 }
2598 }
2599
2600 /**
2601 * scrollto
2602 * @param {Object} params
2603 * @param {String} params.actionCurrent Current action
2604 * @param {Object} params.obj Queue object
2605 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to be animated
2606 * @param {String} params.type Type of element
2607 */
2608 _specialScrollto({ actionCurrent, obj, el, type } = {}) {
2609 const self = this
2610 const options = self.options
2611 if (options.scrollto) {
2612 if (actionCurrent === 'In') {
2613 const scrollto = ({ el }) => {
2614 // using data-xt-hash or options.min or .on and scrollto should not scroll on init
2615 if (!self.initial || options.scrolltoInit) {
2616 // Xt.ready complete and raf to be right after page refresh
2617 const instant = self.initial
2618 Xt.ready({
2619 state: 'complete',
2620 func: () => {
2621 requestAnimationFrame(() => {
2622 if (instant) {
2623 Xt.scrolltoHashforce = true
2624 }
2625 el.dispatchEvent(new CustomEvent('scrollto.trigger.xt.scrollto'))
2626 })
2627 },
2628 })
2629 }
2630 }
2631 // check
2632 if (typeof options.scrollto === 'string') {
2633 if (type === options.scrollto) {
2634 scrollto({ el })
2635 } else if (type === 'elements' && el === obj.elements.queueEls[0]) {
2636 let scrolltoElement = self.container.querySelector(options.scrollto)
2637 scrolltoElement = scrolltoElement ?? document.querySelector(options.scrollto)
2638 if (scrolltoElement) {
2639 scrollto({ el: scrolltoElement })
2640 }
2641 }
2642 } else if (type === 'elements' && el === obj.elements.queueEls[0]) {
2643 scrollto({ el: self.container })
2644 }
2645 }
2646 }
2647 }
2648
2649 /**
2650 * open or close or reset collapse
2651 * @param {Object} params
2652 * @param {String} params.actionCurrent Current action
2653 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to be animated
2654 * @param {String} params.type Type of element
2655 * @param {Boolean} reset Reset
2656 */
2657 _specialCollapse({ actionCurrent, el, type, reset = false } = {}) {
2658 const self = this
2659 const options = self.options
2660 if (options.collapseHeight) {
2661 if (actionCurrent === 'In') {
2662 if (options.collapseHeight === type) {
2663 if (reset) {
2664 el.style.height = 'inherit'
2665 el.style.maxHeight = 'none'
2666 el.classList.add('xt-collapse-reset')
2667 Xt.frameDouble({ el, ns: `${self.ns}CollapseHeightFrame` })
2668 } else {
2669 el.classList.remove('xt-collapse-reset')
2670 el.style.height = 'auto'
2671 el.style.maxHeight = 'none'
2672 const final = el.offsetHeight
2673 el.style.height = ''
2674 el.style.maxHeight = ''
2675 let initial = el.offsetHeight
2676 initial = initial === final ? 0 : initial
2677 el.style.height = `${initial}px`
2678 el.style.maxHeight = 'none'
2679 Xt.frameDouble({
2680 el,
2681 ns: `${self.ns}CollapseHeightFrame`,
2682 func: () => {
2683 el.style.height = `${final}px`
2684 },
2685 })
2686 }
2687 }
2688 } else if (actionCurrent === 'Out') {
2689 if (options.collapseHeight === type) {
2690 if (reset) {
2691 el.style.height = ''
2692 el.style.maxHeight = ''
2693 Xt.frameDouble({ el, ns: `${self.ns}CollapseHeightFrame` })
2694 } else {
2695 el.classList.remove('xt-collapse-reset')
2696 const current = el.offsetHeight // fix keep current off initial
2697 el.style.height = ''
2698 el.style.maxHeight = ''
2699 let final = el.offsetHeight
2700 el.style.height = 'auto'
2701 el.style.maxHeight = 'none'
2702 const initial = el.offsetHeight
2703 final = initial === final ? 0 : final
2704 el.style.height = `${current}px`
2705 Xt.frameDouble({
2706 el,
2707 ns: `${self.ns}CollapseHeightFrame`,
2708 func: () => {
2709 el.style.height = `${final}px`
2710 },
2711 })
2712 }
2713 }
2714 }
2715 }
2716 if (options.collapseWidth) {
2717 if (actionCurrent === 'In') {
2718 if (options.collapseWidth === type) {
2719 if (reset) {
2720 el.style.width = 'inherit'
2721 el.style.maxWidth = 'none'
2722 el.classList.add('xt-collapse-reset')
2723 Xt.frameDouble({ el, ns: `${self.ns}CollapseWidthFrame` })
2724 } else {
2725 el.classList.remove('xt-collapse-reset')
2726 el.style.width = 'auto'
2727 el.style.maxWidth = 'none'
2728 const final = el.offsetWidth
2729 el.style.width = ''
2730 el.style.maxWidth = ''
2731 let initial = el.offsetWidth
2732 initial = initial === final ? 0 : initial
2733 el.style.width = `${initial}px`
2734 el.style.maxWidth = 'none'
2735 Xt.frameDouble({
2736 el,
2737 ns: `${self.ns}CollapseWidthFrame`,
2738 func: () => {
2739 el.style.width = `${final}px`
2740 },
2741 })
2742 }
2743 }
2744 } else if (actionCurrent === 'Out') {
2745 if (options.collapseWidth === type) {
2746 if (reset) {
2747 el.style.width = ''
2748 el.style.maxWidth = ''
2749 Xt.frameDouble({ el, ns: `${self.ns}CollapseWidthFrame` })
2750 } else {
2751 el.classList.remove('xt-collapse-reset')
2752 const current = el.offsetWidth // fix keep current off initial
2753 el.style.width = ''
2754 el.style.maxWidth = ''
2755 let final = el.offsetWidth
2756 el.style.width = 'auto'
2757 el.style.maxWidth = 'none'
2758 const initial = el.offsetWidth
2759 final = initial === final ? 0 : final
2760 el.style.width = `${current}px`
2761 Xt.frameDouble({
2762 el,
2763 ns: `${self.ns}CollapseWidthFrame`,
2764 func: () => {
2765 el.style.width = `${final}px`
2766 },
2767 })
2768 }
2769 }
2770 }
2771 }
2772 }
2773
2774 /**
2775 * add or remove close events on element
2776 * @param {Object} params
2777 * @param {String} params.actionCurrent Current action
2778 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to be animated
2779 * @param {String} params.type Type of element
2780 * @param {Object} obj Queue object
2781 */
2782 _specialClose({ actionCurrent, el, type, obj } = {}) {
2783 const self = this
2784 const options = self.options
2785 if (actionCurrent === 'In') {
2786 // closeInside
2787 if (options.closeInside) {
2788 if (type === 'elements' || type === 'targets') {
2789 const closeElement = el
2790 const specialcloseinsideHandler = Xt.dataStorage.put(
2791 closeElement,
2792 `click/close/${self.ns}`,
2793 self._eventSpecialcloseinsideHandler.bind(self)
2794 )
2795 // raf because do not close when clicking things that trigger this
2796 requestAnimationFrame(() => {
2797 closeElement.removeEventListener('click', specialcloseinsideHandler)
2798 closeElement.addEventListener('click', specialcloseinsideHandler)
2799 })
2800 }
2801 }
2802 // closeOutside
2803 if (options.closeOutside) {
2804 // only one time and if last element
2805 if (type === 'elements' && el === obj.elements.queueEls[0]) {
2806 const outsides = document.querySelectorAll(options.closeOutside)
2807 for (const outside of outsides) {
2808 const specialcloseoutsideHandler = Xt.dataStorage.put(
2809 outside,
2810 `mousedown/close/${self.ns}`,
2811 self._eventSpecialcloseoutsideHandler.bind(self)
2812 )
2813 // raf because do not close when clicking things that trigger this
2814 requestAnimationFrame(() => {
2815 outside.removeEventListener('mousedown', specialcloseoutsideHandler)
2816 outside.addEventListener('mousedown', specialcloseoutsideHandler)
2817 })
2818 }
2819 }
2820 }
2821 // closeDeep
2822 if (options.closeDeep) {
2823 // fix when standalone !self.targets.length && type === 'elements'
2824 if (type === 'targets' || (!self.targets.length && type === 'elements')) {
2825 const closeElements = el.querySelectorAll(options.closeDeep)
2826 for (const closeElement of closeElements) {
2827 const specialclosedeepHandler = Xt.dataStorage.put(
2828 closeElement,
2829 `click/close/${self.ns}`,
2830 self._eventSpecialclosedeepHandler.bind(self)
2831 )
2832 // raf because do not close when clicking things that trigger this
2833 requestAnimationFrame(() => {
2834 closeElement.removeEventListener('click', specialclosedeepHandler)
2835 closeElement.addEventListener('click', specialclosedeepHandler)
2836 })
2837 // focusable
2838 const specialclosedeepKeydownHandler = Xt.dataStorage.put(
2839 closeElement,
2840 `keydown/close/${self.ns}`,
2841 self._eventSpecialclosedeepKeydownHandler.bind(self).bind(self, { closeElement })
2842 )
2843 // raf because do not close when clicking things that trigger this
2844 requestAnimationFrame(() => {
2845 closeElement.addEventListener('keydown', specialclosedeepKeydownHandler)
2846 closeElement.setAttribute('tabindex', '0')
2847 closeElement.setAttribute('role', 'button')
2848 })
2849 }
2850 }
2851 }
2852 } else if (actionCurrent === 'Out') {
2853 // closeInside
2854 if (options.closeInside) {
2855 if (type === 'elements' || type === 'targets') {
2856 const closeElement = el
2857 const specialcloseinsideHandler = Xt.dataStorage.get(closeElement, `click/close/${self.ns}`)
2858 closeElement.removeEventListener('click', specialcloseinsideHandler)
2859 }
2860 }
2861 // closeOutside
2862 if (options.closeOutside) {
2863 // only one time and if last element
2864 if (type === 'elements' && el === obj.elements.queueEls[0]) {
2865 const closeElements = document.querySelectorAll(options.closeOutside)
2866 for (const closeElement of closeElements) {
2867 const specialcloseoutsideHandler = Xt.dataStorage.get(closeElement, `mousedown/close/${self.ns}`)
2868 closeElement.removeEventListener('mousedown', specialcloseoutsideHandler)
2869 }
2870 }
2871 }
2872 // closeDeep
2873 if (options.closeDeep) {
2874 // fix when standalone !self.targets.length && type === 'elements'
2875 if (type === 'targets' || (!self.targets.length && type === 'elements')) {
2876 const closeElements = el.querySelectorAll(options.closeDeep)
2877 for (const closeElement of closeElements) {
2878 const specialclosedeepHandler = Xt.dataStorage.get(closeElement, `click/close/${self.ns}`)
2879 closeElement.removeEventListener('click', specialclosedeepHandler)
2880 // focusable
2881 const specialclosedeepKeydownHandler = Xt.dataStorage.get(closeElement, `keydown/close/${self.ns}`)
2882 closeElement.removeEventListener('keydown', specialclosedeepKeydownHandler)
2883 closeElement.removeAttribute('tabindex')
2884 closeElement.removeAttribute('role')
2885 }
2886 }
2887 }
2888 }
2889 }
2890
2891 /**
2892 * specialCloseinside handler
2893 * @param {Event} e
2894 */
2895 _eventSpecialcloseinsideHandler(e) {
2896 const self = this
2897 const options = self.options
2898 // handler
2899 if (e.target.matches(options.closeInside)) {
2900 const currents = self._getCurrents()
2901 for (const current of currents) {
2902 self._eventOff({ el: current, force: true })
2903 }
2904 }
2905 }
2906
2907 /**
2908 * specialCloseoutside handler
2909 * @param {Event} e
2910 */
2911 _eventSpecialcloseoutsideHandler(e) {
2912 const self = this
2913 // handler
2914 if (!Xt.contains({ els: [...self.elements, ...self.targets], tr: e.target })) {
2915 const currents = self._getCurrents()
2916 for (const current of currents) {
2917 self._eventOff({ el: current, force: true })
2918 }
2919 }
2920 }
2921
2922 /**
2923 * specialClosedeep handler
2924 * @param {Event} e
2925 */
2926 _eventSpecialclosedeepHandler(e) {
2927 const self = this
2928 // handler
2929 if (Xt.contains({ els: [...self.elements, ...self.targets], tr: e.target })) {
2930 const currents = self._getCurrents()
2931 for (const current of currents) {
2932 self._eventOff({ el: current, force: true })
2933 }
2934 }
2935 }
2936
2937 /**
2938 * specialClosedeep keydown handler
2939 * @param {Object} params
2940 * @param {Node|HTMLElement|EventTarget|Window} params.closeElement
2941 * @param {Event} e
2942 */
2943 _eventSpecialclosedeepKeydownHandler({ closeElement }, e) {
2944 const key = e.key
2945 // key enter or space
2946 if (key === 'Enter' || key === ' ') {
2947 e.preventDefault()
2948 closeElement.dispatchEvent(new CustomEvent('click'))
2949 }
2950 }
2951
2952 //
2953 // index
2954 //
2955
2956 /**
2957 * Get next activation index
2958 * @param {Object} params
2959 * @param {Number} params.index
2960 * @param {Number} params.amount
2961 * @param {Boolean} params.loop
2962 * @return {Number} index
2963 */
2964 getNextIndex({ index = false, amount = 1, loop = null } = {}) {
2965 const self = this
2966 // logic
2967 if (index !== false) {
2968 index = index + amount
2969 } else if (self.index !== null) {
2970 index = self.index + amount
2971 } else {
2972 index = 0
2973 }
2974 return self.getNumIndex({ index, loop })
2975 }
2976
2977 /**
2978 * Get next element
2979 * @param {Object} params
2980 * @param {Number} params.amount
2981 * @param {Boolean} params.loop
2982 * @return {Node|HTMLElement|EventTarget|Window} Element
2983 */
2984 getNext({ amount = 1, loop = null } = {}) {
2985 const self = this
2986 // logic
2987 const i = self.getNextIndex({ amount, loop })
2988 return self.getElementsGroups()[i]
2989 }
2990
2991 /**
2992 * Activate next element
2993 * @param {Object} params
2994 * @param {Number} params.amount
2995 * @param {Boolean} params.force
2996 * @param {Boolean} params.loop
2997 * @return {Node|HTMLElement|EventTarget|Window} Element
2998 */
2999 goToNext({ amount = 1, force = false, loop = null } = {}) {
3000 const self = this
3001 // goToNum
3002 self._inverse = false
3003 const index = self.getNextIndex({ amount, loop })
3004 const el = self.goToNum({ index, force, loop })
3005 return el
3006 }
3007
3008 /**
3009 * Get prev activation index
3010 * @param {Object} params
3011 * @param {Number} params.index
3012 * @param {Number} params.amount
3013 * @param {Boolean} params.loop
3014 * @return {Number} index
3015 */
3016 getPrevIndex({ index = false, amount = 1, loop = null } = {}) {
3017 const self = this
3018 // logic
3019 if (index !== false) {
3020 index = index - amount
3021 } else if (self.index !== null) {
3022 index = self.index - amount
3023 } else {
3024 index = self.getElementsGroups().length - 1
3025 }
3026 return self.getNumIndex({ index, loop })
3027 }
3028
3029 /**
3030 * Get previous element
3031 * @param {Object} params
3032 * @param {Number} params.amount
3033 * @param {Boolean} params.loop
3034 * @return {Node|HTMLElement|EventTarget|Window} Element
3035 */
3036 getPrev({ amount = 1, loop = null } = {}) {
3037 const self = this
3038 // logic
3039 const i = self.getPrevIndex({ amount, loop })
3040 return self.getElementsGroups()[i]
3041 }
3042
3043 /**
3044 * Activate previous element
3045 * @param {Object} params
3046 * @param {Number} params.amount
3047 * @param {Boolean} params.force
3048 * @param {Boolean} params.loop
3049 * @return {Node|HTMLElement|EventTarget|Window} Element
3050 */
3051 goToPrev({ amount = 1, force = false, loop = null } = {}) {
3052 const self = this
3053 // goToNum
3054 self._inverse = true
3055 const index = self.getPrevIndex({ amount, loop })
3056 const el = self.goToNum({ index, force, loop })
3057 return el
3058 }
3059
3060 /**
3061 * Get index from number
3062 * @param {Object} params
3063 * @param {Number} params.index
3064 * @param {Boolean} params.loop
3065 * @return {Number} index
3066 */
3067 getNumIndex({ index, loop = null } = {}) {
3068 const self = this
3069 const options = self.options
3070 // check
3071 const min = 0
3072 const max = self.getElementsGroups().length - 1
3073 if (min === max) {
3074 // if only one
3075 index = null
3076 } else {
3077 if (index > max) {
3078 if (loop || (loop === null && (options.loop || self._wrap))) {
3079 index = index - max - 1
3080 index = index > max ? max : index // prevent overflow
3081 } else {
3082 index = null
3083 }
3084 } else if (index < min) {
3085 if (loop || (loop == null && (options.loop || self._wrap))) {
3086 index = index + max + 1
3087 index = index < min ? min : index // prevent overflow
3088 } else {
3089 index = null
3090 }
3091 }
3092 }
3093 // element
3094 return index
3095 }
3096
3097 /**
3098 * Get element from index
3099 * @param {Object} params
3100 * @param {Number} params.index
3101 * @param {Boolean} params.loop
3102 * @return {Node|HTMLElement|EventTarget|Window} Element
3103 */
3104 getNum({ index = 1, loop = null } = {}) {
3105 const self = this
3106 // logic
3107 const i = self.getNumIndex({ index, loop })
3108 return self.getElementsGroups()[i]
3109 }
3110
3111 /**
3112 * activate number element
3113 * @param {Object} params
3114 * @param {Number} params.index
3115 * @param {Boolean} params.force
3116 * @param {Boolean} params.loop
3117 * @return {Node|HTMLElement|EventTarget|Window} Element
3118 */
3119 goToNum({ index, force = false, loop = null } = {}) {
3120 const self = this
3121 // go
3122 const el = self.getNum({ index, loop })
3123 if (el) {
3124 self._eventOn({ el, force })
3125 }
3126 return el
3127 }
3128
3129 //
3130 // status
3131 //
3132
3133 /**
3134 * enable
3135 */
3136 enable() {
3137 const self = this
3138 if (self.disabled) {
3139 // enable
3140 self.disabled = false
3141 self.container.removeAttribute(`data-${self.componentName}-disabled`)
3142 for (const el of self.elements) {
3143 el.removeAttribute(`data-${self.componentName}-disabled`)
3144 }
3145 for (const tr of self.targets) {
3146 tr.removeAttribute(`data-${self.componentName}-disabled`)
3147 }
3148 // dispatch event
3149 self.container.dispatchEvent(new CustomEvent(`status.${self._componentNs}`))
3150 }
3151 }
3152
3153 /**
3154 * disable
3155 * @param {Object} params
3156 * @param {Boolean} params.skipEvent Skip dispatch event
3157 */
3158 disable({ skipEvent = false } = {}) {
3159 const self = this
3160 const options = self.options
3161 if (!self.disabled) {
3162 self.disabled = true
3163 // off from disable/destroy
3164 // need to deactivate on disable on some components (e.g. destroy overlay mobile, matches disable)
3165 if (options.disableDeactivate) {
3166 for (const el of self.elements.filter(x => self.hasCurrent({ el: x }))) {
3167 self._eventOff({ el, force: true })
3168 }
3169 }
3170 // disable
3171 self.container.setAttribute(`data-${self.componentName}-disabled`, '')
3172 for (const el of self.elements) {
3173 el.setAttribute(`data-${self.componentName}-disabled`, '')
3174 }
3175 for (const tr of self.targets) {
3176 tr.setAttribute(`data-${self.componentName}-disabled`, '')
3177 }
3178 // jump
3179 if (options.jump) {
3180 for (const jump of self.targets) {
3181 jump.classList.remove('xt-jump')
3182 }
3183 }
3184 // intersection observer
3185 if (self._observer) {
3186 self._observer.disconnect()
3187 self._observer = null
3188 }
3189 // stop auto
3190 clearTimeout(Xt.dataStorage.get(self.container, `${self.ns}AutoTimeout`))
3191 // dispatch event
3192 if (!skipEvent) {
3193 self.container.dispatchEvent(new CustomEvent(`status.${self._componentNs}`))
3194 }
3195 }
3196 }
3197
3198 //
3199 // a11y
3200 //
3201
3202 /**
3203 * init a11y
3204 */
3205 _initA11y() {
3206 const self = this
3207 const options = self.options
3208 // a11y
3209 if (options.a11y) {
3210 // vars
3211 let els = self.elements
3212 let trs = self.targets
3213 // when self mode
3214 if (!self.targets.length) {
3215 els = []
3216 trs = self.elements
3217 }
3218 // inside self.container
3219 self._hasContainer = self._mode === 'unique' || self.elements.includes(self.container) ? false : true
3220 // init
3221 self._initA11yRole({ els, trs })
3222 self._initA11yId({ els, trs })
3223 self._initA11ySetup()
3224 self._initA11yAuto()
3225 self._initA11yChange()
3226 self._initA11yStatus({ els })
3227 self._initA11yKeyboard({ els: self.elements })
3228 }
3229 }
3230
3231 /**
3232 * init a11y role
3233 * @param {Object} params
3234 * @param {Node|HTMLElement|EventTarget|Window} params.els
3235 * @param {Node|HTMLElement|EventTarget|Window} params.trs
3236 */
3237 _initA11yRole({ els, trs } = {}) {
3238 const self = this
3239 const options = self.options
3240 if (options.a11y.role) {
3241 let multi = false
3242 // aria-orientation
3243 if (options.a11y.vertical && self._hasContainer) {
3244 self.container.setAttribute('aria-orientation', 'vertical')
3245 }
3246 // role
3247 if (options.a11y.role === 'popup') {
3248 // popup
3249 for (const el of els) {
3250 el.setAttribute('aria-haspopup', true)
3251 }
3252 } else if (options.a11y.role === 'dialog') {
3253 // dialog
3254 for (const el of els) {
3255 el.setAttribute('aria-haspopup', 'dialog')
3256 }
3257 for (const tr of trs) {
3258 tr.setAttribute('role', 'dialog')
3259 tr.setAttribute('aria-modal', 'true')
3260 }
3261 } else if (options.a11y.role === 'tooltip') {
3262 // tooltip
3263 for (const tr of trs) {
3264 tr.setAttribute('role', 'tooltip')
3265 }
3266 } else if (options.a11y.role === 'carousel' && self._hasContainer) {
3267 // carousel
3268 self.container.setAttribute('role', 'group')
3269 self.container.setAttribute('aria-roledescription', 'carousel')
3270 for (const tr of trs) {
3271 tr.setAttribute('role', 'group')
3272 tr.setAttribute('aria-roledescription', 'slide')
3273 }
3274 } else if (options.a11y.role === 'tablist' && self._hasContainer && self.targets.length) {
3275 // tab
3276 multi = true
3277 self.container.setAttribute('role', 'tablist')
3278 for (const el of els) {
3279 el.setAttribute('role', 'tab')
3280 }
3281 for (const tr of trs) {
3282 tr.setAttribute('role', 'tabpanel')
3283 }
3284 } else if (options.a11y.role === 'menu' && self._hasContainer && self.targets.length) {
3285 // menu
3286 self.container.setAttribute('role', 'menu')
3287 for (const el of els) {
3288 el.setAttribute('role', 'menuitem')
3289 }
3290 for (const tr of trs) {
3291 tr.setAttribute('role', 'menu')
3292 }
3293 } else if (options.a11y.role === 'listbox' && self._hasContainer && self.targets.length) {
3294 // listbox
3295 multi = true
3296 self.container.setAttribute('role', 'listbox')
3297 for (const el of els) {
3298 el.setAttribute('role', 'option')
3299 }
3300 }
3301 // aria-multiselectable
3302 if (multi && options.max > 1) {
3303 self.container.setAttribute('aria-multiselectable', 'true')
3304 }
3305 }
3306 }
3307
3308 /**
3309 * init a11y id
3310 * @param {Object} params
3311 * @param {Node|HTMLElement|EventTarget|Window} params.els
3312 * @param {Node|HTMLElement|EventTarget|Window} params.trs
3313 */
3314 _initA11yId({ els, trs } = {}) {
3315 const self = this
3316 const options = self.options
3317 // not when self mode (no targets)
3318 if (self.targets.length) {
3319 // id
3320 if (options.a11y.labelElements || options.a11y.controls) {
3321 // targets
3322 for (const tr of trs) {
3323 const id = tr.getAttribute('id')
3324 if (!id) {
3325 tr.setAttribute('id', Xt.uniqueId())
3326 }
3327 }
3328 }
3329 if (options.a11y.labelTargets) {
3330 // elements
3331 for (const el of els) {
3332 const id = el.getAttribute('id')
3333 if (!id) {
3334 el.setAttribute('id', Xt.uniqueId())
3335 }
3336 }
3337 }
3338 }
3339 }
3340
3341 /**
3342 * init a11y setup
3343 */
3344 _initA11ySetup() {
3345 const self = this
3346 const options = self.options
3347 // not when self mode (no targets)
3348 if (self.targets.length) {
3349 // aria-labelledby and aria-controls
3350 if (options.a11y.labelElements || options.a11y.controls) {
3351 for (const el of self.elements) {
3352 const trs = self.getTargets({ el })
3353 let str = ''
3354 for (const tr of trs) {
3355 str += `${tr.getAttribute('id')} `
3356 }
3357 if (options.a11y.labelElements) {
3358 el.setAttribute('aria-labelledby', str.trim())
3359 el.removeAttribute('aria-label')
3360 }
3361 if (options.a11y.controls) {
3362 el.setAttribute('aria-controls', str.trim())
3363 }
3364 }
3365 }
3366 // aria-labelledby
3367 if (options.a11y.labelTargets) {
3368 for (const tr of self.targets) {
3369 const els = self.getElements({ el: tr })
3370 let str = ''
3371 for (const el of els) {
3372 str += `${el.getAttribute('id')} `
3373 }
3374 tr.setAttribute('aria-labelledby', str.trim())
3375 tr.removeAttribute('aria-label')
3376 }
3377 }
3378 }
3379 }
3380
3381 /**
3382 * init a11y change
3383 */
3384 _initA11yChange() {
3385 const self = this
3386 const options = self.options
3387 // aria-selected and aria-expanded
3388 if (options.a11y.selected || options.a11y.expanded) {
3389 if (options.a11y.expanded) {
3390 for (const tr of self.targets) {
3391 tr.setAttribute('aria-expanded', 'false')
3392 }
3393 }
3394 for (const el of self.elements) {
3395 if (options.a11y.selected && self._hasContainer && self.targets.length) {
3396 el.setAttribute('aria-selected', 'false')
3397 }
3398 // on
3399 const onHandler = Xt.dataStorage.put(
3400 el,
3401 `on.${self._componentNs}/ariaselected/${self.ns}`,
3402 self._eventA11yChangeOn.bind(self).bind(self, { el })
3403 )
3404 el.addEventListener(`on.${self._componentNs}`, onHandler)
3405 // off
3406 const offHandler = Xt.dataStorage.put(
3407 el,
3408 `off.${self._componentNs}/ariaselected/${self.ns}`,
3409 self._eventA11yChangeOff.bind(self).bind(self, { el })
3410 )
3411 el.addEventListener(`off.${self._componentNs}`, offHandler)
3412 }
3413 }
3414 }
3415
3416 /**
3417 * event a11y change on
3418 * @param {Object} params
3419 * @param {Node|HTMLElement|EventTarget|Window} params.el
3420 */
3421 _eventA11yChangeOn({ el } = {}) {
3422 const self = this
3423 const options = self.options
3424 // aria-expanded
3425 if (options.a11y.expanded) {
3426 const trs = self.getTargets({ el })
3427 for (const tr of trs) {
3428 tr.setAttribute('aria-expanded', 'true')
3429 }
3430 }
3431 // aria-selected
3432 if (options.a11y.selected && self._hasContainer && self.targets.length) {
3433 el.setAttribute('aria-selected', 'true')
3434 }
3435 }
3436
3437 /**
3438 * event a11y change off
3439 * @param {Object} params
3440 * @param {Node|HTMLElement|EventTarget|Window} params.el
3441 */
3442 _eventA11yChangeOff({ el } = {}) {
3443 const self = this
3444 const options = self.options
3445 // aria-expanded
3446 if (options.a11y.expanded) {
3447 const trs = self.getTargets({ el })
3448 for (const tr of trs) {
3449 tr.setAttribute('aria-expanded', 'false')
3450 }
3451 }
3452 // aria-selected
3453 if (options.a11y.selected && self._hasContainer && self.targets.length) {
3454 el.setAttribute('aria-selected', 'false')
3455 }
3456 }
3457
3458 /**
3459 * init a11y auto
3460 */
3461 _initA11yAuto() {
3462 const self = this
3463 const options = self.options
3464 // aria-live
3465 if (options.auto && options.auto.time) {
3466 if (options.a11y.live) {
3467 const container = self.container
3468 container.setAttribute('aria-live', 'polite')
3469 // on
3470 const onHandler = Xt.dataStorage.put(
3471 container,
3472 `autostart.${self._componentNs}/arialive/${self.ns}`,
3473 self._eventA11yAutostart.bind(self).bind(self, { container })
3474 )
3475 container.addEventListener(`autostart.${self._componentNs}`, onHandler)
3476 // off
3477 const offHandler = Xt.dataStorage.put(
3478 container,
3479 `autostop.${self._componentNs}/arialive/${self.ns}`,
3480 self._eventA11yAutostop.bind(self).bind(self, { container })
3481 )
3482 container.addEventListener(`autostop.${self._componentNs}`, offHandler)
3483 }
3484 }
3485 }
3486
3487 /**
3488 * event a11y autostart
3489 * @param {Object} params
3490 * @param {Node|HTMLElement|EventTarget|Window} params.container
3491 */
3492 _eventA11yAutostart({ container } = {}) {
3493 // aria-live
3494 container.setAttribute('aria-live', 'off')
3495 }
3496
3497 /**
3498 * event a11y autostop
3499 * @param {Object} params
3500 * @param {Node|HTMLElement|EventTarget|Window} params.container
3501 */
3502 _eventA11yAutostop({ container } = {}) {
3503 // aria-live
3504 container.setAttribute('aria-live', 'polite')
3505 }
3506
3507 /**
3508 * init a11y status
3509 * @param {Object} params
3510 * @param {Node|HTMLElement|EventTarget|Window} params.els
3511 */
3512 _initA11yStatus({ els } = {}) {
3513 const self = this
3514 const options = self.options
3515 // aria-disabled
3516 if (options.a11y.disabled) {
3517 const container = self.container
3518 // status
3519 const statusHandler = Xt.dataStorage.put(
3520 container,
3521 `status.${self._componentNs}/ariastatus/${self.ns}`,
3522 self._eventA11yStatus.bind(self).bind(self, { els })
3523 )
3524 container.addEventListener(`status.${self._componentNs}`, statusHandler)
3525 }
3526 }
3527
3528 /**
3529 * event a11y status
3530 * @param {Object} params
3531 * @param {Node|HTMLElement|EventTarget|Window} params.els
3532 */
3533 _eventA11yStatus({ els } = {}) {
3534 const self = this
3535 // aria-disabled
3536 for (const el of els) {
3537 if (self.disabled) {
3538 el.setAttribute('aria-disabled', 'true')
3539 } else {
3540 el.removeAttribute('aria-disabled')
3541 }
3542 }
3543 }
3544
3545 /**
3546 * init a11y keyboard
3547 * @param {Object} params
3548 * @param {Node|HTMLElement|EventTarget|Window} params.els
3549 */
3550 _initA11yKeyboard({ els } = {}) {
3551 const self = this
3552 const options = self.options
3553 // keyboard
3554 if (options.a11y.keyboard) {
3555 for (const el of els) {
3556 // on
3557 const onHandler = Xt.dataStorage.put(
3558 el,
3559 `on.${self._componentNs}/ariakeyboard/${self.ns}`,
3560 self._eventA11yOn.bind(self).bind(self, { el })
3561 )
3562 el.addEventListener(`on.${self._componentNs}`, onHandler)
3563 // off
3564 const offHandler = Xt.dataStorage.put(
3565 el,
3566 `off.${self._componentNs}/ariakeyboard/${self.ns}`,
3567 self._eventA11yOff.bind(self).bind(self, { el })
3568 )
3569 el.addEventListener(`off.${self._componentNs}`, offHandler)
3570 }
3571 }
3572 }
3573
3574 /**
3575 * event a11y on
3576 * @param {Object} params
3577 * @param {Node|HTMLElement|EventTarget|Window} params.el
3578 */
3579 _eventA11yOn({ el } = {}) {
3580 const self = this
3581 const options = self.options
3582 // keydown
3583 const keydownHandler = Xt.dataStorage.put(el, `keydown/ariakeyboard/${self.ns}`, self._eventA11yKeydown.bind(self))
3584 el.addEventListener('keydown', keydownHandler, { passive: false })
3585 // documentKeydown
3586 if (options.a11y.role === 'popup' || options.a11y.role === 'dialog' || options.a11y.items) {
3587 const documentKeydownHandler = Xt.dataStorage.put(
3588 document,
3589 `keydown/ariakeyboard/document/${self.ns}`,
3590 self._eventA11yDocumentKeydown.bind(self).bind(self, { el })
3591 )
3592 document.removeEventListener('keydown', documentKeydownHandler)
3593 document.addEventListener('keydown', documentKeydownHandler)
3594 }
3595 }
3596
3597 /**
3598 * event a11y off
3599 * @param {Object} params
3600 * @param {Node|HTMLElement|EventTarget|Window} params.el
3601 */
3602 _eventA11yOff({ el } = {}) {
3603 const self = this
3604 // keydown
3605 const keydownHandler = Xt.dataStorage.get(el, `keydown/ariakeyboard/${self.ns}`)
3606 el.removeEventListener('keydown', keydownHandler)
3607 // documentKeydown
3608 const documentKeydownHandler = Xt.dataStorage.get(el, `keydown/ariakeyboard/document/${self.ns}`)
3609 document.removeEventListener('keydown', documentKeydownHandler)
3610 }
3611
3612 /**
3613 * event a11y keydown
3614 * @param {Object} params
3615 * @param {Node|HTMLElement|EventTarget|Window} params.el
3616 * @param {Event} e
3617 */
3618 _eventA11yKeydown(e) {
3619 const self = this
3620 const options = self.options
3621 // disabled
3622 if (self.disabled) {
3623 return
3624 }
3625 // logic
3626 const key = e.key
3627 const prevKey = options.a11y.vertical ? 'ArrowUp' : 'ArrowLeft'
3628 const nextKey = options.a11y.vertical ? 'ArrowDown' : 'ArrowRight'
3629 // navigate elements
3630 let el
3631 if (options.a11y.vertical) {
3632 if (key === prevKey) {
3633 el = self.getPrev()
3634 } else if (key === nextKey) {
3635 el = self.getNext()
3636 }
3637 } else {
3638 if (key === prevKey) {
3639 el = self.getPrev()
3640 } else if (key === nextKey) {
3641 el = self.getNext()
3642 }
3643 }
3644 if (el) {
3645 // activation
3646 self._eventOn({ el, focus: true })
3647 // prevent page scroll
3648 e.preventDefault()
3649 return
3650 }
3651 }
3652
3653 /**
3654 * event a11y document keydown
3655 * @param {Object} params
3656 * @param {Node|HTMLElement|EventTarget|Window} params.el
3657 * @param {Event} e
3658 */
3659 _eventA11yDocumentKeydown({ el } = {}, e) {
3660 const self = this
3661 const options = self.options
3662 // disabled
3663 if (self.disabled) {
3664 return
3665 }
3666 // logic
3667 const key = e.key
3668 const prevKey = options.a11y.vertical ? 'ArrowLeft' : 'ArrowUp'
3669 const nextKey = options.a11y.vertical ? 'ArrowRight' : 'ArrowDown'
3670 // Escape
3671 if (key === 'Escape') {
3672 if (options.a11y.role === 'popup' || options.a11y.role === 'dialog') {
3673 // activation
3674 self._eventOff({ el, focus: true })
3675 return
3676 }
3677 }
3678 // navigate items
3679 if (options.a11y.items) {
3680 let item
3681 const items = []
3682 const trs = self.targets.filter(x => self.hasCurrent({ el: x }))
3683 for (const tr of trs) {
3684 items.push(...tr.querySelectorAll(options.a11y.items))
3685 }
3686 if (items.length) {
3687 const current = items.indexOf(document.activeElement)
3688 if (key === prevKey) {
3689 if (current === -1) {
3690 item = items[items.length - 1]
3691 } else {
3692 const prev = (current - 1 + items.length) % items.length
3693 item = items[prev]
3694 }
3695 // prevent page scroll
3696 e.preventDefault()
3697 } else if (key === nextKey) {
3698 if (current === -1) {
3699 item = items[0]
3700 } else {
3701 const next = (current + 1) % items.length
3702 item = items[next]
3703 }
3704 // prevent page scroll
3705 e.preventDefault()
3706 } else {
3707 self._search = self._search + e.key.toLowerCase()
3708 const found = items.filter(x => x.innerText?.toLowerCase().startsWith(self._search))
3709 if (found.length) {
3710 item = found[0]
3711 }
3712 // clear
3713 clearTimeout(Xt.dataStorage.get(document, `${self.ns}SearchTimeout`))
3714 Xt.dataStorage.set(
3715 document,
3716 `${self.ns}SearchTimeout`,
3717 setTimeout(() => {
3718 self._search = ''
3719 }, 500)
3720 )
3721 }
3722 if (item) {
3723 // focus
3724 item.focus()
3725 return
3726 }
3727 }
3728 }
3729 }
3730
3731 //
3732 // util
3733 //
3734
3735 /**
3736 * Reinit component and save currents as initial
3737 * @param {Object} params
3738 * @param {Boolean} params.save Save currents
3739 */
3740 reinit({ save = true } = {}) {
3741 const self = this
3742 const options = self.options
3743 // need to reset appendto
3744 if (options.disableDeactivate) {
3745 for (const tr of self.targets.filter(x => self.hasCurrent({ el: x }))) {
3746 self._specialAppendto({ actionCurrent: 'Out', el: tr, type: 'targets' })
3747 }
3748 }
3749 // reinit
3750 self._initLogic({ save })
3751 }
3752
3753 /**
3754 * restart
3755 */
3756 restart() {
3757 const self = this
3758 // restart
3759 self._initStart()
3760 // dispatch event
3761 self.container.dispatchEvent(new CustomEvent(`restart.${self._componentNs}`))
3762 }
3763
3764 /**
3765 * add namespace
3766 */
3767 _addNamespace() {
3768 const self = this
3769 // xtNamespace linked components
3770 if (self._mode === 'unique') {
3771 const arr = Xt.dataStorage.get(self.ns, 'xtNamespace') ?? []
3772 if (!arr.includes(self)) {
3773 arr.push(self)
3774 Xt.dataStorage.set(self.ns, 'xtNamespace', arr)
3775 }
3776 }
3777 }
3778
3779 /**
3780 * remove namespace
3781 */
3782 _removeNamespace() {
3783 const self = this
3784 // xtNamespace linked components
3785 if (self._mode === 'unique') {
3786 let arr = Xt.dataStorage.get(self.ns, 'xtNamespace')
3787 if (arr) {
3788 arr = arr.filter(x => x !== self)
3789 Xt.dataStorage.set(self.ns, 'xtNamespace', arr)
3790 }
3791 }
3792 }
3793
3794 /**
3795 * removeEvents
3796 */
3797 _removeEvents() {
3798 const self = this
3799 // remove internal events on self._destroyElements
3800 for (const el of self._destroyElements) {
3801 const storages = Xt.dataStorage.getAll(el)
3802 if (storages) {
3803 for (const [key] of storages) {
3804 if (key) {
3805 if (key.endsWith(self.ns)) {
3806 const handler = Xt.dataStorage.get(el, key)
3807 if (typeof handler === 'function') {
3808 const events = key.split('/')[0].split(' ')
3809 for (const event of events) {
3810 el.removeEventListener(event, handler)
3811 el.removeEventListener(event, handler, true) // useCapture event propagation
3812 }
3813 // do not remove key because they are not overrided with Xt.dataStorage.put, or they trigger multiple times Xt.dataStorage.remove(element, key)
3814 }
3815 }
3816 }
3817 }
3818 }
3819 }
3820 }
3821
3822 /**
3823 * destroy
3824 * @param {Object} params
3825 * @param {Boolean} params.weak Do not destroy component
3826 */
3827 destroy({ weak = false } = {}) {
3828 const self = this
3829 // disable
3830 self.disable({ skipEvent: true })
3831 // remove matches
3832 Xt._removeMatches({ self, optionsInitial: self._optionsInitial })
3833 // remove events
3834 self._removeEvents()
3835 // namespace
3836 self._removeNamespace()
3837 // weak
3838 if (!weak) {
3839 // initialized class
3840 self.container.removeAttribute(`data-${self.componentName}-init`)
3841 // set self
3842 Xt._remove({ name: self.componentName, el: self.container })
3843 // dispatch event
3844 self.container.dispatchEvent(new CustomEvent(`destroy.${self._componentNs}`))
3845 // delete
3846 delete this
3847 }
3848 }
3849
3850 //
3851}
3852
3853//
3854// options
3855//
3856
3857Toggle.componentName = 'xt-toggle'
3858Toggle.optionsDefaultSuper = {
3859 debug: false,
3860 // element
3861 elements: '[data-xt-toggle-element]',
3862 targets: '[data-xt-toggle-target]',
3863 elementsInner: false,
3864 targetsInner: false,
3865 exclude: false,
3866 // class
3867 class: 'on',
3868 classIn: 'in',
3869 classOut: 'out',
3870 classDone: 'done',
3871 classInitial: 'initial',
3872 classBefore: 'dir-before',
3873 classAfter: 'dir-after',
3874 classSkip: false,
3875 hash: 'data-xt-hash',
3876 groupSeparator: ',',
3877 groupElements: true,
3878 // quantity
3879 min: 0,
3880 max: 1,
3881 // event
3882 on: 'click',
3883 off: 'click',
3884 mouseParent: false,
3885 eventLimit: '.xt-event-limit',
3886 closeDeep: false,
3887 closeInside: false,
3888 closeOutside: false,
3889 preventEvent: false,
3890 // timing
3891 queue: {
3892 elements: false,
3893 targets: true,
3894 elementsInner: false,
3895 targetsInner: true,
3896 },
3897 delay: false,
3898 delayIn: false,
3899 delayOut: false,
3900 duration: false,
3901 durationIn: false,
3902 durationOut: false,
3903 // auto
3904 auto: {
3905 time: false,
3906 initial: true,
3907 step: 1,
3908 inverse: false,
3909 pause: 'a, button',
3910 loop: true,
3911 },
3912 // other
3913 disableDeactivate: false,
3914 scrollto: false,
3915 scrolltoInit: false,
3916 matches: false,
3917 disabled: false,
3918 visibleReinit: false,
3919 loop: false,
3920 jump: false,
3921 navigation: false,
3922 appendTo: false,
3923 classBody: false,
3924 closeauto: false,
3925 openauto: false,
3926 mediaLoaded: false,
3927 mediaLoadedReinit: false,
3928 zIndex: false,
3929 focusLimit: false,
3930 focusTrap: {
3931 initialFocus: false,
3932 preventScroll: true,
3933 allowOutsideClick: true,
3934 fallbackFocus: 'body', // needed to prevent error on deactivation sometimes
3935 },
3936 collapseHeight: false,
3937 collapseWidth: false,
3938 a11y: {
3939 role: false,
3940 labelElements: false,
3941 labelTargets: true,
3942 controls: true,
3943 selected: false,
3944 expanded: false,
3945 live: true,
3946 disabled: true,
3947 keyboard: true,
3948 vertical: false,
3949 items: false,
3950 },
3951}
3952
3953//
3954// export
3955//
3956
3957Xt.Toggle = Toggle
3958
3959//
3960// observe
3961//
3962
3963if (typeof window !== 'undefined') {
3964 Xt.mount({
3965 matches: `[data-${Xt.Toggle.componentName}]`,
3966 mount: ({ ref }) => {
3967 // vars
3968
3969 const optionsMarkup = ref.getAttribute(`data-${Xt.Toggle.componentName}`)
3970 const options = optionsMarkup ? JSON5.parse(optionsMarkup) : {}
3971
3972 // init
3973
3974 let self = new Xt.Toggle(ref, options)
3975
3976 // unmount
3977
3978 return () => {
3979 self.destroy()
3980 self = null
3981 }
3982 },
3983 })
3984}