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
JavaScript
/*!
* 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