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,211 lines (1,140 loc) • 33.6 kB
JavaScript
/*!
* Xtend UI (https://xtendui.com/)
* @copyright (c) 2017 - 2021 Riccardo Caroli
* @license MIT (https://github.com/xtendui/xtendui/blob/beta/LICENSE.txt)
*/
import DOMPurify from 'dompurify'
//
// constructor
//
export const Xt = {}
if (typeof window !== 'undefined') {
//
// global
//
if (window.XtSetGlobal) {
global[typeof window.XtSetGlobal === 'string' ? window.XtSetGlobal : 'Xt'] = Xt
}
//
// vars
//
Xt._running = {}
Xt._currents = {} // Xt currents based on namespace (so shared between Xt objects)
Xt.options = {}
Xt._mountArr = []
Xt._unmountArr = []
Xt._focusTrapArr = []
Xt.scrollDelay = false
Xt.resizeDelay = 200
Xt.medialoadedDelay = false
Xt.durationTimescale = 1
Xt.autoTimescale = 1
Xt.scrolltoHashforce = null
Xt.formScrollWindowFactor = 0.2
//
// initialization
//
/**
* ready
* @param {Object} params
* @param {Function} params.func Function to execute
* @param {String} params.state States separated by space, can be 'loading' 'interactive' 'complete'
* @param {Function} params.raf Use requestAnimationFrame if the state is instantly matched
*/
Xt.ready = ({ func, state = 'interactive complete', raf = false } = {}) => {
const states = [...state.split(' ')]
if (states.includes(document.readyState)) {
if (raf) {
// raf because we need all functions defined after mount (e.g. all html demos with mount)
requestAnimationFrame(() => {
func()
})
} else {
func()
}
} else {
const interactive = () => {
if (states.includes(document.readyState)) {
func()
// needs to be removed or it will call multiple times
document.removeEventListener('readystatechange', interactive)
}
}
document.addEventListener('readystatechange', interactive)
}
}
//
// mutationObserver
//
/**
* init
*/
Xt._mutationObserver = new MutationObserver(mutationsList => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
// added
for (const added of mutation.addedNodes) {
if (added.nodeType === 1) {
Xt._mountCheck({ added })
}
}
// removed
for (const removed of mutation.removedNodes) {
if (removed.nodeType === 1) {
Xt._unmountCheck({ removed })
}
}
}
}
})
Xt.ready({
func: () => {
Xt._mutationObserver.disconnect()
Xt._mutationObserver.observe(document.documentElement, {
characterData: false,
attributes: false,
childList: true,
subtree: true,
})
},
})
/**
* refresh
*/
Xt.refresh = () => {
Xt._mountCheck()
}
/**
* mount
* @param {Object} obj
*/
Xt.mount = obj => {
Xt._mountArr.push(obj)
Xt.ready({
raf: obj.raf,
func: () => {
Xt._mountCheck({ obj })
},
})
}
/**
* unmount
* @param {Object} obj
*/
Xt.unmount = obj => {
Xt._unmountArr.push(obj)
}
/**
* mountCheck
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.added
* @param {Object} params.obj
*/
Xt._mountCheck = ({ added = document.documentElement, obj } = {}) => {
// fix multiple mount
// we do not mount if not in document, it happends for example when you mount ScrollTrigger after overlay menu
if (!added.closest('html')) {
return
}
const arr = obj ? [obj] : Xt._mountArr
for (const obj of arr) {
// check
const refs = []
if (added.matches(obj.matches)) {
refs.push(added)
}
for (const ref of added.querySelectorAll(obj.matches)) {
refs.push(ref)
}
// call
if (refs.length) {
for (const [index, ref] of refs.entries()) {
// root
if (obj.root && !obj.root.contains(ref)) {
continue
}
// ignore
const ignoreStr = obj.ignoreMount ?? '.xt-ignore'
if (ignoreStr && ref.closest(ignoreStr)) {
continue
}
// fix multiple mount
// we don't remount nodes not unmounted
obj.done = obj.done ? obj.done : []
if (obj.done.includes(ref)) {
return
}
obj.done.push(ref)
// call
const call = obj.mount({ ref, obj, index })
// destroy
if (call) {
Xt.unmount({
ref,
root: obj.root,
ignoreUnmount: obj.ignoreUnmount,
unmount: call,
unmountRemove: function () {
// fix multiple mount
obj.done = obj.done.filter(x => x !== ref)
// unmount remove
Xt._unmountArr = Xt._unmountArr.filter(x => {
return x !== this // this is unmount object using function
})
},
})
}
}
}
}
}
/**
* unmountCheck
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.removed
*/
Xt._unmountCheck = ({ removed = document.documentElement } = {}) => {
// fix multiple mount
// we do not mount if in document, it happends for example when you move nodes
if (removed.closest('html')) {
return
}
for (const obj of Xt._unmountArr) {
// check
if (removed === obj.ref || removed.contains(obj.ref)) {
// root
if (obj.root && !obj.root.contains(obj.ref)) {
continue
}
// ignore
const ignoreStr = obj.ignoreUnmount ?? '.xt-ignore'
if (ignoreStr && obj.ref.closest(ignoreStr)) {
continue
}
// call
obj.unmount({ obj })
obj.unmountRemove()
}
}
}
//
// component
//
/**
* set component
* @param {Object} params
* @param {String} params.name Component name
* @param {Node|HTMLElement|EventTarget|Window} params.el Component's element
* @param {Object} params.self Component' self
*/
Xt._set = ({ name, el, self } = {}) => {
Xt.dataStorage.set(el, name, self)
}
/**
* get component
* @param {Object} params
* @param {String} params.name Component name
* @param {Node|HTMLElement|EventTarget|Window} params.el Component's element
* @return {Object}
*/
Xt.get = ({ name, el } = {}) => {
return Xt.dataStorage.get(el, name)
}
/**
* remove component
* @param {Object} params
* @param {String} params.name Component name
* @param {Node|HTMLElement|EventTarget|Window} params.el Component's element
* @return {Object}
*/
Xt._remove = ({ name, el } = {}) => {
return Xt.dataStorage.remove(el, name)
}
//
// methods
//
/**
* init matches
* @param {Object} params
* @param {Object} params.self Self object
* @param {Object} params.optionsInitial Initial options
*/
Xt._initMatches = ({ self, optionsInitial } = {}) => {
const options = self.options
// only on initialization not on reinit because the mql reinit
if (self.initial === undefined) {
// remove matches
if (self.matches) {
Xt._removeMatches({ self, optionsInitial })
}
// matches
if (options.matches) {
self.matches = []
const mqs = Object.entries(options.matches)
if (mqs.length) {
for (const [key, value] of mqs) {
// matches
const mql = matchMedia(key)
self.matches.push({ mql, value })
Xt._eventMatches({ self, mql, value, skipReinit: true, optionsInitial })
mql.addEventListener('change', Xt._eventMatches.bind(null, { self, mql, value, optionsInitial }))
}
}
}
}
}
/**
* match
* @param {Object} params
* @param {Object} params.self Self object
* @param {Object} params.mql Match media query list
* @param {Object} params.value Match media value
* @param {Boolean} params.skipReinit Skip reinit
*/
Xt._eventMatches = ({ self, mql, value, skipReinit = false, optionsInitial } = {}) => {
// fix NEEDED for chrome not removing mql event listener
if (!self.container.closest('html')) {
return
}
// replace options
if (mql.matches) {
// set options value
self.options = Xt.merge([self.options, value])
} else {
// set options value from initial
self.options = Xt.mergeReset({ start: self.options, reset: optionsInitial, check: value })
}
// reinit one time only with raf
if (!skipReinit) {
// reinit
Xt.frame({
el: self.container,
ns: `${self.ns}MatchFrame`,
func: () => {
Xt._eventReinit({ self })
},
})
}
}
/**
* removeMatches
* @param {Object} params
* @param {Object} params.self Self object
*/
Xt._removeMatches = ({ self, optionsInitial } = {}) => {
// remove matches
if (self.matches?.length) {
for (const obj of self.matches) {
// matches
const mql = obj.mql
const value = obj.value
mql.removeEventListener('change', Xt._eventMatches.bind(null, { self, mql, value, optionsInitial }))
}
}
}
/**
* reinit
* @param {Object} params
* @param {Object} params.self Self object
* @param {Event} e
*/
Xt._eventReinit = ({ self } = {}, e) => {
// triggering e.detail.container
if (!e?.detail?.container || e?.detail?.container.contains(self.container)) {
Xt.eventDelay({
e,
el: self.container,
ns: `${self.ns}Reinit`,
func: () => {
// handler
self.reinit()
},
})
}
}
//
// dataStorage
// map storage for HTML elements
//
Xt.dataStorage = {
/**
* properties
*/
_storage: new Map(),
/**
* set key/obj pair on element's map
* @param {Node|HTMLElement|EventTarget|Window} el
* @param {String} key
* @param {*} obj
* @return {*}
*/
set: (el, key, obj) => {
// new map if not already there
if (!Xt.dataStorage._storage.has(el)) {
Xt.dataStorage._storage.set(el, new Map())
}
// set
const getEl = Xt.dataStorage._storage.get(el)
getEl.set(key, obj)
// return
return getEl.get(key)
},
/**
* put key/obj pair on element's map, return old if exist already
* @param {Node|HTMLElement|EventTarget|Window} el
* @param {String} key
* @param {*} obj
* @return {*}
*/
put: (el, key, obj) => {
// new map if not already there
if (!Xt.dataStorage._storage.has(el)) {
Xt.dataStorage._storage.set(el, new Map())
}
// return if already set
const getEl = Xt.dataStorage._storage.get(el)
const getKey = getEl.get(key)
if (getKey) {
return getKey
}
// set
getEl.set(key, obj)
// return
return getEl.get(key)
},
/**
* get obj from key on element's map
* @param {Node|HTMLElement|EventTarget|Window} el
* @param {String} key
* @return {*}
*/
get: (el, key) => {
const getEl = Xt.dataStorage._storage.get(el)
// null if empty
if (!getEl) {
return null
}
// return
return getEl.get(key)
},
/**
* get all obj/key on element's map
* @param {Node|HTMLElement|EventTarget|Window} el
* @return {*}
*/
getAll: el => {
const getEl = Xt.dataStorage._storage.get(el)
// null if empty
if (!getEl) {
return null
}
// return
return getEl
},
/**
* has key on element's map
* @param {Node|HTMLElement|EventTarget|Window} el
* @param {String} key
* @return {Boolean}
*/
has: (el, key) => {
// return
return Xt.dataStorage._storage.get(el).has(key)
},
/**
* remove element's map key
* @param {Node|HTMLElement|EventTarget|Window} el
* @param {String} key
* @return {Boolean}
*/
remove: (el, key) => {
const getEl = Xt.dataStorage._storage.get(el)
// null if empty
if (!getEl) {
return null
}
// remove
const ret = getEl.delete(key)
// remove storage if empty
if (getEl.size === false) {
Xt.dataStorage._storage.delete(el)
}
// return
return ret
},
}
//
// classBody
// util to remember classBody state
//
Xt._classBody = {
/**
* properties
*/
currents: [],
/**
* add classBody currents
* @param {Object} obj Object
*/
add: obj => {
Xt._classBody.currents.push(obj)
},
/**
* remove classBody currents
* @param {Object} obj Object
*/
remove: obj => {
Xt._classBody.currents = Xt._classBody.currents.filter(x => x.c !== obj.c || x.ns !== obj.ns)
},
/**
* get classBody currents
* @param {Object} obj Object
* @return {Array} Currents
*/
get: obj => {
return Xt._classBody.currents.filter(x => x.c === obj.c)
},
}
//
// util
//
/**
* friction
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element
* @param {Object} params.obj Object with x and y values
* @param {Boolean} params.transform Use transforms instead of position
*/
Xt._friction = ({ el, obj, transform = true } = {}) => {
Xt.frame({
el,
ns: `xtFrictionFrame`,
})
Xt.frame({
el,
ns: `xtFrictionInitFrame`,
func: () => {
// fix loop when not visible
if (Xt.visible({ el })) {
let xCurrent
let yCurrent
if (transform) {
const translate = Xt.getTranslate({ el })
xCurrent = translate[0]
yCurrent = translate[1]
} else {
const rect = el.getBoundingClientRect()
xCurrent = rect.left
yCurrent = rect.top
}
let xDist = obj.x - xCurrent
let yDist = obj.y - yCurrent
// momentum
const fncFriction = obj.friction
// set
if (fncFriction && Xt.dataStorage.get(el, 'xtFrictionX') && Xt.durationTimescale !== 1000) {
// friction
xCurrent += fncFriction({ delta: Math.abs(xDist) }) * Math.sign(xDist)
yCurrent += fncFriction({ delta: Math.abs(yDist) }) * Math.sign(yDist)
if (transform) {
el.style.transform = `translateX(${xCurrent}px) translateY(${yCurrent}px)`
} else {
el.style.left = `${xCurrent}px`
el.style.top = `${yCurrent}px`
}
} else {
// instant
xCurrent = obj.x
yCurrent = obj.y
// set
if (transform) {
el.style.transform = `translateX(${xCurrent}px) translateY(${yCurrent}px)`
} else {
el.style.top = `${yCurrent}px`
el.style.left = `${xCurrent}px`
}
}
// next interaction friction
Xt.dataStorage.set(el, 'xtFrictionX', xCurrent)
Xt.dataStorage.set(el, 'xtFrictionY', yCurrent)
// loop
if (fncFriction && Xt.durationTimescale !== 1000) {
const frictionLimit = obj.frictionLimit ? obj.frictionLimit : 1.5
xDist = obj.x - xCurrent
yDist = obj.y - yCurrent
Xt.frame({
el,
ns: `xtFrictionFrame`,
func: () => {
if (Math.abs(xDist) >= frictionLimit || Math.abs(yDist) >= frictionLimit) {
// continue friction
Xt._friction({ el, obj, transform })
} else {
// next interaction instant
Xt.dataStorage.remove(el, 'xtFrictionX')
Xt.dataStorage.remove(el, 'xtFrictionY')
}
},
})
}
}
},
})
}
/**
* Return translate values https://gist.github.com/aderaaij/a6b666bf756b2db1596b366da921755d
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element to check target
* @return {Array} Values [x, y]
*/
Xt.getTranslate = ({ el } = {}) => {
const transArr = []
const style = getComputedStyle(el)
const transform = style.transform
let mat = transform.match(/^matrix3d\((.+)\)$/)
if (mat) {
transArr.push(parseFloat(mat[1].split(', ')[13]))
} else {
mat = transform.match(/^matrix\((.+)\)$/)
mat ? transArr.push(parseFloat(mat[1].split(', ')[4])) : transArr.push(0)
mat ? transArr.push(parseFloat(mat[1].split(', ')[5])) : transArr.push(0)
}
return transArr
}
/**
* Contains for multiple elements
* @param {Object} params
* @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.els Elements to check if contains
* @param {Node|HTMLElement|EventTarget|Window} params.tr Element to check if contained
* @return {Boolean}
*/
Xt.contains = ({ els, tr } = {}) => {
if (els instanceof HTMLElement) {
return els.contains(tr)
}
for (const el of els) {
if (el.contains(tr)) {
return true
}
}
return false
}
/**
* Get unique id
* @return {String} Unique id
*/
Xt.uniqueId = () => {
Xt.uid = Xt.uid !== undefined ? Xt.uid : 0
return `xt-${Xt.uid++}`
}
/**
* Merge deep array of objects
* @param {Array} arr Array of objects to merge
* @return {Object} Merged object
*/
Xt.merge = arr => {
const final = {}
for (const obj of arr) {
if (obj) {
for (const [key, value] of Object.entries(obj)) {
if (Array.isArray(value)) {
// if array merge arrays (e.g. options.popperjs modifiers)
final[key] = final[key] ? final[key] : []
final[key].push(...value)
} else if (
value !== null &&
typeof value === 'object' &&
!value.nodeName && // not HTML element
value !== window // not window
) {
// if object deep merge
final[key] = Xt.merge([final[key], value])
} else {
final[key] = value
}
}
}
}
return final
}
/**
* Merge deep reset object only when equals to check
* @param {Object} params
* @param {Object} params.start object Start object
* @param {Object} params.reset object Reset object
* @param {Object} params.check object Check with start object to reset with reset object
* @return {Object} Merged object
*/
Xt.mergeReset = ({ start, reset, check } = {}) => {
const final = start
for (const [key, value] of Object.entries(check)) {
if (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) && // not array
!value.nodeName && // not HTML element
value !== window // not window
) {
final[key] = Xt.mergeReset({ start: start[key], reset: reset[key], check: check[key] })
} else {
if (start[key] === check[key]) {
final[key] = reset[key]
}
}
}
return final
}
/**
* Purify html string
* @param {String} str String
* @return {String} Purified string
*/
Xt.sanitize = str => {
return DOMPurify.sanitize(str)
}
/**
* Create HTML element from html string
* @param {Object} params
* @param {Boolean} params.sanitize Sanitize
* @param {String} params.str String (only 1 root html tag)
* @return {Node} HTML elements
*/
Xt.node = ({ sanitize = true, str }) => {
const template = document.createElement('template')
template.innerHTML = sanitize ? Xt.sanitize(str.trim()) : str.trim()
return template.content.firstChild
}
/**
* Create HTML elements from html string
* @param {Boolean} params.sanitize Sanitize
* @param {String} params.str Html String
* @return {Node} HTML elements
*/
Xt.nodes = ({ sanitize = true, str }) => {
const template = document.createElement('template')
template.innerHTML = sanitize ? Xt.sanitize(str.trim()) : str.trim()
return template.content.childNodes
}
/**
* Add script to document
* @param {Object} params
* @param {String} params.url
* @param {Function} params.callback
* @param {Boolean} params.defer
* @param {Boolean} params.async
*/
Xt.script = ({ url, callback, defer = true, async = true } = {}) => {
if (!document.querySelector(`script[src="${url}"]`)) {
const script = document.createElement('script')
if (callback) {
script.onload = callback
}
script.type = 'text/javascript'
script.src = url
script.defer = defer
script.async = async
document.body.append(script)
}
}
/**
* requestAnimationFrame
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {String} params.ns Namespace
* @param {Function} params.func Function to execute after transition or animation
*/
Xt.frame = ({ el, ns = '', func } = {}) => {
cancelAnimationFrame(Xt.dataStorage.get(el, `${ns}Frame`))
if (func) {
// needs one raf
Xt.dataStorage.set(
el,
`${ns}Frame`,
requestAnimationFrame(() => {
func()
})
)
}
}
/**
* double requestAnimationFrame
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {String} params.ns Namespace
* @param {Function} params.func Function to execute after transition or animation
*/
Xt.frameDouble = ({ el, ns = '', func } = {}) => {
cancelAnimationFrame(Xt.dataStorage.get(el, `${ns}FrameDouble`))
if (func) {
// needs two raf or sometimes classes doesn't animate properly
Xt.dataStorage.set(
el,
`${ns}FrameDouble`,
requestAnimationFrame(() => {
Xt.dataStorage.set(
el,
`${ns}FrameDouble`,
requestAnimationFrame(() => {
func()
})
)
})
)
}
}
/**
* animation on classes
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {String} params.ns Namespace
* @param {Number} params.duration Duration
* @param {Boolean} params.raf Use requestAnimationFrame
* @param {Boolean} params.initial Instant animations with initial
* @param {Function} params.callback
*/
Xt.on = ({ el, ns = '', duration, raf = true, initial = false, callback } = {}) => {
Xt.animTimeout({ el, ns })
el.classList.add('on')
el.classList.remove('out', 'done')
const func = () => {
el.classList.add('in')
Xt.animTimeout({
el,
ns,
duration,
actionCurrent: 'In',
func: () => {
el.classList.add('done')
if (callback) {
callback()
}
},
})
}
if (raf) {
// needs TWO raf or sequential off/on flickr (e.g. display)
Xt.frameDouble({ el, ns, func })
} else {
// fix need to repeat inside frameDouble in case we cancel
Xt.frameDouble({ el, ns })
func()
}
// initial
if (initial) {
el.classList.add('initial')
}
Xt.frameDouble({
el,
ns: `${ns}Initial`,
func: () => {
if (initial) {
el.classList.remove('initial')
}
},
})
}
/**
* animation off classes
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {String} params.ns Namespace
* @param {Number} params.duration Duration
* @param {Boolean} params.raf Use requestAnimationFrame
* @param {Boolean} params.initial Instant animations with initial
* @param {Function} params.callback
*/
Xt.off = ({ el, ns = '', duration, raf = true, initial = false, callback } = {}) => {
Xt.animTimeout({ el, ns })
// must be outside inside raf or page jumps (e.g. noqueue)
el.classList.remove('on', 'done')
const func = () => {
el.classList.remove('in')
el.classList.add('out')
Xt.animTimeout({
el,
ns,
duration,
actionCurrent: 'Out',
func: () => {
el.classList.remove('out')
if (callback) {
callback()
}
},
})
}
if (raf) {
// needs TWO raf or sequential off/on flickr (e.g. backdrop megamenu)
Xt.frameDouble({ el, ns, func })
} else {
// fix need to repeat inside frameDouble in case we cancel
Xt.frameDouble({ el, ns })
func()
}
// initial
if (initial) {
el.classList.add('initial')
}
Xt.frameDouble({
el,
ns: `${ns}Initial`,
func: () => {
if (initial) {
el.classList.remove('initial')
}
},
})
}
/**
* execute function after transition or animation
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {String} params.ns Namespace
* @param {Number} params.duration Duration
* @param {String} params.actionCurrent Current action
* @param {Function} params.func Function to execute after transition or animation
*/
Xt.animTimeout = ({ el, ns = '', duration, actionCurrent, func } = {}) => {
clearTimeout(Xt.dataStorage.get(el, `${ns}AnimTimeout`))
if (func) {
duration = Xt.animTime({ el, duration, actionCurrent }) ?? 0
if (!duration) {
func()
} else {
Xt.dataStorage.set(el, `${ns}AnimTimeout`, setTimeout(func, duration))
}
}
}
/**
* get transition or animation time
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {Number} params.duration Duration
* @param {String} params.actionCurrent Current action
*/
Xt.animTime = ({ el, duration, actionCurrent } = {}) => {
const custom =
(actionCurrent && el.getAttribute(`data-xt-duration-${actionCurrent}`)) || el.getAttribute('data-xt-duration')
if (custom) {
// if not number return the string
return isNaN(parseFloat(custom)) ? custom : parseFloat(custom) / Xt.durationTimescale
} else if (typeof duration === 'function') {
return duration
} else if (duration || duration === 0) {
return duration / Xt.durationTimescale
}
}
/**
* get delay time
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {Number} params.duration Duration
* @param {String} params.actionCurrent Current action
*/
Xt.delayTime = ({ el, duration, actionCurrent } = {}) => {
const custom =
(actionCurrent && el.getAttribute(`data-xt-delay-${actionCurrent}`)) || el.getAttribute('data-xt-delay')
if (custom) {
// if not number return the string
return isNaN(parseFloat(custom)) ? custom : parseFloat(custom) / Xt.durationTimescale
} else if (typeof duration === 'function') {
return duration
} else if (duration || duration === 0) {
return duration / Xt.durationTimescale
}
}
/**
* query array of elements or element
* @param {Object} params
* @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.els Element to search from
* @param {String} params.query Query for querySelectorAll
* @return {Array}
*/
Xt.queryAll = ({ els, query } = {}) => {
// not when no query or empty array
if (!query || els.length === 0) {
return []
}
if (!els.length) {
// search element
return Array.from(els.querySelectorAll(query))
} else {
// search array
const arr = []
for (const el of els) {
arr.push(...el.querySelectorAll(query))
}
return arr
}
}
/**
* check element visibility
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @return {Boolean}
*/
Xt.visible = ({ el } = {}) => {
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length)
}
/**
* Fix resize event multiple calls and adds delay on resize and scroll events
* @param {Object} params
* @param {Event|Object} params.e Event
* @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
* @param {String} params.ns Namespace
* @param {Number} params.duration Duration
* @param {Function} params.func Function to execute after transition or animation
*/
Xt.eventDelay = ({ e, el, ns = '', duration, func } = {}) => {
Xt.frame({
el,
ns: `${ns}EventDelayFrame`,
})
clearTimeout(Xt.dataStorage.get(window, `eventDelaySaveTimeout`))
clearTimeout(Xt.dataStorage.get(el, `${ns}eventDelayTimeout`))
if (func) {
if (e) {
let delay = e?.detail?.delay
delay = duration ?? delay ? delay : Xt[`${e.type}Delay`]
if (e.type === 'resize') {
// we check also if outerWidth changes because on android doesn't change with ScrollTriggers
// we check also innerWidth for resize desktop (e.g. inspect and resize)
const w = window.innerWidth + window.outerWidth
const h = window.innerHeight + window.outerHeight
// multiple calls check
if (
!e?.detail?.force && // not when setting delay on event
Xt.dataStorage.get(window, 'xtEventDelayWidth') === w && // when width changes
(matchMedia('(hover: none)').matches || Xt.dataStorage.get(window, 'xtEventDelayHeight') === h) // when height changes not touch
) {
// only width no height because it changes on scroll on mobile
return
}
// save
Xt.dataStorage.set(
window,
`eventDelaySaveTimeout`,
setTimeout(() => {
Xt.dataStorage.set(window, 'xtEventDelayWidth', w)
Xt.dataStorage.set(window, 'xtEventDelayHeight', h)
}, Xt[`${e.type}Delay`])
)
}
// delay
if (!delay) {
Xt.frame({
el,
ns: `${ns}EventDelayFrame`,
func: () => {
func(e)
},
})
} else {
Xt.dataStorage.set(
el,
`${ns}eventDelayTimeout`,
setTimeout(() => {
func(e)
}, delay)
)
}
} else {
Xt.frame({
el,
ns: `${ns}EventDelayFrame`,
func: () => {
func(e)
},
})
}
}
}
Xt.dataStorage.set(document.documentElement, 'xtEventDelayWidth', window.innerWidth)
Xt.dataStorage.set(document.documentElement, 'xtEventDelayHeight', window.innerHeight)
/**
* Set scrollbar width of document
*/
Xt._setScrollbarWidth = () => {
if (Xt.scrollbarWidth === undefined) {
const scrollbarWidthHandler = Xt.dataStorage.put(window, 'resize/scrollbar', Xt._setScrollbarWidth)
removeEventListener('resize', scrollbarWidthHandler)
addEventListener('resize', scrollbarWidthHandler)
}
// add outer
const outer = document.createElement('div')
outer.style.visibility = 'hidden'
outer.style.width = '100%'
outer.style.msOverflowStyle = 'scrollbar' // needed for WinJS apps
outer.classList.add('xt-ignore', 'xt-overflow-main')
document.body.append(outer)
// force scrollbars
outer.style.overflow = 'scroll'
// add inner
const inner = document.createElement('div')
inner.style.width = '100%'
inner.classList.add('xt-ignore')
outer.append(inner)
// return
const widthNoScroll = outer.offsetWidth
const widthWithScroll = inner.offsetWidth
Xt.scrollbarWidth = widthNoScroll - widthWithScroll
document.documentElement.style.setProperty('--scrollbar-width', `${Xt.scrollbarWidth}px`)
// remove
outer.remove()
}
Xt.ready({
func: () => {
Xt._setScrollbarWidth()
},
})
/**
* Xt._innerHeightSet and --vh
*/
Xt._innerHeightSet = () => {
Xt.innerHeight = window.innerHeight
document.documentElement.style.setProperty('--vh', `${Xt.innerHeight * 0.01}px`)
}
addEventListener('resize', e => {
Xt.eventDelay({
e,
el: document.documentElement,
ns: 'WindowHeightResize',
duration: 0,
func: () => {
Xt._innerHeightSet()
},
})
})
Xt.ready({
func: () => {
Xt._innerHeightSet()
},
})
//
// plugins
//
/**
* scrolltriggerRerfreshFix fixes refresh on touch screen not on vertical resize
* @param {Object} params
* @param {Function} params.ScrollTrigger
*/
Xt.scrolltriggerRerfreshFix = ({ ScrollTrigger } = {}) => {
// removed resize we trigger it manually
ScrollTrigger.config({
autoRefreshEvents: 'visibilitychange,DOMContentLoaded,load',
})
// window resize
const resize = e => {
Xt.eventDelay({
e,
ns: 'xtScrolltriggerRerfreshFix',
func: () => {
ScrollTrigger.refresh()
},
})
}
removeEventListener('resize', resize)
addEventListener('resize', resize)
}
//
}