UNPKG

xtendui

Version:

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

1,629 lines (1,575 loc) 110 kB
/*! * Xtend UI (https://xtendui.com/) * @copyright (c) 2017 - 2021 Riccardo Caroli * @license MIT (https://github.com/xtendui/xtendui/blob/beta/LICENSE.txt) */ import { Xt } from './xt' import RJSON from 'relaxed-json' import * as focusTrap from 'focus-trap' Xt.RJSON = RJSON Xt.focusTrap = focusTrap /** * Toggle */ class Toggle { /** * fields */ _optionsCustom _optionsDefault _optionsInitial _componentNs _mode _classes = [] _classesIn = [] _classesOut = [] _classesDone = [] _classesInitial = [] _classesBefore = [] _classesAfter = [] _initialCurrents = [] _destroyElements _containerElements _containerTargets _oldIndex _inverse _queueIn = [] _queueOut = [] _autoblock _disabledManual _hasHash _autorunning _observer _focusTrap componentName ns options initial disabled = false container elements = [] targets = [] index direction // slider _usedWidth _wrap _keepHeight _autoHeight _groups drag = {} pags /** * constructor * @param {Node|HTMLElement|EventTarget|Window} object Base node * @param {Object} optionsCustom User options * @constructor */ constructor(object, optionsCustom = {}) { const self = this self.container = object self._optionsCustom = optionsCustom self.componentName = self.constructor.componentName self._componentNs = self.componentName.replace('-', '.') // set self Xt._set({ name: self.componentName, el: self.container, self }) // init self._init() } // // 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._initAria() self._initEvents() 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] } } /** * 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) } } /** * 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 const todo = options.min - currents const start = 0 if (todo > 0) { // initial currents += todo } // todo if (todo > 0) { for (let i = start; i < todo; i++) { const el = self.elements[i] if (el) { // toggle event if present because of custom listeners if (options.on) { const event = options.on.split(' ')[0] el.dispatchEvent(new CustomEvent(event, { detail: { force: true } })) } else { self._eventOn({ el, force: true }) } } } } // currents if (save) { self._initialCurrents = self._getCurrents().slice(0) // copy array with slice(0) } // no currents if (currents === 0) { // init Xt.frame({ el: self.container, ns: `${self.ns}Init`, func: () => { // 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.classSkip !== true && !options.classSkip.elementsInner) { const elsInner = Xt.queryAll({ els: el, query: options.elementsInner }) for (const elInner of elsInner) { elInner.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 if ((activated && currents < options.max) || obj.arr.includes(el)) { // 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.classSkip !== true && !options.classSkip.targetsInner) { const trsInner = Xt.queryAll({ els: tr, query: options.targetsInner }) for (const trInner of trsInner) { trInner.classList.remove( ...self._classes, ...self._classesIn, ...self._classesOut, ...self._classesDone, ...self._classesInitial, ...self._classesBefore, ...self._classesAfter ) } } } } // activate if (activated && currents < options.max) { // initial currents++ // toggle event if present because of custom listeners if (options.on) { const event = options.on.split(' ')[0] el.dispatchEvent(new CustomEvent(event, { detail: { force: true } })) } else { self._eventOn({ el, force: true }) } } } // return return currents } /** * init aria */ _initAria() { const self = this const options = self.options // role self._initAriaRole() // aria if (options.aria) { if (self.targets.length) { // targets for (const tr of self.targets) { // id if (options.aria === true || options.aria.labelledby || options.aria.controls) { const id = tr.getAttribute('id') if (!id) { tr.setAttribute('id', Xt.uniqueId()) } } } // elements for (const el of self.elements) { // selected if (options.aria === true || options.aria.activation) { el.setAttribute('aria-selected', 'false') } // id if (options.aria === true || options.aria.labelledby || options.aria.controls) { const id = el.getAttribute('id') if (!id) { el.setAttribute('id', Xt.uniqueId()) } } // controls if (options.aria === true || options.aria.controls) { const trs = self.getTargets({ el }) let str = ' ' str += '' for (const tr of trs) { str += ` ${tr.getAttribute('id')}` } el.setAttribute('aria-controls', str.trim()) } } // targets for (const tr of self.targets) { // expanded if (options.aria === true || options.aria.activation) { const role = tr.getAttribute('role') if (role === 'tabpanel' || role === 'listbox' || role === 'tooltip' || role === 'dialog') { tr.setAttribute('aria-expanded', 'false') } } // describedby if (options.aria === true || options.aria.describedby) { const els = self.getElements({ el: tr }) let str = ' ' str += '' for (const el of els) { str += ` ${el.getAttribute('id')}` } tr.setAttribute('aria-describedby', str.trim()) } // labelledby if (options.aria === true || options.aria.labelledby) { const els = self.getElements({ el: tr }) let str = ' ' str += '' for (const el of els) { str += ` ${el.getAttribute('id')}` } tr.setAttribute('aria-labelledby', str.trim()) } } } } } /** * init aria role */ _initAriaRole() { const self = this const options = self.options // aria if (options.aria) { if (self.targets.length) { // role if (options.aria === true || options.aria.role) { if (self._mode === 'multiple') { self.container.setAttribute('role', 'tablist') if (options.max > 1) { self.container.setAttribute('aria-multiselectable', 'true') } for (const el of self.elements) { el.setAttribute('role', 'tab') } for (const tr of self.targets) { tr.setAttribute('role', 'tabpanel') } } } } } } /** * 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 onHandler = Xt.dataStorage.put(el, `${options.on}/on/${self.ns}`, self._eventOnHandler.bind(self, { el })) const events = [...options.on.split(' ')] for (const event of events) { el.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 offHandler = Xt.dataStorage.put( el, `${options.off}/off/${self.ns}`, self._eventOffHandler.bind(self, { el }) ) const events = [...options.off.split(' ')] for (const event of events) { // fix same event for on and off same namespace if (![...options.on.split(' ')].includes(event)) { el.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')) { // fix prevents click links on click until clicked two times const preventeventStartTouchHandler = Xt.dataStorage.put( el, `touchend/preventevent/${self.ns}`, self._eventPreventeventStartHandler.bind(self, { el }) ) el.addEventListener('touchend', preventeventStartTouchHandler) } if (events.includes('click')) { const preventeventStartMouseHandler = Xt.dataStorage.put( el, `mouseup/preventevent/${self.ns}`, self._eventPreventeventStartHandler.bind(self, { el }) ) el.addEventListener('mouseup', preventeventStartMouseHandler) } } Xt.dataStorage.put(el, `active/preventevent/${self.ns}`, self.hasCurrent({ el })) } } // targets 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) } } } // keyboard if (options.keyboard && options.keyboard.selector) { const keyboards = options.keyboard.selector === 'object' ? [self.container] : self.container.querySelectorAll(options.keyboard.selector) self._destroyElements.push(...keyboards) for (const keyboard of keyboards) { keyboard.setAttribute('tabindex', '0') // focus const keyboardfocusHandler = Xt.dataStorage.put( keyboard, `focus/keyboard/${self.ns}`, self._eventKeyboardfocusHandler.bind(self) ) keyboard.addEventListener('focus', keyboardfocusHandler) // blur const keyboardblurHandler = Xt.dataStorage.put( keyboard, `blur/keyboard/${self.ns}`, self._eventKeyboardblurHandler.bind(self) ) keyboard.addEventListener('blur', keyboardblurHandler) } } // 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')).filter(x => !x.closest('.xt-clone')) self._destroyElements.push(...imgs) for (const img of imgs) { if (!Xt.dataStorage.get(img, `${self.ns}MedialoadedDone`)) { Xt.dataStorage.set(img, `${self.ns}MedialoadedDone`, true) // useCapture event propagation 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')).filter(x => !x.closest('.xt-clone')) 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 }) ) 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 })) // event link const preventeventHandler = Xt.dataStorage.put( el, `click/preventevent/${self.ns}`, self._eventPreventeventHandler.bind(self, { el }) ) el.addEventListener('click', preventeventHandler) // event reset 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}`) 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) } /** * keyboard focus handler */ _eventKeyboardfocusHandler() { const self = this // handler const keyboardHandler = Xt.dataStorage.put( document, `keyup/keyboard/${self.ns}`, self._eventKeyboardHandler.bind(self) ) document.addEventListener('keyup', keyboardHandler) } /** * keyboard blur handler */ _eventKeyboardblurHandler() { const self = this // handler const keyboardHandler = Xt.dataStorage.get(document, `keyup/keyboard/${self.ns}`) document.removeEventListener('keyup', keyboardHandler) } /** * keyboard handler * @param {Event} e */ _eventKeyboardHandler(e) { const self = this const options = self.options // key const code = e.keyCode ? e.keyCode : e.which let prev let next if (options.keyboard.vertical) { if (options.keyboard.inverse) { prev = 40 next = 38 } else { prev = 38 next = 40 } } else { if (options.keyboard.inverse) { prev = 39 next = 37 } else { prev = 37 next = 39 } } if (code === prev) { self.goToPrev({ amount: 1 }) } else if (code === next) { self.goToNext({ amount: 1 }) } } /** * 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 */ _eventMedialoadedHandler({ img, el, deferred = false } = {}) { const self = this const options = self.options // fix multiple calls Xt.dataStorage.set(img, `${self.ns}MedialoadedDone`, true) // mediaLoadedReinit if (options.mediaLoadedReinit && deferred) { Xt.eventDelay({ e: { detail: { delay: Xt.medialoadedDelay } }, el: self.container, ns: `${self.ns}MedialoadedReinit`, func: () => { // mediaLoaded self._eventMediaLoadedReinit() }, }) } // 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(self.ns, 'xtNamespace') 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 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, ...self._classesDone) // needs TWO raf or sequential off/on flickr (e.g. display) Xt.frameDouble({ el, func: () => { el.classList.add(...self._classesIn) }, }) // 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} param