UNPKG

xtendui

Version:

Xtend UI is a powerful frontend library of Tailwind CSS components enhanced by vanilla js. It helps you build interfaces with advanced interactions and animations.

1,560 lines (1,510 loc) 121 kB
/*! * Xtend UI (https://xtendui.github.io/xtendui/) * @copyright (c) 2017-2026 Riccardo Caroli * @license MIT (https://github.com/xtendui/xtendui/blob/master/LICENSE.txt) */ import { Xt } from '../xt.mjs' /** * ToggleInit */ export class ToggleInit { // // init // /** * init */ _init() { const self = this // init self._initVars() self._initLogic() } /** * init vars */ _initVars() { const self = this // options self._optionsDefault = Xt.merge([self.constructor.optionsDefaultSuper, self.constructor.optionsDefault]) self._optionsDefault = Xt.merge([self._optionsDefault, Xt.options[self.componentName]]) self._optionsInitial = self.options = Xt.merge([self._optionsDefault, self._optionsCustom]) // classes const options = self.options self._classes = options.class ? options.class.split(' ') : [] self._classesIn = options.classIn ? options.classIn.split(' ') : [] self._classesOut = options.classOut ? options.classOut.split(' ') : [] self._classesDone = options.classDone ? options.classDone.split(' ') : [] self._classesInitial = options.classInitial ? options.classInitial.split(' ') : [] self._classesBefore = options.classBefore ? options.classBefore.split(' ') : [] self._classesAfter = options.classAfter ? options.classAfter.split(' ') : [] } /** * init logic * @param {Object} params * @param {Boolean} params.save Save currents */ _initLogic({ save = true } = {}) { const self = this // vars self._destroyElements = [document, window, self.container] // enable first for proper initial activation self.enable() // init self._initSetup() Xt._initMatches({ self, optionsInitial: self._optionsInitial }) self._initScope() self._initEvents() self._initA11y() self._initStart({ save }) // disable last for proper options.disableDeactivate if (self.options.disabled || self._disabledManual) { self.disable() } } /** * init setup */ _initSetup() { const self = this const options = self.options // mode self._containerTargets = self.container if (options.targets && options.targets.indexOf('#') !== -1) { self._mode = 'unique' self._containerTargets = document.documentElement self.ns = `${self.componentName}-${options.targets.toString()}-${self._classes.toString()}` } else { self._mode = 'multiple' self.ns = self.ns ?? Xt.uniqueId() } // final namespace self.ns = self.ns.replace(/^[^a-z]+|[ ,#_:.-]+/gi, '') // namespace self._addNamespace() // currents array based on namespace (so shared between Xt objects) self._setCurrents([]) } /** * init elements, targets and currents */ _initScope() { const self = this // elements self._initScopeElements() // targets self._initScopeTargets() } /** * init elements */ _initScopeElements() { const self = this const options = self.options // elements self._containerElements = self.container if (options.elements) { if (options.elements.indexOf('#') !== -1) { self._containerElements = document.documentElement } let arr = Array.from(self._containerElements.querySelectorAll(options.elements)) if (options.exclude) { arr = arr.filter(x => !x.matches(options.exclude)) } self.elements = arr self._destroyElements.push(...self.elements) } // object if no elements if (!self.elements.length) { self.elements = [self.container] } // elementsInner if (options.elementsInner) { for (const el of self.elements) { const elements = Xt.queryAll({ els: el, query: options.elementsInner }) Xt.dataStorage.set(el, `elementsInner/${self.ns}`, elements) } } } /** * init targets */ _initScopeTargets() { const self = this const options = self.options // targets if (options.targets) { let arr = Array.from(self._containerTargets.querySelectorAll(options.targets)) if (options.exclude) { arr = arr.filter(x => !x.matches(options.exclude)) } self.targets = arr self._destroyElements.push(...self.targets) // elementsInner if (options.targetsInner) { for (const tr of self.targets) { const elements = Xt.queryAll({ els: tr, query: options.targetsInner }) Xt.dataStorage.set(tr, `targetsInner/${self.ns}`, elements) } } } } /** * init start * @param {Object} params * @param {Boolean} params.save Save currents */ _initStart({ save = false } = {}) { const self = this const options = self.options // currents self._setCurrents([]) // vars let currents = 0 self.initial = true self.index = null self._oldIndex = null Xt._running[self.ns] = [] // INSTANT ACTIVATION because we need activation classes right away (e.g.: slider inside demos toggle must be visible to get values) // check initial activation currents = self._initActivate({ save }) // if currents < min let todo = options.min - currents if (todo > 0) { // initial currents += todo for (let i = 0; i < todo; i++) { const el = self.elements[i] if (el && !el.classList.contains(self._classes) && !el.checked) { // toggle event if present because of custom listeners if (options.on) { const event = options.on.split(' ')[0] const elEvent = self._getEventParent({ el, event }) elEvent.dispatchEvent(new CustomEvent(event, { detail: { force: true } })) } else { self._eventOn({ el, force: true }) } } else if (todo < self.elements.length) { todo++ } } } // currents if (save) { self._initialCurrents = self._getCurrents().slice(0) // copy array with slice(0) } // no currents if (currents === 0) { // setup // dispatch event self.container.dispatchEvent(new CustomEvent(`setup.${self._componentNs}`)) // needs frameDouble after ondone Xt.frameDouble({ el: self.container, ns: `${self.ns}Init`, func: () => { // init // fix before _initScope or slider absolute has multiple active and bugs initial calculations self.container.setAttribute(`data-${self.componentName}-init`, '') // dispatch event self.container.dispatchEvent(new CustomEvent(`init.${self._componentNs}`)) // fix autostart after self.initial or it gives error on reinitialization (demos fullscreen) self._eventAutostart() // initial after autostart self.initial = false // debug if (options.debug) { // eslint-disable-next-line no-console console.debug(`${self.componentName} init`, self) } }, }) } } /** * init activate * @param {Object} params * @param {Boolean} params.save Save currents * @return {Number} currents count */ _initActivate({ save = false } = {}) { const self = this const options = self.options // check const checkClass = el => { for (const c of self._classes) { if (el.classList.contains(c) || el.checked) { return true } } return false } // check hash const obj = self._hashChange({ save }) let currents = obj.currents ?? 0 // check class for (const el of self.getElementsGroups()) { let activated = false // check if activated if (save) { if (options.classSkip !== true && !options.classSkip.elements) { activated = checkClass(el) } } else if (self._initialCurrents.includes(el)) { activated = true } // check if activated // fix check options.max for currents of _hashChange current reset if hash has current // fix check obj.arr has element already activated if ((activated && currents < options.max) || obj.arr.includes(el)) { // instant animation el.classList.add(...self._classes) el.classList.add(...self._classesIn) el.classList.add(...self._classesInitial) } else { // reset classes if (options.classSkip !== true && !options.classSkip.elements) { const elsSame = self.getElements({ el }) for (const elSame of elsSame) { elSame.classList.remove( ...self._classes, ...self._classesIn, ...self._classesOut, ...self._classesDone, ...self._classesInitial, ...self._classesBefore, ...self._classesAfter, ) } } if (options.elementsInner) { if (options.classSkip !== true && !options.classSkip.elementsInner) { const elementsInner = Xt.dataStorage.get(el, `elementsInner/${self.ns}`) for (const elementInner of elementsInner) { elementInner.classList.remove( ...self._classes, ...self._classesIn, ...self._classesOut, ...self._classesDone, ...self._classesInitial, ...self._classesBefore, ...self._classesAfter, ) } } } } // check targets const targets = self.getTargets({ el }) for (const tr of targets) { // check if activated if (save && !activated) { if (options.classSkip !== true && !options.classSkip.targets) { activated = checkClass(tr) } } // check if activated // fix check options.max for currents of _hashChange current reset if hash has current // fix check tr with same activation const els = self.getElements({ el: tr, same: true }) if ((activated && currents < options.max) || obj.arr.some(x => els.includes(x))) { // instant animation tr.classList.add(...self._classes) tr.classList.add(...self._classesIn) tr.classList.add(...self._classesInitial) } else { // reset classes if (options.classSkip !== true && !options.classSkip.targets) { tr.classList.remove( ...self._classes, ...self._classesIn, ...self._classesOut, ...self._classesDone, ...self._classesInitial, ...self._classesBefore, ...self._classesAfter, ) } if (options.targetsInner) { if (options.classSkip !== true && !options.classSkip.targetsInner) { const targetsInner = Xt.dataStorage.get(tr, `targetsInner/${self.ns}`) for (const targetInner of targetsInner) { targetInner.classList.remove( ...self._classes, ...self._classesIn, ...self._classesOut, ...self._classesDone, ...self._classesInitial, ...self._classesBefore, ...self._classesAfter, ) } } } } } // activate if (activated && currents < options.max) { // initial currents++ // fix check tr with same activation obj.arr.push(el) // toggle event if present because of custom listeners if (options.on) { const event = options.on.split(' ')[0] const elEvent = self._getEventParent({ el, event }) elEvent.dispatchEvent(new CustomEvent(event, { detail: { force: true } })) } else { self._eventOn({ el, force: true }) } } } // return return currents } /** * init events */ _initEvents() { const self = this const options = self.options // remove events self._removeEvents() // elements for (const el of self.elements) { // event on const onHandlerCustom = Xt.dataStorage.put( el, `${options.on}/oncustom/${self.ns}`, self._eventOnHandler.bind(self, { el, force: true }), ) el.addEventListener(`on.trigger.${self._componentNs}`, onHandlerCustom) if (options.on) { const events = options.on.split(' ') for (const event of events) { const elEvent = self._getEventParent({ el, event }) if (elEvent !== el) { self._destroyElements.push(elEvent) } const onHandler = Xt.dataStorage.put( elEvent, `${options.on}/on/${self.ns}`, self._eventOnHandler.bind(self, { el }), ) elEvent.addEventListener(event, onHandler) } } // event off const offHandlerCustom = Xt.dataStorage.put( el, `${options.off}/offcustom/${self.ns}`, self._eventOffHandler.bind(self, { el, force: true }), ) el.addEventListener(`off.trigger.${self._componentNs}`, offHandlerCustom) if (options.off) { const events = options.off.split(' ') for (const event of events) { // same event for on and off same namespace if (!options.on.split(' ').includes(event)) { const elEvent = self._getEventParent({ el, event }) if (elEvent !== el) { self._destroyElements.push(elEvent) } const offHandler = Xt.dataStorage.put( elEvent, `${options.off}/off/${self.ns}`, self._eventOffHandler.bind(self, { el }), ) elEvent.addEventListener(event, offHandler) } } } // preventEvent if (options.on) { if (options.preventEvent) { const events = options.on.split(' ') if (events.includes('click') || events.includes('mouseenter') || events.includes('mousehover')) { // prevent touch links const preventeventStartHandler = Xt.dataStorage.put( el, `touchend/preventevent/${self.ns}`, self._eventPreventeventStartHandler.bind(self, { el }), ) el.addEventListener('touchend', preventeventStartHandler) } if (events.includes('click')) { // prevent click links const preventeventStartHandler = Xt.dataStorage.put( el, `mouseup keyup/preventevent/${self.ns}`, self._eventPreventeventStartHandler.bind(self, { el }), ) el.addEventListener('mouseup', preventeventStartHandler) el.addEventListener('keyup', preventeventStartHandler) } } Xt.dataStorage.put(el, `active/preventevent/${self.ns}`, self.hasCurrent({ el })) } } // targets let skipTargetsTrigger = false if (self._mode === 'unique') { const selfs = Xt.dataStorage.get(document.documentElement, `xtNamespace${self.ns}`) if (selfs.length > 1) { skipTargetsTrigger = true } } if (!skipTargetsTrigger) { for (const tr of self.targets) { // event on const onHandlerCustom = Xt.dataStorage.put( tr, `${options.on}/oncustom/${self.ns}`, self._eventOnHandler.bind(self, { el: tr, force: true }), ) tr.addEventListener(`on.trigger.${self._componentNs}`, onHandlerCustom) // event off const offHandlerCustom = Xt.dataStorage.put( tr, `${options.off}/offcustom/${self.ns}`, self._eventOffHandler.bind(self, { el: tr, force: true }), ) tr.addEventListener(`off.trigger.${self._componentNs}`, offHandlerCustom) } } // auto if (options.auto && options.auto.time) { const autostartHandler = Xt.dataStorage.put( self.container, `autostart/${self.ns}`, self._eventAutostart.bind(self), ) const autostopHandler = Xt.dataStorage.put(self.container, `autostop/${self.ns}`, self._eventAutostop.bind(self)) // focus // Xt.dataStorage.set with window to fix unique mode same self.ns const focusHandler = Xt.dataStorage.set(window, `focus/auto/${self.ns}`, autostartHandler) addEventListener('focus', focusHandler) // blur // Xt.dataStorage.set with window to fix unique mode same self.ns const blurHandler = Xt.dataStorage.set(window, `blur/auto/${self.ns}`, autostopHandler) addEventListener('blur', blurHandler) // event self.container.addEventListener(`autostart.trigger.${self._componentNs}`, autostartHandler) self.container.addEventListener(`autostop.trigger.${self._componentNs}`, autostopHandler) // autopause if (options.auto.pause) { const autopauseEls = self.container.querySelectorAll(options.auto.pause) if (autopauseEls.length) { self._destroyElements.push(...autopauseEls) for (const el of autopauseEls) { // pause const autopauseOnHandler = Xt.dataStorage.put( el, `mouseenter focus/auto/${self.ns}`, self._eventAutostop.bind(self), ) const eventsPause = ['mouseenter', 'focus'] for (const event of eventsPause) { el.addEventListener(event, autopauseOnHandler) } // resume const autoresumeOnHandler = Xt.dataStorage.put( el, `mouseleave blur/auto/${self.ns}`, self._eventAutostart.bind(self), ) const eventsResume = ['mouseleave', 'blur'] for (const event of eventsResume) { el.addEventListener(event, autoresumeOnHandler) } } } } } // hash if (options.hash) { for (const el of self.elements) { if (el.getAttribute(options.hash)) { self._hasHash = true break } } if (!self._hasHash) { for (const tr of self.targets) { if (tr.getAttribute(options.hash)) { self._hasHash = true break } } } } if (self._hasHash) { // hash const hashHandler = Xt.dataStorage.put( window, `popstate/${self.ns}`, self._hashChange.bind(self).bind(self, { save: true }), ) addEventListener('popstate', hashHandler) } // jump if (options.jump) { for (const el of self.targets) { const jumpHandler = Xt.dataStorage.put( el, `click/jump/${self.ns}`, self._eventJumpHandler.bind(self).bind(self, { el }), ) el.addEventListener('click', jumpHandler, true) // fix elements inside targets (slider pagination) // jump if (!self.disabled) { el.classList.add('xt-jump') } } } // navigation if (options.navigation) { self.navs = self.container.querySelectorAll(options.navigation) if (self.navs.length) { self._destroyElements.push(...self.navs) for (const el of self.navs) { const navHandler = Xt.dataStorage.put( el, `click/nav/${self.ns}`, self._eventNavHandler.bind(self).bind(self, { el }), ) el.addEventListener('click', navHandler) } } } // closeauto if (options.closeauto) { // Xt.dataStorage.set with window to fix unique mode same self.ns const closeautoHandler = Xt.dataStorage.set( window, `closeauto.trigger.xt/${self.ns}`, self._eventCloseautoHandler.bind(self), ) addEventListener('closeauto.trigger.xt', closeautoHandler, true) // useCapture event propagation } if (options.openauto) { // Xt.dataStorage.set with window to fix unique mode same self.ns const openautoHandler = Xt.dataStorage.set( window, `openauto.trigger.xt/${self.ns}`, self._eventOpenautoHandler.bind(self), ) addEventListener('openauto.trigger.xt', openautoHandler, true) // useCapture event propagation } // mediaLoaded if (options.mediaLoaded || options.mediaLoadedReinit) { for (const el of self.elements) { const imgs = Array.from(el.querySelectorAll('img')) self._destroyElements.push(...imgs) for (const img of imgs) { if (!Xt.dataStorage.get(img, `${self.ns}MedialoadedDone`)) { if (!img.complete) { const medialoadedHandler = Xt.dataStorage.put( img, `load/media/${self.ns}`, self._eventMedialoadedHandler.bind(self).bind(self, { img, el, deferred: true }), ) img.addEventListener('load', medialoadedHandler) } else { self._eventMedialoadedHandler({ img, el }) } } } } for (const tr of self.targets) { const imgs = Array.from(tr.querySelectorAll('img')) self._destroyElements.push(...imgs) for (const img of imgs) { if (!Xt.dataStorage.get(img, `${self.ns}MedialoadedDone`)) { if (!img.complete) { const medialoadedHandler = Xt.dataStorage.put( img, `load/media/${self.ns}`, self._eventMedialoadedHandler.bind(self).bind(self, { img, el: tr, deferred: true, reinit: true }), ) img.addEventListener('load', medialoadedHandler) } else { self._eventMedialoadedHandler({ img, el: tr }) } } } } } // visibleReinit if (options.visibleReinit) { if (!Xt.visible({ el: self.container })) { // intersection observer self._observer = new IntersectionObserver( (entries, observer) => { for (const entry of entries) { if (entry.intersectionRatio > 0) { self._eventVisibleReinit() observer.disconnect() self._observer = null } } }, { root: null }, ) self._observer.observe(self.container) } } } // // handler // /** * element on handler * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {Boolean} params.force * @param {Event} e */ _eventOnHandler({ el, force = false }, e) { const self = this const options = self.options force = force ? force : e?.detail?.force // fix groupElements and targets el = options.groupElements || self.targets.includes(el) ? self.getElements({ el })[0] : el // handler if (!force && options.eventLimit) { const eventLimit = self._containerElements.querySelectorAll(options.eventLimit) if (self._containerElements.matches(options.eventLimit)) { return } else if (eventLimit.length) { if (Xt.contains({ els: eventLimit, tr: e.target })) { return } } } self._eventOn({ el, force }, e) } /** * element off handler * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {Boolean} params.force * @param {Event} e */ _eventOffHandler({ el, force = false }, e) { const self = this const options = self.options force = force ? force : e?.detail?.force // fix groupElements and targets el = options.groupElements || self.targets.includes(el) ? self.getElements({ el })[0] : el // handler if (!force && options.eventLimit) { const eventLimit = self._containerElements.querySelectorAll(options.eventLimit) if (self._containerElements.matches(options.eventLimit)) { return } else if (eventLimit.length) { if (Xt.contains({ els: eventLimit, tr: e.target })) { return } } } self._eventOff({ el, force }, e) } /** * init prevents click on touch until clicked two times * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el */ _eventPreventeventStartHandler({ el } = {}) { const self = this // active Xt.dataStorage.put(el, `active/preventevent/${self.ns}`, self.hasCurrent({ el })) // prevent link but execute on.xt because added before const preventeventHandler = Xt.dataStorage.put( el, `click keypress/preventevent/${self.ns}`, self._eventPreventeventHandler.bind(self, { el }), ) el.addEventListener('click', preventeventHandler) el.addEventListener('keypress', preventeventHandler) // reset prevent event const preventeventResetHandler = Xt.dataStorage.put( el, `off/preventevent/${self.ns}`, self._eventPreventeventResetHandler.bind(self, { el }), ) el.addEventListener(`off.${self._componentNs}`, preventeventResetHandler) } /** * remove prevents click on touch until clicked two times * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el */ _eventPreventeventEndHandler({ el } = {}) { const self = this // event link const preventeventHandler = Xt.dataStorage.get(el, `click/preventevent/${self.ns}`) el.removeEventListener('click', preventeventHandler) // event reset const preventeventResetHandler = Xt.dataStorage.get(el, `off/preventevent/${self.ns}`) el.removeEventListener(`off.${self._componentNs}`, preventeventResetHandler) } /** * prevents click on touch until clicked two times * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {Event} e */ _eventPreventeventHandler({ el }, e) { const self = this const active = Xt.dataStorage.get(el, `active/preventevent/${self.ns}`) // only no key or key enter if (e.key && e.key !== 'Enter') { return } // logic if (!active && !Xt.dataStorage.get(el, `${self.ns}PreventeventDone`)) { Xt.dataStorage.set(el, `${self.ns}PreventeventDone`, true) // prevent default e.preventDefault() } else { self._eventPreventeventEndHandler({ el }) Xt.dataStorage.remove(el, `${self.ns}PreventeventDone`) Xt.dataStorage.remove(el, `active/preventevent/${self.ns}`) } } /** * reset prevents click on touch until clicked two times * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el */ _eventPreventeventResetHandler({ el } = {}) { const self = this self._eventPreventeventEndHandler({ el }) Xt.dataStorage.remove(el, `${self.ns}PreventeventDone`) Xt.dataStorage.remove(el, `active/preventevent/${self.ns}`) } /** * hash change * @param {Object} params * @param {Boolean} params.save Save currents * @return {Object} return * @return {Number} return.currents * @return {Array} return.arr */ _hashChange({ save = false } = {}) { const self = this const options = self.options // vars let currents = 0 const arr = [] // disabled if (self.disabled) { return { currents, arr } } // logic if (self._hasHash) { if (!Xt.dataStorage.get(self.container, `${self.ns}HashSkip`)) { const hash = decodeURI(location.hash.split('#')[1]) if (hash) { // check const checkHash = (el, hash) => { if (el.getAttribute(options.hash) === hash) { return true } return false } // check hash for (const el of self.elements) { let activated = false // check if activated if (save) { activated = checkHash(el, hash) } // check targets const targets = self.getTargets({ el }) for (const tr of targets) { // check if activated if (save && !activated) { activated = checkHash(tr, hash) } } // activate if (activated && currents < options.max) { // initial currents++ arr.push(el) // toggle event if present because of custom listeners Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, true) if (options.on) { const event = options.on.split(' ')[0] el.dispatchEvent(new CustomEvent(event, { detail: { force: true } })) } else { self._eventOn({ el, force: true }) } Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, false) } } } } } // return return { currents, arr } } /** * jump handler * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {Event} e */ _eventJumpHandler({ el }, e) { const self = this // disabled if (self.disabled) { return } // useCapture event propagation check if (self.targets.includes(el)) { // handler self._eventJump({ el }, e) } } /** * nav handler * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {Event} e */ _eventNavHandler({ el }, e) { const self = this // handler self._eventNav({ el }, e) } /** * closeauto handler * @param {Event} e */ _eventCloseautoHandler(e) { const self = this // triggering e.detail.container if (!e?.detail?.container || e?.detail?.container.contains(self.container)) { // handler const currents = self._getCurrents() for (const current of currents) { self._eventOff({ el: current, force: true }, e) } } } /** * openauto handler * @param {Event} e */ _eventOpenautoHandler(e) { const self = this // handler let found for (const el of Array.from(self.elements).filter(x => x.contains(e.target))) { found = el break } if (!found) { for (const tr of Array.from(self.targets).filter(x => x.contains(e.target))) { found = tr break } } if (found) { self._eventOn({ el: found }, e) } } /** * medialoaded * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.img * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {Boolean} params.deferred * @param {Boolean} params.reinit */ _eventMedialoadedHandler({ img, el, deferred = false, reinit = false } = {}) { const self = this const options = self.options // fix multiple calls Xt.dataStorage.set(img, `${self.ns}MedialoadedDone`, true) const medialoadedHandler = Xt.dataStorage.get(img, `load/media/${self.ns}`) img.removeEventListener('load', medialoadedHandler) // mediaLoadedReinit if (options.mediaLoadedReinit && deferred && reinit) { clearTimeout(Xt.dataStorage.get(self.container, `${self.ns}MedialoadedTimeout`)) Xt.dataStorage.set( self.container, `${self.ns}MedialoadedTimeout`, setTimeout(() => { self._eventMediaLoadedReinit() }, Xt.medialoadedDelay), ) } // mediaLoaded if (options.mediaLoaded) { el.classList.add('xt-medialoaded') } // dispatch event el.dispatchEvent( new CustomEvent(`medialoaded.${self._componentNs}`, { detail: { deferred: deferred }, }), ) } // // event util // /** * Get all elements from element or target * @return {Array} array of elements */ getElementsGroups() { const self = this // groups const groups = [] for (const el of self.elements) { // choose element by group const group = el.getAttribute('data-xt-group') if (group) { const alreadyFound = groups.filter(x => x.getAttribute('data-xt-group') === group) if (!alreadyFound.length) { groups.push(el) } } else { groups.push(el) } } return groups } /** * filter elements or targets array with groups array * @param {Object} params * @param {Array} params.els Elements or Targets * @param {String} params.attr Groups attribute * @param {Boolean} params.some Filter also if some in Groups attribute * @param {Boolean} params.same Use also data-xt-group-same * @return {Array} Filtered array */ _groupFilter({ els, attr, some = false, same = false } = {}) { const self = this const options = self.options // logic const found = [] for (const el of els) { let currentAttr = el.getAttribute('data-xt-group') if (same) { const currentAttrSame = el.getAttribute('data-xt-group-same') if (currentAttrSame) { currentAttr += options.groupSeparator + currentAttrSame } } // if same attr if (currentAttr === attr) { found.push(el) continue } // if some in attr if (some) { const groups = attr?.split(options.groupSeparator).filter(x => x) // filter out nullish const currentGroups = currentAttr?.split(options.groupSeparator).filter(x => x) // filter out nullish if (currentGroups && groups && currentGroups.some(x => groups.includes(x))) { found.push(el) } } } return found } /** * get elements from element or target * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating * @param {Boolean} params.same Use also data-xt-group-same * @return {Array} The first element is the one on getElementsGroups() */ getElements({ el, same = false } = {}) { const self = this const options = self.options // getElements if (!self.elements || !self.elements.length) { return [] } if (!el) { return [] } else if (self._mode === 'unique') { // xtNamespace linked components const final = [] const selfs = Xt.dataStorage.get(document.documentElement, `xtNamespace${self.ns}`) if (selfs) { for (const s of selfs) { // choose element by group final.push(...s.elements) } return final } return [] } else if (self._mode === 'multiple') { // choose element by group let final let attr = el.getAttribute('data-xt-group') if (same) { const attrSame = el.getAttribute('data-xt-group-same') if (attrSame) { attr += options.groupSeparator + attrSame } } const some = self.elements.includes(el) ? false : true // data-xt-group some only if finding elements from targets const groupElements = self._groupFilter({ els: self.elements, attr, some, same }) const groupTargets = self._groupFilter({ els: self.targets, attr, some, same }) if (attr) { // if group all group targets final = groupElements } else { // not group targets by index if (Array.from(self.elements).includes(el)) { final = [el].filter(x => x) // filter out nullish } else { // groupElements and groupTargets are elements and targets without data-xt-group here const index = groupTargets.findIndex(x => x === el) final = [groupElements[index]].filter(x => x) // filter out nullish } } return final } } /** * Get all targets from element or target * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating * @param {Boolean} params.same Use also data-xt-group-same * @return {Array} */ getTargets({ el, same = false } = {}) { const self = this const options = self.options // getTargets if (!self.targets || !self.targets.length) { return [] } if (!el) { return [] } else if (self._mode === 'unique') { // xtNamespace linked components const final = self.targets return final } else if (self._mode === 'multiple') { // choose only target by group let final let attr = el.getAttribute('data-xt-group') if (same) { const attrSame = el.getAttribute('data-xt-group-same') if (attrSame) { attr += options.groupSeparator + attrSame } } const some = self.targets.includes(el) ? false : true // data-xt-group some only if finding targets from elements const groupElements = self._groupFilter({ els: self.elements, attr, some, same }) const groupTargets = self._groupFilter({ els: self.targets, attr, some, same }) if (attr) { // if group all group targets final = groupTargets } else { // not group targets by index if (Array.from(self.targets).includes(el)) { final = [el].filter(x => x) // filter out nullish } else { // groupElements and groupTargets are elements and targets without data-xt-group here const index = groupElements.findIndex(x => x === el) final = [groupTargets[index]].filter(x => x) // filter out nullish } } return final } } /** * Get elements inner * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.els Elements * @return {Array} */ _getElementsInner({ els } = {}) { const self = this const options = self.options // inners let inners = [] if (options.elementsInner) { for (const el of els) { const inner = Xt.dataStorage.get(el, `elementsInner/${self.ns}`) if (inner.length) { inners = inners.concat(inner) } } } return inners } /** * Get targets inner * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.els Elements * @return {Array} */ _getTargetsInner({ els } = {}) { const self = this const options = self.options // inners let inners = [] if (options.targetsInner) { for (const el of els) { const inner = Xt.dataStorage.get(el, `targetsInner/${self.ns}`) if (inner.length) { inners = inners.concat(inner) } } } return inners } /** * get event parent * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {String} params.event * @return {Node|HTMLElement|EventTarget|Window} */ _getEventParent({ el, event } = {}) { const self = this const options = self.options // getCurrents if (options.mouseParent) { if (['mouseenter', 'mouseleave', 'mousehover', 'mouseout'].includes(event)) { if (typeof options.mouseParent === 'string') { return el.closest(options.mouseParent) } else { return el.parentNode } } } return el } /** * get currents based on namespace (so shared between Xt objects) * @return {Array} */ _getCurrents() { const self = this // getCurrents return Xt._currents[self.ns] } /** * set currents based on namespace (so shared between Xt objects) * @param {Array} arr */ _setCurrents(arr) { const self = this // setCurrents Xt._currents[self.ns] = arr } /** * add current based on namespace (so shared between Xt objects) * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el * @param {Boolean} params.running Running currents */ _addCurrent({ el, running = false } = {}) { const self = this // addCurrent if (!self.hasCurrent({ el, running })) { const arr = running ? Xt._running : Xt._currents arr[self.ns].push(el) } } /** * remove currents based on namespace (so shared between Xt objects) * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el To be removed * @param {Boolean} params.running Running currents */ _removeCurrent({ el, running = false } = {}) { const self = this // removeCurrent const arr = running ? Xt._running : Xt._currents arr[self.ns] = arr[self.ns].filter(x => x !== el) } /** * Check if element or target is activated * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating * @param {Boolean} params.same Use also data-xt-group-same * @param {Boolean} params.running Running currents */ hasCurrent({ el, same = false, running = false } = {}) { const self = this const options = self.options // fix groupElements and targets const elements = options.groupElements || self.targets.includes(el) ? self.getElements({ el, same }) : [el] // hasCurrent const arr = running ? Xt._running : Xt._currents return arr[self.ns].filter(x => elements.includes(x)).length } /** * check element on * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el To be checked * @return {Boolean} If elements can activate */ _checkOn({ el } = {}) { const self = this // check return !self.hasCurrent({ el }) } /** * check element off * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el To be checked * @return {Boolean} If elements can deactivate */ _checkOff({ el } = {}) { const self = this const options = self.options // skip if min >= currents if (options.min - self._getCurrents().length >= 0) { return false } // check return self.hasCurrent({ el }) } /** * check element on * @param {Object} params * @param {Object} params.obj Queue object to end * @return {Boolean} If elements can activate */ _checkOnRunning({ obj } = {}) { const self = this // running check to stop multiple activation/deactivation with delay const check = obj.elements.runningOn || !self.hasCurrent({ el: obj.elements.queueEls[0], running: true }) obj.elements.runningOn = check return check } /** * check element off running * @param {Object} params * @param {Object} params.obj Queue object to end * @return {Boolean} If elements can activate */ _checkOffRunning({ obj } = {}) { const self = this // running check to stop multiple activation/deactivation with delay const check = obj.elements.runningOff || self.hasCurrent({ el: obj.elements.queueEls[0], running: true }) obj.elements.runningOff = check return check } /** * set index * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el */ _setIndex({ el } = {}) { const self = this // set index const index = self.getIndex({ el }) self._oldIndex = self.index ?? index self.index = index } /** * get index * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el */ getIndex({ el } = {}) { const self = this // fix groupElements and targets el = self.getElements({ el })[0] // set index let index = null for (const [i, element] of self.getElementsGroups().entries()) { if (el === element) { index = i break } } return index } /** * set direction */ _setDirection() { const self = this // set direction if (self.index === null || self.index === self._oldIndex) { // initial direction and same index direction self.direction = 0 } else if (self._inverse !== null) { // forced value self.direction = self._inverse ? -1 : 1 } else { self.direction = self.index < self._oldIndex ? -1 : 1 } } /** * activate element * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be activated * @param {String} params.type Type of element * @param {Boolean} params.skipSame If skip activation classes and events */ _activate({ el, type, skipSame } = {}) { const self = this const options = self.options // activation if (!skipSame && options.classSkip !== true && !options.classSkip[type]) { // input el.checked = true // activation el.classList.add(...self._classes) el.classList.remove(...self._classesOut) // needs TWO raf or sequential off/on flickr (e.g. display) Xt.frameDouble({ el, func: () => { el.classList.add(...self._classesIn) el.classList.remove(...self._classesDone) }, }) // direction el.classList.remove(...self._classesBefore, ...self._classesAfter) if (self.direction < 0) { el.classList.add(...self._classesBefore) } else if (self.direction > 0) { el.classList.add(...self._classesAfter) } } } /** * activate element done * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated * @param {String} params.type Type of element * @param {Boolean} params.skipSame If skip activation classes and events */ _activateDone({ el, type, skipSame } = {}) { const self = this const options = self.options // activation if (!skipSame && options.classSkip !== true && !options.classSkip[type]) { // fix need to repeat inside frameDouble in case we cancel Xt.frameDouble({ el }) el.classList.add(...self._classesIn, ...self._classesDone) } } /** * activate hash * @param {Object} params * @param {Object} params.obj Queue object * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be activated * @param {String} type Type of element */ _activateHash({ obj, el, type } = {}) { const self = this const options = self.options // hash if (!Xt.dataStorage.get(self.container, `${self.ns}HashSkip`)) { if (self._hasHash && !self.initial) { // fix no data-xt-group-same const elMain = obj.elements.queueEls[0] if ( (type === 'elements' && self.getElements({ el: elMain }).includes(el)) || (type === 'targets' && self.getTargets({ el: elMain }).includes(el)) ) { const attr = el.getAttribute(options.hash) if (attr) { // raf prevents hash on chained activations (e.g: multiple hash on elements with same activation) Xt.frame({ el: window, ns: `${self.ns}Hash`, func: () => { Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, true) history.pushState({}, '', `#${encodeURIComponent(attr)}`) Xt.dataStorage.set(self.container, `${self.ns}HashSkip`, false) }, }) } } } } } /** * deactivate element * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated * @param {String} params.type Type of element * @param {Boolean} params.skipSame If skip activation classes and events */ _deactivate({ el, type, skipSame } = {}) { const self = this const options = self.options // activation if (!skipSame && options.classSkip !== true && !options.classSkip[type]) { // input el.checked = false // must be outside inside raf or page jumps (e.g. noqueue, done outside for toggle inverse) el.classList.remove(...self._classes) // needs TWO raf or sequential off/on flickr (e.g. backdrop megamenu) Xt.frameDouble({ el, func: () => { el.classList.remove(...self._classesIn, ...self._classesDone) el.classList.add(...self._classesOut) }, }) // direction el.classList.remove(...self._classesBefore, ...self._classesAfter) if (self.direction < 0) { el.classList.add(...self._classesBefore) } else if (self.direction > 0) { el.classList.add(...self._classesAfter) } } } /** * deactivate element done * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated * @param {String} params.type Type of element * @param {Boolean} params.skipSame If skip activation classes and events */ _deactivateDone({ el, type, skipSame } = {}) { const self = this const options = self.options // activation if (!skipSame && options.classSkip !== true && !options.classSkip[type]) { // fix need to repeat inside frameDouble in case we cancel Xt.frameDouble({ el }) el.classList.remove(...self._classesIn, ...self._classesOut) } } /** * deactivate hash * @param {Object} params * @param {Object} params.obj Queue object * @param {Node|HTMLElement|EventTarget|Window} params.el Elements to be deactivated * @param {String} params.type Type of element */ _deactivateHash({ obj, el, type } = {}) { const self = this const options = self.options // hash if (!Xt.dataStorage.get(self.container, `${self.ns}HashSkip`)) { if (options.hash && self._hasHash && !self.initial) { // fix no data-xt-group-same const elMain = obj.elements.queueEls[0] if ( (type === 'elements' && self.getElements({ el: elMain }).includes(el)) || (type === 'targets' && self.getTargets({ el: elMain }).includes(el)) ) { const attr = el.getAttribute(options.hash) if (attr && attr === location.hash.split('#')[1]) { // raf prevents hash on chained activations (e.g: multiple hash on elements with same activation) Xt.frame({ el: wind