UNPKG

32.2 kBJavaScriptView Raw
1/*!
2 * Xtend UI (https://xtendui.com/)
3 * @copyright (c) 2017-2022 Riccardo Caroli
4 * @license MIT (https://github.com/xtendui/xtendui/blob/master/LICENSE.txt)
5 */
6
7import DOMPurify from 'dompurify'
8
9//
10// constructor
11//
12
13export const Xt = {}
14Xt.DOMPurify = DOMPurify
15
16if (typeof window !== 'undefined') {
17 //
18 // global
19 //
20
21 if (window.XtSetGlobal) {
22 global[typeof window.XtSetGlobal === 'string' ? window.XtSetGlobal : 'Xt'] = Xt
23 }
24
25 //
26 // vars
27 //
28
29 Xt._running = {}
30 Xt._currents = {} // Xt currents based on namespace (so shared between Xt objects)
31 Xt.options = {}
32 Xt._mountArr = []
33 Xt._unmountArr = []
34 Xt.resizeSkip = () => matchMedia('(hover: none), (pointer: coarse)').matches
35 Xt.resizeDelay = 200
36 Xt.medialoadedDelay = false
37 Xt.durationTimescale = 1
38 Xt.autoTimescale = 1
39 Xt.scrolltoHashforce = null
40 Xt.formScrollWindowFactor = 0.2
41
42 //
43 // initialization
44 //
45
46 /**
47 * ready
48 * @param {Object} params
49 * @param {Function} params.func Function to execute
50 * @param {String} params.state States separated by space, can be 'loading' 'interactive' 'complete'
51 * @param {Function} params.raf Use requestAnimationFrame if the state is instantly matched
52 */
53 Xt.ready = ({ func, state = 'interactive complete', raf = false } = {}) => {
54 const states = [...state.split(' ')]
55 if (states.includes(document.readyState)) {
56 if (raf) {
57 // raf because we need all functions defined after mount (e.g. all html demos with mount)
58 requestAnimationFrame(() => {
59 func()
60 })
61 } else {
62 func()
63 }
64 } else {
65 const interactive = () => {
66 if (states.includes(document.readyState)) {
67 func()
68 // needs to be removed or it will call multiple times
69 document.removeEventListener('readystatechange', interactive)
70 }
71 }
72 document.addEventListener('readystatechange', interactive)
73 }
74 }
75
76 //
77 // mutationObserver
78 //
79
80 /**
81 * init
82 */
83 Xt._mutationObserver = new MutationObserver(mutationsList => {
84 for (const mutation of mutationsList) {
85 if (mutation.type === 'childList') {
86 // added
87 for (const added of mutation.addedNodes) {
88 if (added.nodeType === 1) {
89 Xt._mountCheck({ added })
90 }
91 }
92 // removed
93 for (const removed of mutation.removedNodes) {
94 if (removed.nodeType === 1) {
95 Xt._unmountCheck({ removed })
96 }
97 }
98 }
99 }
100 })
101
102 Xt.ready({
103 func: () => {
104 Xt._mutationObserver.disconnect()
105 Xt._mutationObserver.observe(document.documentElement, {
106 characterData: false,
107 attributes: false,
108 childList: true,
109 subtree: true,
110 })
111 },
112 })
113
114 /**
115 * refresh
116 */
117 Xt.refresh = () => {
118 Xt._mountCheck()
119 }
120
121 /**
122 * mount
123 * @param {Object} obj
124 */
125 Xt.mount = obj => {
126 Xt._mountArr.push(obj)
127 Xt.ready({
128 raf: obj.raf,
129 func: () => {
130 Xt._mountCheck({ obj })
131 },
132 })
133 }
134
135 /**
136 * unmount
137 * @param {Object} obj
138 */
139 Xt.unmount = obj => {
140 Xt._unmountArr.push(obj)
141 }
142
143 /**
144 * mountCheck
145 * @param {Object} params
146 * @param {Node|HTMLElement|EventTarget|Window} params.added
147 * @param {Object} params.obj
148 */
149 Xt._mountCheck = ({ added = document.documentElement, obj } = {}) => {
150 // fix multiple mount
151 // we do not mount if not in document, it happends for example when you mount ScrollTrigger after overlay menu
152 if (!added.closest('html')) {
153 return
154 }
155 const arr = obj ? [obj] : Xt._mountArr
156 for (const obj of arr) {
157 // check
158 const refs = []
159 if (added.matches(obj.matches)) {
160 refs.push(added)
161 }
162 for (const ref of added.querySelectorAll(obj.matches)) {
163 refs.push(ref)
164 }
165 // call
166 if (refs.length) {
167 for (const [index, ref] of refs.entries()) {
168 // root
169 if (obj.root && !obj.root.contains(ref)) {
170 continue
171 }
172 // ignore
173 const ignoreStr = obj.ignoreMount ?? '.xt-ignore'
174 if (ignoreStr && ref.closest(ignoreStr)) {
175 continue
176 }
177 // fix multiple mount
178 // we don't remount nodes not unmounted
179 obj.done = obj.done ? obj.done : []
180 if (obj.done.includes(ref)) {
181 return
182 }
183 obj.done.push(ref)
184 // call
185 const call = obj.mount({ ref, obj, index })
186 // destroy
187 if (call) {
188 Xt.unmount({
189 ref,
190 root: obj.root,
191 ignoreUnmount: obj.ignoreUnmount,
192 unmount: call,
193 unmountRemove: function () {
194 // fix multiple mount
195 obj.done = obj.done.filter(x => x !== ref)
196 // unmount remove
197 Xt._unmountArr = Xt._unmountArr.filter(x => {
198 return x !== this // this is unmount object using function
199 })
200 },
201 })
202 }
203 }
204 }
205 }
206 }
207
208 /**
209 * unmountCheck
210 * @param {Object} params
211 * @param {Node|HTMLElement|EventTarget|Window} params.removed
212 */
213 Xt._unmountCheck = ({ removed = document.documentElement } = {}) => {
214 // fix multiple mount
215 // we do not mount if in document, it happends for example when you move nodes
216 if (removed.closest('html')) {
217 return
218 }
219 for (const obj of Xt._unmountArr) {
220 // check
221 if (removed === obj.ref || removed.contains(obj.ref)) {
222 // root
223 if (obj.root && !obj.root.contains(obj.ref)) {
224 continue
225 }
226 // ignore
227 const ignoreStr = obj.ignoreUnmount ?? '.xt-ignore'
228 if (ignoreStr && obj.ref.closest(ignoreStr)) {
229 continue
230 }
231 // call
232 obj.unmount({ obj })
233 obj.unmountRemove()
234 }
235 }
236 }
237
238 //
239 // component
240 //
241
242 /**
243 * set component
244 * @param {Object} params
245 * @param {String} params.name Component name
246 * @param {Node|HTMLElement|EventTarget|Window} params.el Component's element
247 * @param {Object} params.self Component' self
248 */
249 Xt._set = ({ name, el, self } = {}) => {
250 Xt.dataStorage.set(el, name, self)
251 }
252
253 /**
254 * get component
255 * @param {Object} params
256 * @param {String} params.name Component name
257 * @param {Node|HTMLElement|EventTarget|Window} params.el Component's element
258 * @return {Object}
259 */
260 Xt.get = ({ name, el } = {}) => {
261 return Xt.dataStorage.get(el, name)
262 }
263
264 /**
265 * remove component
266 * @param {Object} params
267 * @param {String} params.name Component name
268 * @param {Node|HTMLElement|EventTarget|Window} params.el Component's element
269 * @return {Object}
270 */
271 Xt._remove = ({ name, el } = {}) => {
272 return Xt.dataStorage.remove(el, name)
273 }
274
275 //
276 // methods
277 //
278
279 /**
280 * init matches
281 * @param {Object} params
282 * @param {Object} params.self Self object
283 * @param {Object} params.optionsInitial Initial options
284 */
285 Xt._initMatches = ({ self, optionsInitial } = {}) => {
286 const options = self.options
287 // only on initialization not on reinit because the mql reinit
288 if (self.initial === undefined) {
289 // remove matches
290 if (self.matches) {
291 Xt._removeMatches({ self, optionsInitial })
292 }
293 // matches
294 if (options.matches) {
295 self.matches = []
296 const mqs = Object.entries(options.matches)
297 if (mqs.length) {
298 for (const [key, value] of mqs) {
299 // matches
300 const mql = matchMedia(key)
301 self.matches.push({ mql, value })
302 Xt._eventMatches({ self, mql, value, skipReinit: true, optionsInitial })
303 if (mql.addEventListener) {
304 mql.addEventListener('change', Xt._eventMatches.bind(null, { self, mql, value, optionsInitial }))
305 } else {
306 mql.addListener(Xt._eventMatches.bind(null, { self, mql, value, optionsInitial }))
307 }
308 }
309 }
310 }
311 }
312 }
313
314 /**
315 * match
316 * @param {Object} params
317 * @param {Object} params.self Self object
318 * @param {Object} params.mql Match media query list
319 * @param {Object} params.value Match media value
320 * @param {Boolean} params.skipReinit Skip reinit
321 */
322 Xt._eventMatches = ({ self, mql, value, skipReinit = false, optionsInitial } = {}) => {
323 // fix NEEDED for chrome not removing mql event listener
324 if (!self.container.closest('html')) {
325 return
326 }
327 // replace options
328 if (mql.matches) {
329 // set options value
330 self.options = Xt.merge([self.options, value])
331 } else {
332 // set options value from initial
333 self.options = Xt.mergeReset({ start: self.options, reset: optionsInitial, check: value })
334 }
335 // reinit one time only with raf
336 if (!skipReinit) {
337 // reinit
338 Xt.frame({
339 el: self.container,
340 ns: `${self.ns}MatchFrame`,
341 func: () => {
342 Xt._eventReinit({ self })
343 },
344 })
345 }
346 }
347
348 /**
349 * removeMatches
350 * @param {Object} params
351 * @param {Object} params.self Self object
352 */
353 Xt._removeMatches = ({ self, optionsInitial } = {}) => {
354 // remove matches
355 if (self.matches?.length) {
356 for (const obj of self.matches) {
357 // matches
358 const mql = obj.mql
359 const value = obj.value
360 if (mql.removeEventListener) {
361 mql.removeEventListener('change', Xt._eventMatches.bind(null, { self, mql, value, optionsInitial }))
362 } else {
363 mql.removeListener(Xt._eventMatches.bind(null, { self, mql, value, optionsInitial }))
364 }
365 }
366 }
367 }
368
369 /**
370 * reinit
371 * @param {Object} params
372 * @param {Object} params.self Self object
373 * @param {Event} e
374 */
375 Xt._eventReinit = ({ self } = {}, e) => {
376 // triggering e.detail.container
377 if (!e?.detail?.container || e?.detail?.container.contains(self.container)) {
378 // handler
379 self.reinit()
380 }
381 }
382
383 //
384 // dataStorage
385 // map storage for HTML elements
386 //
387
388 Xt.dataStorage = {
389 /**
390 * properties
391 */
392 _storage: new Map(),
393
394 /**
395 * set key/obj pair on element's map
396 * @param {Node|HTMLElement|EventTarget|Window} el
397 * @param {String} key
398 * @param {*} obj
399 * @return {*}
400 */
401 set: (el, key, obj) => {
402 // new map if not already there
403 if (!Xt.dataStorage._storage.has(el)) {
404 Xt.dataStorage._storage.set(el, new Map())
405 }
406 // set
407 const getEl = Xt.dataStorage._storage.get(el)
408 getEl.set(key, obj)
409 // return
410 return getEl.get(key)
411 },
412
413 /**
414 * put key/obj pair on element's map, return old if exist already
415 * @param {Node|HTMLElement|EventTarget|Window} el
416 * @param {String} key
417 * @param {*} obj
418 * @return {*}
419 */
420 put: (el, key, obj) => {
421 // new map if not already there
422 if (!Xt.dataStorage._storage.has(el)) {
423 Xt.dataStorage._storage.set(el, new Map())
424 }
425 // return if already set
426 const getEl = Xt.dataStorage._storage.get(el)
427 const getKey = getEl.get(key)
428 if (getKey) {
429 return getKey
430 }
431 // set
432 getEl.set(key, obj)
433 // return
434 return getEl.get(key)
435 },
436
437 /**
438 * get obj from key on element's map
439 * @param {Node|HTMLElement|EventTarget|Window} el
440 * @param {String} key
441 * @return {*}
442 */
443 get: (el, key) => {
444 const getEl = Xt.dataStorage._storage.get(el)
445 // null if empty
446 if (!getEl) {
447 return null
448 }
449 // return
450 return getEl.get(key)
451 },
452
453 /**
454 * get all obj/key on element's map
455 * @param {Node|HTMLElement|EventTarget|Window} el
456 * @return {*}
457 */
458 getAll: el => {
459 const getEl = Xt.dataStorage._storage.get(el)
460 // null if empty
461 if (!getEl) {
462 return null
463 }
464 // return
465 return getEl
466 },
467
468 /**
469 * has key on element's map
470 * @param {Node|HTMLElement|EventTarget|Window} el
471 * @param {String} key
472 * @return {Boolean}
473 */
474 has: (el, key) => {
475 // return
476 return Xt.dataStorage._storage.get(el).has(key)
477 },
478
479 /**
480 * remove element's map key
481 * @param {Node|HTMLElement|EventTarget|Window} el
482 * @param {String} key
483 * @return {Boolean}
484 */
485 remove: (el, key) => {
486 const getEl = Xt.dataStorage._storage.get(el)
487 // null if empty
488 if (!getEl) {
489 return null
490 }
491 // remove
492 const ret = getEl.delete(key)
493 // remove storage if empty
494 if (getEl.size === false) {
495 Xt.dataStorage._storage.delete(el)
496 }
497 // return
498 return ret
499 },
500 }
501
502 //
503 // classBody
504 // util to remember classBody state
505 //
506
507 Xt._classBody = {
508 /**
509 * properties
510 */
511 currents: [],
512
513 /**
514 * add classBody currents
515 * @param {Object} obj Object
516 */
517 add: obj => {
518 Xt._classBody.currents.push(obj)
519 },
520
521 /**
522 * remove classBody currents
523 * @param {Object} obj Object
524 */
525 remove: obj => {
526 Xt._classBody.currents = Xt._classBody.currents.filter(x => x.c !== obj.c || x.ns !== obj.ns)
527 },
528
529 /**
530 * get classBody currents
531 * @param {Object} obj Object
532 * @return {Array} Currents
533 */
534 get: obj => {
535 return Xt._classBody.currents.filter(x => x.c === obj.c)
536 },
537 }
538
539 //
540 // util
541 //
542
543 /**
544 * friction
545 * @param {Object} params
546 * @param {Node|HTMLElement|EventTarget|Window} params.el Element
547 * @param {Object} params.obj Object with x and y values
548 * @param {Boolean} params.transform Use transforms instead of position
549 */
550 Xt._friction = ({ el, obj, transform = true } = {}) => {
551 Xt.frame({
552 el,
553 ns: `xtFrictionFrame`,
554 })
555 Xt.frame({
556 el,
557 ns: `xtFrictionInitFrame`,
558 func: () => {
559 // fix loop when not visible
560 if (Xt.visible({ el })) {
561 let xCurrent
562 let yCurrent
563 if (transform) {
564 const translate = Xt.getTranslate({ el })
565 xCurrent = translate[0]
566 yCurrent = translate[1]
567 } else {
568 const rect = el.getBoundingClientRect()
569 xCurrent = rect.left
570 yCurrent = rect.top
571 }
572 let xDist = obj.x - xCurrent
573 let yDist = obj.y - yCurrent
574 // momentum
575 const fncFriction = obj.friction
576 // set
577 if (fncFriction && Xt.dataStorage.get(el, 'xtFrictionX') && Xt.durationTimescale !== 1000) {
578 // friction
579 xCurrent += fncFriction({ delta: Math.abs(xDist) }) * Math.sign(xDist)
580 yCurrent += fncFriction({ delta: Math.abs(yDist) }) * Math.sign(yDist)
581 if (transform) {
582 el.style.transform = `translateX(${xCurrent}px) translateY(${yCurrent}px)`
583 } else {
584 el.style.left = `${xCurrent}px`
585 el.style.top = `${yCurrent}px`
586 }
587 } else {
588 // instant
589 xCurrent = obj.x
590 yCurrent = obj.y
591 // set
592 if (transform) {
593 el.style.transform = `translateX(${xCurrent}px) translateY(${yCurrent}px)`
594 } else {
595 el.style.top = `${yCurrent}px`
596 el.style.left = `${xCurrent}px`
597 }
598 }
599 // next interaction friction
600 Xt.dataStorage.set(el, 'xtFrictionX', xCurrent)
601 Xt.dataStorage.set(el, 'xtFrictionY', yCurrent)
602 // loop
603 if (fncFriction && Xt.durationTimescale !== 1000) {
604 const frictionLimit = obj.frictionLimit ? obj.frictionLimit : 1.5
605 xDist = obj.x - xCurrent
606 yDist = obj.y - yCurrent
607 Xt.frame({
608 el,
609 ns: `xtFrictionFrame`,
610 func: () => {
611 if (Math.abs(xDist) >= frictionLimit || Math.abs(yDist) >= frictionLimit) {
612 // continue friction
613 Xt._friction({ el, obj, transform })
614 } else {
615 // next interaction instant
616 Xt.dataStorage.remove(el, 'xtFrictionX')
617 Xt.dataStorage.remove(el, 'xtFrictionY')
618 }
619 },
620 })
621 }
622 }
623 },
624 })
625 }
626
627 /**
628 * Return translate values https://gist.github.com/aderaaij/a6b666bf756b2db1596b366da921755d
629 * @param {Object} params
630 * @param {Node|HTMLElement|EventTarget|Window} params.el Element to check target
631 * @return {Array} Values [x, y]
632 */
633 Xt.getTranslate = ({ el } = {}) => {
634 const transArr = []
635 const style = getComputedStyle(el)
636 const transform = style.transform
637 let mat = transform.match(/^matrix3d\((.+)\)$/)
638 if (mat) {
639 transArr.push(parseFloat(mat[1].split(', ')[13]))
640 } else {
641 mat = transform.match(/^matrix\((.+)\)$/)
642 mat ? transArr.push(parseFloat(mat[1].split(', ')[4])) : transArr.push(0)
643 mat ? transArr.push(parseFloat(mat[1].split(', ')[5])) : transArr.push(0)
644 }
645 return transArr
646 }
647
648 /**
649 * Contains for multiple elements
650 * @param {Object} params
651 * @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.els Elements to check if contains
652 * @param {Node|HTMLElement|EventTarget|Window} params.tr Element to check if contained
653 * @return {Boolean}
654 */
655 Xt.contains = ({ els, tr } = {}) => {
656 if (els instanceof HTMLElement) {
657 return els.contains(tr)
658 }
659 for (const el of els) {
660 if (el.contains(tr)) {
661 return true
662 }
663 }
664 return false
665 }
666
667 /**
668 * Get unique id
669 * @return {String} Unique id
670 */
671 Xt.uniqueId = () => {
672 Xt.uid = Xt.uid !== undefined ? Xt.uid : 0
673 return `xt-${Xt.uid++}`
674 }
675
676 /**
677 * Merge deep array of objects
678 * @param {Array} arr Array of objects to merge
679 * @return {Object} Merged object
680 */
681 Xt.merge = arr => {
682 const final = {}
683 for (const obj of arr) {
684 if (obj) {
685 for (const [key, value] of Object.entries(obj)) {
686 if (Array.isArray(value)) {
687 // if array merge arrays (e.g. options.popperjs modifiers)
688 final[key] = final[key] ? final[key] : []
689 final[key].push(...value)
690 } else if (
691 value !== null &&
692 typeof value === 'object' &&
693 !value.nodeName && // not HTML element
694 value !== window // not window
695 ) {
696 // if object deep merge
697 final[key] = Xt.merge([final[key], value])
698 } else {
699 final[key] = value
700 }
701 }
702 }
703 }
704 return final
705 }
706
707 /**
708 * Merge deep reset object only when equals to check
709 * @param {Object} params
710 * @param {Object} params.start object Start object
711 * @param {Object} params.reset object Reset object
712 * @param {Object} params.check object Check with start object to reset with reset object
713 * @return {Object} Merged object
714 */
715 Xt.mergeReset = ({ start, reset, check } = {}) => {
716 const final = start
717 for (const [key, value] of Object.entries(check)) {
718 if (
719 value !== null &&
720 typeof value === 'object' &&
721 !Array.isArray(value) && // not array
722 !value.nodeName && // not HTML element
723 value !== window // not window
724 ) {
725 final[key] = Xt.mergeReset({ start: start[key], reset: reset[key], check: check[key] })
726 } else {
727 if (start[key] === check[key]) {
728 final[key] = reset[key]
729 }
730 }
731 }
732 return final
733 }
734
735 /**
736 * Purify html string
737 * @param {String} str String
738 * @return {String} Purified string
739 */
740
741 Xt.sanitize = str => {
742 return DOMPurify.sanitize(str)
743 }
744
745 /**
746 * Create HTML element from html string
747 * @param {Object} params
748 * @param {Boolean} params.sanitize Sanitize
749 * @param {String} params.str String (only 1 root html tag)
750 * @return {Node} HTML elements
751 */
752 Xt.node = ({ sanitize = true, str }) => {
753 const template = document.createElement('template')
754 template.innerHTML = sanitize ? Xt.sanitize(str.trim()) : str.trim()
755 return template.content.firstChild
756 }
757
758 /**
759 * Create HTML elements from html string
760 * @param {Boolean} params.sanitize Sanitize
761 * @param {String} params.str Html String
762 * @return {Node} HTML elements
763 */
764 Xt.nodes = ({ sanitize = true, str }) => {
765 const template = document.createElement('template')
766 template.innerHTML = sanitize ? Xt.sanitize(str.trim()) : str.trim()
767 return template.content.childNodes
768 }
769
770 /**
771 * Add script to document
772 * @param {Object} params
773 * @param {String} params.url
774 * @param {Function} params.callback
775 * @param {Boolean} params.defer
776 * @param {Boolean} params.async
777 */
778 Xt.script = ({ url, callback, defer = true, async = true } = {}) => {
779 if (!document.querySelector(`script[src="${url}"]`)) {
780 const asyncfix = async
781 const script = document.createElement('script')
782 if (callback) {
783 script.onload = callback
784 }
785 script.type = 'text/javascript'
786 script.src = url
787 script.defer = defer
788 script.async = asyncfix
789 document.body.append(script)
790 }
791 }
792
793 /**
794 * requestAnimationFrame
795 * @param {Object} params
796 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
797 * @param {String} params.ns Namespace
798 * @param {Function} params.func Function to execute after transition or animation
799 */
800 Xt.frame = ({ el, ns = '', func } = {}) => {
801 cancelAnimationFrame(Xt.dataStorage.get(el, `${ns}Frame`))
802 if (func) {
803 // needs one raf
804 Xt.dataStorage.set(
805 el,
806 `${ns}Frame`,
807 requestAnimationFrame(() => {
808 func()
809 })
810 )
811 }
812 }
813
814 /**
815 * double requestAnimationFrame
816 * @param {Object} params
817 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
818 * @param {String} params.ns Namespace
819 * @param {Function} params.func Function to execute after transition or animation
820 */
821 Xt.frameDouble = ({ el, ns = '', func } = {}) => {
822 cancelAnimationFrame(Xt.dataStorage.get(el, `${ns}FrameDouble`))
823 if (func) {
824 // needs two raf or sometimes classes doesn't animate properly
825 Xt.dataStorage.set(
826 el,
827 `${ns}FrameDouble`,
828 requestAnimationFrame(() => {
829 Xt.dataStorage.set(
830 el,
831 `${ns}FrameDouble`,
832 requestAnimationFrame(() => {
833 func()
834 })
835 )
836 })
837 )
838 }
839 }
840
841 /**
842 * animation on classes
843 * @param {Object} params
844 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
845 * @param {String} params.ns Namespace
846 * @param {Number} params.duration Duration
847 * @param {Boolean} params.raf Use requestAnimationFrame
848 * @param {Boolean} params.initial Instant animations with initial
849 * @param {Function} params.callback
850 */
851 Xt.on = ({ el, ns = '', duration, raf = true, initial = false, callback } = {}) => {
852 Xt.animTimeout({ el, ns })
853 el.classList.add('on')
854 el.classList.remove('out')
855 const func = () => {
856 el.classList.add('in')
857 el.classList.remove('done')
858 Xt.animTimeout({
859 el,
860 ns,
861 duration,
862 actionCurrent: 'In',
863 func: () => {
864 el.classList.add('done')
865 if (callback) {
866 callback()
867 }
868 },
869 })
870 }
871 if (raf) {
872 // needs TWO raf or sequential off/on flickr (e.g. display)
873 Xt.frameDouble({ el, ns, func })
874 } else {
875 // fix need to repeat inside frameDouble in case we cancel
876 Xt.frameDouble({ el, ns })
877 func()
878 }
879 // initial
880 if (initial) {
881 el.classList.add('initial')
882 }
883 Xt.frameDouble({
884 el,
885 ns: `${ns}Initial`,
886 func: () => {
887 if (initial) {
888 el.classList.remove('initial')
889 }
890 },
891 })
892 }
893
894 /**
895 * animation off classes
896 * @param {Object} params
897 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
898 * @param {String} params.ns Namespace
899 * @param {Number} params.duration Duration
900 * @param {Boolean} params.raf Use requestAnimationFrame
901 * @param {Boolean} params.initial Instant animations with initial
902 * @param {Function} params.callback
903 */
904 Xt.off = ({ el, ns = '', duration, raf = true, initial = false, callback } = {}) => {
905 Xt.animTimeout({ el, ns })
906 // must be outside inside raf or page jumps (e.g. noqueue)
907 el.classList.remove('on')
908 const func = () => {
909 el.classList.remove('in', 'done')
910 el.classList.add('out')
911 Xt.animTimeout({
912 el,
913 ns,
914 duration,
915 actionCurrent: 'Out',
916 func: () => {
917 el.classList.remove('out')
918 if (callback) {
919 callback()
920 }
921 },
922 })
923 }
924 if (raf) {
925 // needs TWO raf or sequential off/on flickr (e.g. backdrop megamenu)
926 Xt.frameDouble({ el, ns, func })
927 } else {
928 // fix need to repeat inside frameDouble in case we cancel
929 Xt.frameDouble({ el, ns })
930 func()
931 }
932 // initial
933 if (initial) {
934 el.classList.add('initial')
935 }
936 Xt.frameDouble({
937 el,
938 ns: `${ns}Initial`,
939 func: () => {
940 if (initial) {
941 el.classList.remove('initial')
942 }
943 },
944 })
945 }
946
947 /**
948 * execute function after transition or animation
949 * @param {Object} params
950 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
951 * @param {String} params.ns Namespace
952 * @param {Number} params.duration Duration
953 * @param {String} params.actionCurrent Current action
954 * @param {Function} params.func Function to execute after transition or animation
955 */
956 Xt.animTimeout = ({ el, ns = '', duration, actionCurrent, func } = {}) => {
957 clearTimeout(Xt.dataStorage.get(el, `${ns}AnimTimeout`))
958 if (func) {
959 duration = Xt.animTime({ el, duration, actionCurrent }) ?? 0
960 if (!duration) {
961 func()
962 } else {
963 Xt.dataStorage.set(el, `${ns}AnimTimeout`, setTimeout(func, duration))
964 }
965 }
966 }
967
968 /**
969 * get transition or animation time
970 * @param {Object} params
971 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
972 * @param {Number} params.duration Duration
973 * @param {String} params.actionCurrent Current action
974 */
975 Xt.animTime = ({ el, duration, actionCurrent } = {}) => {
976 const custom =
977 (actionCurrent && el.getAttribute(`data-xt-duration-${actionCurrent}`)) || el.getAttribute('data-xt-duration')
978 if (custom) {
979 // if not number return the string
980 return isNaN(parseFloat(custom)) ? custom : parseFloat(custom) / Xt.durationTimescale
981 } else if (typeof duration === 'function') {
982 return duration
983 } else if (duration || duration === 0) {
984 return duration / Xt.durationTimescale
985 }
986 }
987
988 /**
989 * get delay time
990 * @param {Object} params
991 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
992 * @param {Number} params.duration Duration
993 * @param {String} params.actionCurrent Current action
994 */
995 Xt.delayTime = ({ el, duration, actionCurrent } = {}) => {
996 const custom =
997 (actionCurrent && el.getAttribute(`data-xt-delay-${actionCurrent}`)) || el.getAttribute('data-xt-delay')
998 if (custom) {
999 // if not number return the string
1000 return isNaN(parseFloat(custom)) ? custom : parseFloat(custom) / Xt.durationTimescale
1001 } else if (typeof duration === 'function') {
1002 return duration
1003 } else if (duration || duration === 0) {
1004 return duration / Xt.durationTimescale
1005 }
1006 }
1007
1008 /**
1009 * query array of elements or element
1010 * @param {Object} params
1011 * @param {NodeList|Array|Node|HTMLElement|EventTarget|Window} params.els Element to search from
1012 * @param {String} params.query Query for querySelectorAll
1013 * @return {Array}
1014 */
1015 Xt.queryAll = ({ els, query } = {}) => {
1016 // not when no query or empty array
1017 if (!query || els.length === 0) {
1018 return []
1019 }
1020 if (!els.length) {
1021 // search element
1022 return Array.from(els.querySelectorAll(query))
1023 } else {
1024 // search array
1025 const arr = []
1026 for (const el of els) {
1027 arr.push(...el.querySelectorAll(query))
1028 }
1029 return arr
1030 }
1031 }
1032
1033 /**
1034 * check element visibility
1035 * @param {Object} params
1036 * @param {Node|HTMLElement|EventTarget|Window} params.el Element animating
1037 * @return {Boolean}
1038 */
1039 Xt.visible = ({ el } = {}) => {
1040 return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length)
1041 }
1042
1043 /**
1044 * Set scrollbar width of document
1045 */
1046 Xt._setScrollbarWidth = () => {
1047 if (Xt.scrollbarWidth === undefined) {
1048 const scrollbarWidthHandler = Xt.dataStorage.put(window, 'resize/scrollbar', Xt._setScrollbarWidth)
1049 removeEventListener('resize', scrollbarWidthHandler)
1050 addEventListener('resize', scrollbarWidthHandler)
1051 }
1052 // add outer
1053 const outer = document.createElement('div')
1054 outer.style.visibility = 'hidden'
1055 outer.style.width = '100%'
1056 outer.style.msOverflowStyle = 'scrollbar' // needed for WinJS apps
1057 outer.classList.add('xt-ignore', 'xt-overflow-main')
1058 document.body.append(outer)
1059 // force scrollbars
1060 outer.style.overflow = 'scroll'
1061 // add inner
1062 const inner = document.createElement('div')
1063 inner.style.width = '100%'
1064 inner.classList.add('xt-ignore')
1065 outer.append(inner)
1066 // return
1067 const widthNoScroll = outer.offsetWidth
1068 const widthWithScroll = inner.offsetWidth
1069 Xt.scrollbarWidth = widthNoScroll - widthWithScroll
1070 document.documentElement.style.setProperty('--scrollbar-width', `${Xt.scrollbarWidth}px`)
1071 // remove
1072 outer.remove()
1073 }
1074
1075 Xt.ready({
1076 func: () => {
1077 Xt._setScrollbarWidth()
1078 },
1079 })
1080
1081 /**
1082 * resize.xt
1083 */
1084
1085 addEventListener('resize', e => {
1086 // we check also if outerWidth changes because on android doesn't change with ScrollTriggers
1087 // we check also innerWidth for resize desktop (e.g. inspect and resize)
1088 const w = window.innerWidth + window.outerWidth
1089 const h = window.innerHeight + window.outerHeight
1090 if (
1091 !e?.detail?.force && // not when setting delay on event
1092 Xt.dataStorage.get(window, 'xtEventDelayWidth') === w && // when width changes
1093 (Xt.resizeSkip() || Xt.dataStorage.get(window, 'xtEventDelayHeight') === h) // when height changes not touch
1094 ) {
1095 // only width no height because it changes on scroll on mobile
1096 return
1097 }
1098 Xt.dataStorage.set(
1099 window,
1100 `eventDelaySaveTimeout`,
1101 setTimeout(() => {
1102 Xt.dataStorage.set(window, 'xtEventDelayWidth', w)
1103 Xt.dataStorage.set(window, 'xtEventDelayHeight', h)
1104 dispatchEvent(new CustomEvent('resize.xt', { detail: e?.detail }))
1105 }, Xt.resizeDelay)
1106 )
1107 })
1108
1109 Xt.dataStorage.set(window, 'xtEventDelayWidth', window.innerWidth + window.outerWidth)
1110 Xt.dataStorage.set(window, 'xtEventDelayHeight', window.innerHeight + window.outerHeight)
1111
1112 /**
1113 * Xt._innerHeightSet and --vh
1114 */
1115 Xt._innerHeightSet = () => {
1116 Xt.innerHeight = window.innerHeight
1117 document.documentElement.style.setProperty('--vh', `${Xt.innerHeight * 0.01}px`)
1118 }
1119
1120 addEventListener('resize.xt', () => {
1121 Xt._innerHeightSet()
1122 })
1123
1124 Xt.ready({
1125 func: () => {
1126 Xt._innerHeightSet()
1127 },
1128 })
1129
1130 //
1131 // plugins
1132 //
1133
1134 /**
1135 * scrolltriggerRerfreshFix fixes refresh on touch screen not on vertical resize
1136 * @param {Object} params
1137 * @param {Function} params.ScrollTrigger
1138 */
1139 Xt.scrolltriggerRerfreshFix = ({ ScrollTrigger } = {}) => {
1140 // removed resize we trigger it manually
1141 ScrollTrigger.config({
1142 autoRefreshEvents: 'visibilitychange,DOMContentLoaded,load',
1143 })
1144 // window resize
1145 const resize = () => {
1146 ScrollTrigger.refresh()
1147 }
1148 removeEventListener('resize.xt', resize)
1149 addEventListener('resize.xt', resize)
1150 }
1151
1152 //
1153}