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,481 lines (1,440 loc) • 55 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'
/**
* SliderInit
*/
export class SliderInit extends Xt.Toggle {
//
// init
//
/**
* init elements, targets and currents
*/
_initScope() {
const self = this
const options = self.options
// wheel
self.wheel.deltaY = false
self.wheel.timeout
// dragger
self.dragger = self.container.querySelector(options.drag.dragger)
self._destroyElements.push(self.dragger)
// dragger initial
self.dragger.classList.add('initial')
// @PERF
self._destroyDrag()
self.drag._wrapDir = 0
self.drag._wrapIndex = null
const rect = self.dragger.getBoundingClientRect()
self.drag.size = self.drag.width = rect.width
self.drag.height = rect.height
self.drag._left = rect.left
// fix when dragger not :visible (offsetWidth === 0) do not initialize
if (self.drag.size === 0) {
return
}
// grab
if (!self.disabled) {
if (!options.drag.noMouse) {
self.dragger.classList.add('xt-grab')
}
}
// autoHeight and keepHeight
if (options.autoHeight) {
self._autoHeight = self.container.querySelector(options.autoHeight)
}
if (options.keepHeight) {
self._keepHeight = self.container.querySelector(options.keepHeight)
}
// val
self.drag._position = self.drag._final = self.drag._initial = 0
// clean
self._destroyNooverflow()
self._destroyWrap()
self._destroyPagination()
// targets
self._initScopeTargets()
// initGroups
self._initGroups()
// initPagination
self._initPagination()
// elements
self._initScopeElements()
}
/**
* init elements
*/
_initScopeElements() {
super._initScopeElements()
const self = this
const options = self.options
// elements
self.elements = self.elements.filter(x => !x.classList.contains(...options.elementsTemplate.split(' ')))
}
/**
* init groups
*/
_initGroups() {
const self = this
const options = self.options
// @PERF
let sizeContent = 0
let trWidthMax = 0
for (const tr of self.targets) {
// muse reset all targets before computation
if (!options.noobserver) {
Xt.unobserve({
container: tr,
id: self.ns,
})
self._resetPerfSize.bind(self, tr)()
}
}
for (const tr of self.targets) {
let trLeft
let trWidth
let trHeight
let trHeightContent
if (options.mode === 'absolute') {
trLeft = 0
trWidth = self.drag.width
trHeight = self.drag.height
trHeightContent = self.drag.height
} else {
const rect = tr.getBoundingClientRect()
trLeft = rect.left - self.drag._left
trWidth = rect.width
trHeight = rect.height
const content = tr.children.length ? tr.children[0] : tr
trHeightContent = content.offsetHeight
}
sizeContent += trWidth
trWidthMax = trWidth > trWidthMax ? trWidth : trWidthMax
Xt.dataStorage.set(tr, `${self.ns}TrLeftInitial`, trLeft)
Xt.dataStorage.set(tr, `${self.ns}TrLeft`, trLeft)
Xt.dataStorage.set(tr, `${self.ns}TrWidth`, trWidth)
Xt.dataStorage.set(tr, `${self.ns}TrHeight`, trHeight)
Xt.dataStorage.set(tr, `${self.ns}TrHeightContent`, trHeightContent)
}
self.drag.sizeContent = sizeContent
// initGroupsInitial
self._initGroupsInitial()
// disable slider if not overflowing
if (options.nooverflow) {
if (self.drag._availableSpace <= 0) {
self.dragger.classList.add(...options.nooverflow.split(' '))
// disabledManual
self._disabledManual = true
// needed for activation all slides
self._initGroupsInitial({ group: 1 })
} else {
self._destroyNooverflow()
}
}
// initGroupsPosition
self._initGroupsPosition()
// wrap
if (options.wrap && options.mode !== 'absolute') {
if (self.drag._availableSpace >= trWidthMax) {
self._wrap = true
}
} else {
self._wrap = false
}
// wrap indexes
self.drag._wrapFirst = 0
self.drag._wrapLast = self._groups.length - 1
// initGroupsContain
self._initGroupsContain()
// save cloned array of targets because we change it in the first loop and breaks second loop
for (const group of self._groups) {
group.targetsInitial = [...group.targets]
}
// initGroupsSame
self._initGroupsSame()
}
/**
* init groups initial
* @param {Object} params
* @param {Number} params.group
*/
_initGroupsInitial({ group } = {}) {
const self = this
const options = self.options
// inital groups
self._groups = []
let currentGroup = 0
group = group ?? options.group
const sizeAvailable = group ? self.drag.size * group : 0
let currentCount = sizeAvailable
self.drag._availableSpace = -self.drag.size
for (const [i, target] of self.targets.entries()) {
const targetWidth = Xt.dataStorage.get(target, `${self.ns}TrWidth`)
currentCount -= targetWidth
self.drag._availableSpace += targetWidth
if (currentCount >= 0) {
// add to previous group
} else if (i !== 0) {
// next group and reset count
currentGroup++
currentCount = sizeAvailable
currentCount -= targetWidth
}
// assign group
if (!self._groups[currentGroup]) {
self._groups[currentGroup] = {
target: target,
targets: [target],
}
} else {
self._groups[currentGroup].targets.push(target)
}
target.removeAttribute('data-xt-group') // needed or nooverflow doesn't reset
target.setAttribute('data-xt-group', `${self.ns}-${currentGroup}`)
target.removeAttribute('data-xt-group-same')
}
}
/**
* init groups position
*/
_initGroupsPosition() {
const self = this
const options = self.options
// groups position
self._usedWidth = 0
for (const group of self._groups) {
const tr = group.target
// vars
const trLeft = Xt.dataStorage.get(tr, `${self.ns}TrLeft`)
const targets = self.getTargets({ el: tr })
let groupLeft = Infinity
let groupWidth = 0
// vars
for (const tr of targets) {
// @PERF
const targetLeft = Xt.dataStorage.get(tr, `${self.ns}TrLeft`)
// groupLeft is last on the left
groupLeft = targetLeft < groupLeft ? trLeft : groupLeft
if (options.mode === 'absolute') {
// when absolute mode make fake positions as if all items displaced inside dragger
groupLeft += self._usedWidth
}
groupWidth += Xt.dataStorage.get(tr, `${self.ns}TrWidth`)
self._usedWidth += groupWidth
}
// left with alignment
let left
if (options.align === 'center') {
left = self.drag.size / 2 - groupLeft - groupWidth / 2
} else if (options.align === 'left') {
left = -groupLeft
} else if (options.align === 'right') {
left = self.drag.size - groupLeft - groupWidth
}
// save position
for (const tr of targets) {
Xt.dataStorage.set(tr, `${self.ns}GroupLeft`, left)
Xt.dataStorage.set(tr, `${self.ns}GroupWidth`, groupWidth)
}
}
}
/**
* init groups contain
*/
_initGroupsContain() {
const self = this
const options = self.options
// contain groups
if (options.contain && options.mode !== 'absolute' && !self._wrap && self._usedWidth > self.drag.size) {
// only if slides overflow dragger
const first = self._groups[self.drag._wrapFirst].target
const last = self._groups[self.drag._wrapLast].target
const firstLeft = Xt.dataStorage.get(first, `${self.ns}TrLeft`)
const lastLeft = Xt.dataStorage.get(last, `${self.ns}TrLeft`)
const lastWidth = Xt.dataStorage.get(last, `${self.ns}GroupWidth`)
const min = -firstLeft
const max = -lastLeft - lastWidth + self.drag.size
// group contain slides with same position
let iRemoved = 0
for (let i = 0; i < self._groups.length - iRemoved; i++) {
const group = self._groups[i]
for (const tr of group.targets) {
let left = Xt.dataStorage.get(tr, `${self.ns}GroupLeft`)
if (left >= min) {
left = min
// first group
const firstIndex = 0
let width = Xt.dataStorage.get(self._groups[firstIndex].target, `${self.ns}GroupWidth`)
width += Xt.dataStorage.get(group.target, `${self.ns}GroupWidth`)
// put group in firstIndex group
if (i > firstIndex) {
const groupStr = self._groups[firstIndex].target.getAttribute('data-xt-group')
for (const target of group.targets) {
self._groups[firstIndex].targets.push(target) // put at end
target.setAttribute('data-xt-group', groupStr)
}
}
// group firstIndex contain new position on dragger limit
for (const tr of self._groups[firstIndex].targets) {
Xt.dataStorage.set(tr, `${self.ns}GroupLeft`, left)
Xt.dataStorage.set(tr, `${self.ns}GroupWidth`, width)
}
// splice reindex
if (i > firstIndex) {
self._groups.splice(i, 1)
iRemoved++
i--
}
} else {
// break loop
i = self._groups.length
break
}
}
}
for (let i = self._groups.length - 1; i >= 0; i--) {
const group = self._groups[i]
for (const tr of group.targets) {
let left = Xt.dataStorage.get(tr, `${self.ns}GroupLeft`)
if (left <= max) {
left = max
// last group
const lastIndex = self._groups.length - 1
let width = Xt.dataStorage.get(self._groups[lastIndex].target, `${self.ns}GroupWidth`)
width += Xt.dataStorage.get(group.target, `${self.ns}GroupWidth`)
// put group in lastIndex group
if (i < lastIndex) {
const groupStr = self._groups[lastIndex].target.getAttribute('data-xt-group')
for (const target of group.targets) {
self._groups[lastIndex].targets.unshift(target) // put at start
target.setAttribute('data-xt-group', groupStr)
}
}
// group lastIndex contain new position on dragger limit
for (const target of self._groups[lastIndex].targets) {
Xt.dataStorage.set(target, `${self.ns}GroupLeft`, left)
Xt.dataStorage.set(target, `${self.ns}GroupWidth`, width)
}
// splice reindex
if (i < lastIndex) {
self._groups.splice(i, 1)
}
} else {
// break loop
i = 0
break
}
}
}
// save position
for (const group of self._groups) {
let groupWidth = 0
const left = Xt.dataStorage.get(group.target, `${self.ns}GroupLeft`)
for (const tr of group.targets) {
groupWidth += Xt.dataStorage.get(tr, `${self.ns}TrWidth`)
}
for (const tr of group.targets) {
Xt.dataStorage.set(tr, `${self.ns}GroupLeft`, left)
Xt.dataStorage.set(tr, `${self.ns}GroupWidth`, groupWidth)
}
}
// wrap indexes
self.drag._wrapFirst = 0
self.drag._wrapLast = self._groups.length - 1
}
}
/**
* init groups same
*/
_initGroupsSame() {
const self = this
const options = self.options
// groups multiple targets if are inside dragger
if (options.groupSame && options.mode !== 'absolute') {
for (const [z, group] of self._groups.entries()) {
const tr = group.target
const groupGroup = tr.getAttribute('data-xt-group')
const groupWidth = Xt.dataStorage.get(tr, `${self.ns}GroupWidth`)
const groupLeft = Xt.dataStorage.get(tr, `${self.ns}GroupLeft`)
const trLeft = Xt.dataStorage.get(tr, `${self.ns}TrLeft`)
const spaceLeft = trLeft + groupLeft // left space available from slide to dragger
const spaceRight = self.drag.size - spaceLeft - groupWidth // right space available from slide to dragger
// group on the right of current
let usedWidth = 0
for (let i = 0; i <= self._groups.length - 1; i++) {
// loop from current group to nexts
let iLoop = z + 1 + i
// when wrap and more than length, loop to first
iLoop = self._wrap && iLoop > self._groups.length - 1 ? iLoop - self._groups.length : iLoop
if (iLoop > self._groups.length - 1) break
const groupCurrent = self._groups[z]
for (let k = 0; k < self._groups[iLoop].targetsInitial.length; k++) {
const targetTargets = self._groups[iLoop].targetsInitial[k]
const width = Xt.dataStorage.get(targetTargets, `${self.ns}TrWidth`)
usedWidth += width
if (usedWidth <= spaceRight) {
// add to current group this target
groupCurrent.targets.push(targetTargets) // put at end
// add to target current group
const groupStr =
groupGroup +
options.groupSeparator +
targetTargets.getAttribute('data-xt-group') +
options.groupSeparator +
(targetTargets.getAttribute('data-xt-group-same') ?? '')
targetTargets.setAttribute('data-xt-group-same', groupStr)
} else {
// break loop
i = self._groups.length - 1
break
}
}
}
// group on the left of current
usedWidth = 0
for (let i = 0; i <= self._groups.length - 1; i++) {
// loop from current group to previouses
let iLoop = z - 1 - i
// when wrap and less than 0, loop to last
iLoop = self._wrap && iLoop < 0 ? iLoop + self._groups.length : iLoop
if (iLoop < 0) break
const groupCurrent = self._groups[z]
for (let k = self._groups[iLoop].targetsInitial.length - 1; k >= 0; k--) {
const targetTargets = self._groups[iLoop].targetsInitial[k]
const width = Xt.dataStorage.get(targetTargets, `${self.ns}TrWidth`)
usedWidth += width
if (usedWidth <= spaceLeft) {
// add to current group this target
groupCurrent.targets.unshift(targetTargets) // put at start
// add to target current group
const groupStr =
targetTargets.getAttribute('data-xt-group') +
options.groupSeparator +
groupGroup +
options.groupSeparator +
(targetTargets.getAttribute('data-xt-group-same') ?? '')
targetTargets.setAttribute('data-xt-group-same', groupStr)
} else {
// break loop
i = self._groups.length - 1
break
}
}
}
}
}
}
/**
* init slider pagination
*/
_initPagination() {
const self = this
const options = self.options
// not when empty it's possible to start empty
if (!self.targets.length) {
return false
}
// generate elements
const pags = self.container.querySelectorAll(options.pagination)
if (!pags.length) {
console.error('Error: Xt.Slider pagination not found for', self.container)
}
// pags
self.pags = self.pags ? self.pags : []
const template = `${options.elements}.${options.elementsTemplate.split(' ').join('.')}`
const templateInverse = `${options.elements}:not(.${options.elementsTemplate.split(' ').join('.')})`
for (const [z, pag] of pags.entries()) {
// vars
const cloned = pag.querySelector(template)
if (!cloned) {
console.error(`Error: Xt.Slider ${template} not found inside`, pag)
}
const container = cloned.parentNode
// populate
self.pags[z] = []
for (const [i, group] of self._groups.entries()) {
const item = document.createElement('div') // needed to set innerHTML instead of outerHTML to do html.search also attributes
const clone = cloned.cloneNode(true)
clone.classList.remove(...options.elementsTemplate.split(' '))
item.append(clone)
let html = item.innerHTML
const classes = ['xt-clone']
let regex = new RegExp('xt-content', 'ig')
if (html.search(regex) !== -1) {
let replace = ''
for (const tr of group.targets) {
const content = tr.querySelector('[data-xt-slider-content]')
if (content) {
replace += content.innerHTML
}
const attr = tr.querySelector('[data-xt-slider-element-classes]')
if (attr) {
classes.push(attr.getAttribute('data-xt-slider-element-classes'))
}
}
html = html.replace(regex, replace)
}
regex = new RegExp('xt-num', 'ig')
if (html.search(regex) !== -1) {
html = html.replace(regex, (i + 1).toString())
}
regex = new RegExp('xt-tot', 'ig')
if (html.search(regex) !== -1) {
html = html.replace(regex, self._groups.length.toString())
}
item.innerHTML = Xt.sanitize(html)
if (classes.length) {
item.children[0].classList.add(...classes)
}
item.children[0].setAttribute('data-xt-group', group.target.getAttribute('data-xt-group'))
container.insertBefore(item.children[0], cloned)
item.remove()
self.pags[z][i] = container.querySelectorAll(templateInverse)[i]
// save group element for activation
self._groups[i].element = self.pags[0][i]
}
}
}
/**
* init events
*/
_initEvents() {
super._initEvents()
const self = this
const options = self.options
// init
const initHandler = Xt.dataStorage.put(self.container, `init/${self.ns}`, self._eventInitHandler.bind(self))
self.container.addEventListener('init.xt.slider', initHandler)
// drag start
const dragstartHandler = Xt.dataStorage.put(
window,
`mousedown touchstart/drag/${self.ns}`,
self._eventDragstartHandler.bind(self),
)
const events = options.drag.noMouse ? ['touchstart'] : ['mousedown', 'touchstart']
for (const event of events) {
addEventListener(event, dragstartHandler, { passive: false })
}
// fix prevent dragging links and images
const dragstartFixHandler = Xt.dataStorage.put(self.dragger, `dragstart/drag/${self.ns}`, self._eventDragstartFix)
self.dragger.addEventListener('dragstart', dragstartFixHandler)
// resize
const reinitHandler = Xt.dataStorage.put(window, `resize.xt/${self.ns}`, Xt._eventReinit.bind(null, { self }))
addEventListener('resize.xt', reinitHandler)
}
/**
* init start
* @param {Object} params
* @param {Boolean} params.save Save currents
*/
_initStart({ save = false } = {}) {
const self = this
// fix when dragger not :visible (offsetWidth === 0) do not initialize
if (self.drag.size === 0) {
return
}
// @PERF
self._initPerfSize()
// init drag
Xt.frame({
el: self.container,
ns: `${self.ns}InitDrag`,
func: () => {
// dispatch event
self.drag._instant = true
self.drag._dragging = false
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
self._logicDragposition()
},
})
// super after
super._initStart({ save })
}
/**
* init performance size
*/
_initPerfSize() {
const self = this
const options = self.options
// @PERF
if (options.mode === 'relative' && !options.noobserver) {
Xt.perf({
func: () => {
for (const tr of self.targets) {
// intersection observer
const func = self._funcPerfSize.bind(self, tr)
Xt.observe({
container: tr,
func,
id: self.ns,
})
}
},
})
}
}
/**
* set performance size
*/
_setPerfSize(tr) {
const self = this
const options = self.options
// logic
const width = Xt.dataStorage.get(tr, `${self.ns}TrWidth`)
const height = Xt.dataStorage.get(tr, `${self.ns}TrHeight`)
tr.style.width = `${width}px`
tr.style.height = `${height}px`
for (const child of tr.children) {
child.style.display = 'none'
}
if (options.a11y.hidden) {
tr.setAttribute('aria-hidden', 'true')
}
}
/**
* reset performance size
*/
_resetPerfSize(tr) {
const self = this
const options = self.options
// logic
tr.style.height = ''
tr.style.width = ''
for (const child of tr.children) {
child.style.display = ''
}
if (options.a11y.hidden) {
tr.setAttribute('aria-hidden', 'false')
}
}
/**
* func performance size
*/
_funcPerfSize(tr, intersecting) {
const self = this
// logic
if (intersecting) {
self._resetPerfSize.bind(self, tr)()
} else {
self._setPerfSize.bind(self, tr)()
}
}
//
// handler
//
/**
* init handler
* @param {Event} e
*/
_eventInitHandler() {
const self = this
// dragger initial
self.dragger.classList.remove('initial')
}
/**
* drag fix
* @param {Event} e
*/
_eventDragstartFix(e) {
e.preventDefault()
}
/**
* drag on handler
* @param {Event} e
*/
_eventDragstartHandler(e) {
const self = this
const options = self.options
// not when outside dragger
if (!self.dragger.contains(e.target)) {
return
}
// not right click or it gets stuck
if (!e.button || e.button !== 2) {
// handler
if (options.eventLimit) {
const eventLimit = self._containerTargets.querySelectorAll(options.eventLimit)
if (!Xt.contains({ els: eventLimit, tr: e.target })) {
self._eventDragstart(e)
}
} else {
self._eventDragstart(e)
}
// dragend
const dragendHandler = Xt.dataStorage.put(
window,
`mouseup touchend/drag/${self.ns}`,
self._eventDragendHandler.bind(self),
)
const events = ['mouseup', 'touchend']
for (const event of events) {
addEventListener(event, dragendHandler)
}
}
}
/**
* drag off handler
* @param {Event} e
*/
_eventDragendHandler(e) {
const self = this
const options = self.options
// logic
if (options.eventLimit) {
const eventLimit = self._containerTargets.querySelectorAll(options.eventLimit)
if (!Xt.contains({ els: eventLimit, tr: e.target })) {
self._eventDragend(e)
}
} else {
self._eventDragend(e)
}
}
/**
* drag on
* @param {Event} e
*/
_eventDragstart(e) {
const self = this
// event move
const dragHandler = Xt.dataStorage.put(
window,
`mousemove touchmove/drag/${self.ns}`,
self._eventDragHandler.bind(self),
)
const events = ['mousemove', 'touchmove']
for (const event of events) {
addEventListener(event, dragHandler, { passive: false })
}
// logic
self._logicDragstart(e)
}
/**
* drag handler
* @param {Event} e
*/
_eventDragHandler(e) {
const self = this
// logic
self._logicDragmove(e)
}
/**
* drag off
* @param {Event} e
*/
_eventDragend(e) {
const self = this
// event off
const dragendHandler = Xt.dataStorage.get(window, `mouseup touchend/drag/${self.ns}`)
const eventsoff = ['mouseup', 'touchend']
for (const event of eventsoff) {
removeEventListener(event, dragendHandler)
}
// event move
const dragHandler = Xt.dataStorage.get(window, `mousemove touchmove/drag/${self.ns}`)
const eventsmove = ['mousemove', 'touchmove']
for (const event of eventsmove) {
removeEventListener(event, dragHandler)
}
// logic
self._logicDragend(e)
}
//
// event
//
/**
* element on
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.el To be activated
* @param {Boolean} params.force
* @param {Event} e
* @return {Boolean} If activated
*/
_eventOn({ el, force = false }, e) {
const self = this
const options = self.options
// disabled
if (self.disabled) {
return
}
// get the right slide
let found
for (const group of self._groups) {
// targetsInitial because multiple pagination
if (group.element === el || group.targetsInitial.includes(el)) {
found = group
}
}
if (!found) return
const group = found
el = found.element
const tr = found.target
// fix keep self.drag._instant (e.g. slider-hero-v2)
const isInstant = self.drag._instant
// activation
super._eventOn({ el, force }, e)
// vars
const first = self._groups[self.drag._wrapFirst].target
const last = self._groups[self.drag._wrapLast].target
const min = Xt.dataStorage.get(first, `${self.ns}GroupLeft`)
const max = Xt.dataStorage.get(last, `${self.ns}GroupLeft`)
const maxCheck = options.mode !== 'absolute' ? max : Xt.dataStorage.get(first, `${self.ns}GroupWidth`)
// val
self.drag._initial = Xt.dataStorage.get(tr, `${self.ns}GroupLeft`)
// fix absolute loop
if (options.mode === 'absolute' && !self.initial && self.direction) {
const loopingMoreThanOne = Math.abs(self.drag._initial - self.drag._position) > maxCheck
if (options.loop && tr === last && loopingMoreThanOne && self.direction < 0) {
// calculate position when looping to end
const remainder = max - min + self.drag._position - maxCheck
// val
self.drag._final = remainder
// dispatch event
self.drag._instant = true
self.drag._dragging = false
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
self._logicDragposition()
} else if (options.loop && tr === first && loopingMoreThanOne && self.direction > 0) {
// calculate position when looping to start
const remainder = min - max + self.drag._position + maxCheck
// val
self.drag._final = remainder
// dispatch event
self.drag._instant = true
self.drag._dragging = false
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
self._logicDragposition()
} else if (loopingMoreThanOne) {
// calculate difference of distance still to be covered when looping more than 1 item
const remainder = self.drag._final - self.drag._position - maxCheck * self.direction
// val
self.drag._final = self.drag._initial - remainder
// dispatch event
self.drag._instant = true
self.drag._dragging = false
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
self._logicDragposition()
}
}
// val
self.drag._final = self.drag._initial
self.drag._direction = null
// ratio
self.drag._ratioInverse = Math.abs(self.drag._final - self.drag._position) / Math.abs(maxCheck - min)
self.drag._ratio = 1 - self.drag._ratioInverse
// dispatch event
self.drag._instant = false
self.drag._dragging = false
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
self._logicDragposition()
// fix keep self.drag._instant (e.g. slider-hero-v2 dragging mask)
self.drag._instant = isInstant
Xt.frame({
el: self.container,
ns: `${self.ns}isInstant`,
func: () => {
// needed for off event (e.g. slider-hero-v2 clicking next furiously)
self.drag._instant = false
},
})
// wrap after self.drag._final for proper initial initialization direction (e.g. slider api)
self._eventWrap({ index: self.index })
// autoHeight and keepHeight
if (self._autoHeight || (self._keepHeight && self.initial)) {
let groupHeight = 0
for (const tr of group.targets) {
const trHeight = Xt.dataStorage.get(tr, `${self.ns}TrHeightContent`)
groupHeight = trHeight > groupHeight ? trHeight : groupHeight
}
if (groupHeight > 0) {
groupHeight += 'px'
if (self._autoHeight.style.height !== groupHeight) {
self._autoHeight.style.height = groupHeight
// dispatch event
tr.dispatchEvent(new CustomEvent(`autoheight.${self._componentNs}`))
}
if (self._keepHeight && self.initial) {
self._keepHeight.style.height = groupHeight
}
}
}
}
/**
* 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 {
// direction before setting positions we check target activations positions (also when wrap there's no other way)
const left = Xt.dataStorage.get(self._groups[self.index].target, `${self.ns}GroupLeft`)
const leftOld = Xt.dataStorage.get(self._groups[self._oldIndex].target, `${self.ns}GroupLeft`)
self.direction = left > leftOld ? -1 : 1
}
self._inverse = self.direction < 0
}
/**
* wrap
* @param {Object} params
* @param {Boolean} params.index
*/
_eventWrap({ index } = {}) {
const self = this
// logic
if (self._wrap) {
const tr = self._groups[index].target
const wrapLeft = Xt.dataStorage.get(tr, `${self.ns}GroupLeft`)
const previousWidth = Xt.dataStorage.get(tr, `${self.ns}GroupWidth`)
// only one call
let direction = self.direction
if (!direction) {
// fix on init and reinit set direction to left (-1) if on left, to right (1) if on right
direction = self.drag._final > -self.drag.size ? -1 : 1
}
if (self.drag._wrapDir !== direction || self.drag._wrapIndex !== index) {
self.drag._wrapDir = direction
self.drag._wrapIndex = index
self._eventMove({ index, direction, wrapLeft, previousWidth, wrapSpace: previousWidth })
}
}
}
/**
* eventMove
* @param {Object} params
* @param {Number} params.index
* @param {Number} params.direction
* @param {Number} params.wrapLeft
* @param {Number} params.previousWidth
* @param {Number} params.wrapSpace
*/
_eventMove({ index, direction, wrapLeft, previousWidth, wrapSpace } = {}) {
const self = this
const options = self.options
// index
const tot = self._groups.length
if (direction < 0) {
index = self.getPrevIndex({ index })
// keep index of moved slides
self.drag._wrapFirst = index
self.drag._wrapFirst = self.drag._wrapFirst < tot ? self.drag._wrapFirst : self.drag._wrapFirst - tot
self.drag._wrapFirst = self.drag._wrapFirst >= 0 ? self.drag._wrapFirst : tot + self.drag._wrapFirst
// keep index of moved slides
self.drag._wrapLast = self.drag._wrapFirst - 1
self.drag._wrapLast = self.drag._wrapLast < tot ? self.drag._wrapLast : self.drag._wrapLast - tot
self.drag._wrapLast = self.drag._wrapLast >= 0 ? self.drag._wrapLast : tot + self.drag._wrapLast
} else if (direction > 0) {
index = self.getNextIndex({ index })
// keep index of moved slides
self.drag._wrapLast = index
self.drag._wrapLast = self.drag._wrapLast < tot ? self.drag._wrapLast : self.drag._wrapLast - tot
self.drag._wrapLast = self.drag._wrapLast >= 0 ? self.drag._wrapLast : tot + self.drag._wrapLast
// keep index of moved slides
self.drag._wrapFirst = self.drag._wrapLast + 1
self.drag._wrapFirst = self.drag._wrapFirst < tot ? self.drag._wrapFirst : self.drag._wrapFirst - tot
self.drag._wrapFirst = self.drag._wrapFirst >= 0 ? self.drag._wrapFirst : tot + self.drag._wrapFirst
}
// when only one item index is null
if (index === null) return
// logic
let translate
let left = wrapLeft
const group = self._groups[index].targetsInitial
const tr = group[0]
const leftInitial = Xt.dataStorage.get(tr, `${self.ns}TrLeftInitial`)
const width = Xt.dataStorage.get(tr, `${self.ns}GroupWidth`)
// calculate left (left position of the dragger when tr activated)
// values are inverted (+ is going left, - is going right)
// we need different calculation depending on direction because the line is on the left or on the right of tr
if (direction < 0) {
if (options.align === 'center') {
// left is the center position of current tr
left += previousWidth / 2 + width / 2
} else if (options.align === 'left') {
// left is the left position of current tr
left += width
} else if (options.align === 'right') {
// left is the right position of current tr
left += previousWidth
}
} else if (direction > 0) {
if (options.align === 'center') {
// left is the center position of current tr
left -= previousWidth / 2 + width / 2
} else if (options.align === 'left') {
// left is the left position of current tr
left -= previousWidth
} else if (options.align === 'right') {
// left is the right position of current tr
left -= width
}
}
for (const tr of group) {
Xt.dataStorage.set(tr, `${self.ns}GroupLeft`, left)
}
// calculate translate (translate position of the tr)
// values are inverted (+ is going left, - is going right)
if (direction < 0) {
// translate is the difference from current wrapLeft and tr initial left and tr width
translate = wrapLeft + leftInitial + width
} else if (direction > 0) {
// translate is the difference from current wrapLeft and tr initial left and previous tr width
translate = wrapLeft + leftInitial - previousWidth
}
if (options.align === 'center') {
// remove the space between left drag position and final position
// remove from translate the difference between half drag size and half previous tr width
translate -= self.drag.size / 2 - previousWidth / 2
} else if (options.align === 'left') {
// remove the space between left drag position and final position
translate -= 0
} else if (options.align === 'right') {
// remove the space between left drag position and final position
// remove from translate the difference from drag size and previous tr width
translate -= self.drag.size - previousWidth
}
for (const tr of group) {
const trLeftInitial = Xt.dataStorage.get(tr, `${self.ns}TrLeftInitial`)
Xt.dataStorage.set(tr, `${self.ns}TrLeft`, trLeftInitial - translate)
tr.style.transform = `translateX(${-translate}px)`
}
// calculate space (current wrap space difference of activation to check against drag size)
// we need different calculation depending on direction because the line is on the left or on the right of tr
if (direction < 0) {
if (options.align === 'center') {
wrapSpace += width + previousWidth / 2
} else if (options.align === 'left') {
wrapSpace += width + self.drag.size - previousWidth
} else if (options.align === 'right') {
wrapSpace += width
}
} else if (direction > 0) {
if (options.align === 'center') {
wrapSpace += width + previousWidth / 2
} else if (options.align === 'left') {
wrapSpace += width
} else if (options.align === 'right') {
wrapSpace += width + self.drag.size - previousWidth
}
}
// continue moving only if wrapSpace is smaller than drag
if (wrapSpace <= self.drag.size) {
self._eventMove({ index, direction, wrapLeft: left, previousWidth: width, wrapSpace })
}
}
//
// drag
//
/**
* drag on public
* @param {Event} e
*/
dragstart(e) {
const self = this
// logic
self._logicDragstart(e)
}
/**
* drag on logic
* @param {Event} e
*/
_logicDragstart(e) {
const self = this
// disabled
if (self.disabled) {
return
}
// save event
if (e.clientX !== undefined) {
self.drag._start = e.clientX
self.drag._startOther = e.clientY
} else if (e.touches && e.touches.length) {
self.drag._start = e.touches[0].clientX
self.drag._startOther = e.touches[0].clientY
}
// auto
self._eventAutostop()
// vars
self._autoblock = true
self.drag._lock = false
self.drag._prevent = false
self.drag._index = self.index
self.drag._old = self.drag._start
self.drag._overflow = null
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`dragstart.${self._componentNs}`))
}
/**
* drag move public
* @param {Event} e
* @param {Object} params
* @param {Boolean} params.keepActivated Do not disable with pointer-events-none
* @param {Number} params.setup
*/
dragmove(e, { keepActivated, setup } = {}) {
const self = this
// logic
self._logicDragmove(e, { keepActivated, setup })
}
/**
* drag move logic
* @param {Event} e
* @param {Object} params
* @param {Boolean} params.keepActivated Do not disable with pointer-events-none
* @param {Boolean} params.setup first dragmove instant setup
*/
_logicDragmove(e, { keepActivated = false, setup = false } = {}) {
const self = this
const options = self.options
// disabled
if (self.disabled) {
return
}
// save event
if (e.clientX !== undefined) {
self.drag._current = e.clientX
self.drag._currentOther = e.clientY
} else if (e.touches && e.touches.length) {
self.drag._current = e.touches[0].clientX
self.drag._currentOther = e.touches[0].clientY
}
// check threshold
self.drag._distance = self.drag._start - self.drag._current
self.drag._distanceOther = self.drag._startOther - self.drag._currentOther
// prevent drag and lock drag
if (!self.drag._lock && !self.drag._prevent) {
if (Math.abs(self.drag._distanceOther) > options.drag.threshold) {
// only if dragging enough other
// prevent drag
self.drag._prevent = true
} else if (Math.abs(self.drag._distance) > options.drag.threshold) {
// only if dragging enough
// lock drag
self.drag._lock = true
}
}
// fix no drag change when click
if (self.drag._start === self.drag._current) {
return
}
// first dragmove instant setup
if (setup) {
// only 1 pixel same direction
self.drag._current = Math.sign(self.drag._current)
}
// prevent drag
if (self.drag._prevent) {
return
}
if (self.drag._lock) {
// prevent page scroll
if (e.cancelable) {
e.preventDefault()
}
// disable interaction
if (!keepActivated) {
for (const tr of self.targets) {
tr.classList.add('pointer-events-none')
}
}
}
// calc
const first = self._groups[self.drag._wrapFirst].target
const last = self._groups[self.drag._wrapLast].target
const min = Xt.dataStorage.get(first, `${self.ns}GroupLeft`)
const max = Xt.dataStorage.get(last, `${self.ns}GroupLeft`)
const maxCheck = options.mode !== 'absolute' ? max : Xt.dataStorage.get(first, `${self.ns}GroupWidth`)
// val
let final = self.drag._position + (self.drag._current - self.drag._old) * options.drag.factor
self.drag._direction = self.drag._current > self.drag._old ? -1 : 1
self.drag._old = self.drag._current
// overflow
if (!self._wrap && options.mode !== 'absolute') {
const direction = Math.sign(self.drag._distance)
const func = options.drag.overflow ? options.drag.overflow : () => 0
if (final > min && direction < 0) {
self.drag._overflow = self.drag._overflow ? self.drag._overflow : self.drag._current
const overflow = self.drag._current - self.drag._overflow
final = overflow >= 0 ? min + func({ overflow }) : final
} else if (final < max && direction > 0) {
self.drag._overflow = self.drag._overflow ? self.drag._overflow : self.drag._current
const overflow = self.drag._current - self.drag._overflow
final = overflow <= 0 ? max - func({ overflow: -overflow }) : final
}
}
// val
self.drag._final = final
// set direction
self.direction = Math.sign(self.drag._initial - self.drag._final)
self._inverse = self.direction < 0
// ratio
self.drag._ratio = Math.abs(self.drag._final - self.drag._initial) / Math.abs(maxCheck - min)
self.drag._ratio = self.drag._ratio > 1 ? 1 : self.drag._ratio
self.drag._ratio = self.drag._ratio < -1 ? -1 : self.drag._ratio
self.drag._ratioInverse = 1 - self.drag._ratio
if (options.mode === 'absolute') {
// fix direction and ratio when on dragmove same direction (e.g. wheel)
if (
(self.direction > 0 && self.drag._final > self.drag._initial) ||
(self.direction < 0 && self.drag._final < self.drag._initial)
) {
self.drag._ratio = 0
self.drag._ratioInverse = 0
}
}
// dispatch event
self.drag._instant = true
if (self.direction !== self.directionOld) {
// fix direction and ratio when on dragmove same direction (e.g. wheel)
self.drag._dragging = false
} else {
self.drag._dragging = true
}
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
self._logicDragposition()
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`drag.${self._componentNs}`))
// reset
self._inverse = null
self.directionOld = self.direction
// activation
if (self.drag._lock && options.mode !== 'absolute') {
// get nearest
const found = self._logicDragfind({ index: self.index })
if (found !== null && found !== self.index) {
super._eventOn({ el: self._groups[found].element, force: true })
self._eventWrap({ index: found })
}
}
}
/**
* drag off public
* @param {Event} e
*/
dragend(e) {
const self = this
// logic
self._logicDragend(e)
}
/**
* drag off logic
* @param {Event} e
*/
_logicDragend(e) {
const self = this
const options = self.options
// disabled
if (self.disabled) {
return
}
// save event
if (e.clientX !== undefined) {
self.drag._current = e.clientX
self.drag._currentOther = e.clientY
} else if (e.touches && e.touches.length) {
self.drag._current = e.touches[0].clientX
self.drag._currentOther = e.touches[0].clientY
}
// vars
self._autoblock = false
// disable interaction
for (const tr of self.targets) {
tr.classList.remove('pointer-events-none')
}
// fix no drag change when click
if (self.drag._start === self.drag._current) {
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`dragend.${self._componentNs}`))
return
}
// raf because on.xt.slider event after all drag.xt.slider
requestAnimationFrame(() => {
// only if dragging enough
if (self.drag._lock) {
const index = self.index
if (index !== self.drag._index) {
// if on the same slide as we started dragging or in absolute mode looping
// options.free and self.drag._overflow to fix reset when overflowing
if (!options.free || self.drag._overflow) {
self.goToNum({ index })
}
} else {
// if not on the same slide as we started dragging
// depending on direction and if direction is not going back
const direction = Math.sign(self.drag._distance)
if (
direction > 0 &&
self.drag._direction > 0 &&
(options.loop || self._wrap || index !== self.getElementsGroups().length - 1)
) {
if (!options.free || index === self.getElementsGroups().length - 1) {
self.goToNext({ amount: 1 })
}
} else if (direction < 0 && self.drag._direction < 0 && (options.loop || self._wrap || index !== 0)) {
if (!options.free || index === 0) {
self.goToPrev({ amount: 1 })
}
} else {
if (!self._wrap) {
self._logicDragreset()
}
}
}
} else {
if (!options.free) {
self._logicDragreset()
}
}
// auto
self._eventAutostart()
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`dragend.${self._componentNs}`))
})
}
/**
* drag reset logic
*/
_logicDragreset() {
const self = this
const options = self.options
// set direction
// inverse of default one
self.direction = -Math.sign(self.drag._initial - self.drag._final)
self._inverse = self.direction < 0
// calc
const first = self._groups[self.drag._wrapFirst].target
const last = self._groups[self.drag._wrapLast].target
const min = Xt.dataStorage.get(first, `${self.ns}GroupLeft`)
const max = Xt.dataStorage.get(last, `${self.ns}GroupLeft`)
const maxCheck = options.mode !== 'absolute' ? max : Xt.dataStorage.get(first, `${self.ns}GroupWidth`)
// ratio
// inverse of default one
self.drag._ratio = 1 - Math.abs(self.drag._final - self.drag._initial) / Math.abs(maxCheck - min)
self.drag._ratioInverse = 1 - self.drag._ratio
// val
self.drag._final = self.drag._initial
self.drag._direction = null
// dispatch event
self.drag._instant = false
self.drag._dragging = false
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
self._logicDragposition()
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`dragreset.${self._componentNs}`))
// reset
self._inverse = null
// auto
self._eventAutostart()
}
/**
* activate index depending on drag position
* @param {Object} params
* @param {Boolean} params.index
* @return {Number} Activation index
*/
_logicDragfind({ index } = {}) {
const self = this
// find activation
const direction = self.drag._direction
if (direction < 0) {
const first = self._groups[self.drag._wrapFirst].target
const min = Xt.dataStorage.get(first, `${self.ns}GroupLeft`)
if (self.drag._final >= min) {
return self.drag._wrapFirst
}
for (let i = index; i >= 0; i--) {
const tr = self._groups[i].target
const left = Xt.dataStorage.get(tr, `${self.ns}GroupLeft`)
const width = Xt.dataStorage.get(tr, `${self.ns}GroupWidth`)
// first inside dragger on the left
if (self.drag._final < left + width / 2) {
return i
}
if (i === 0) {
// continue loop
i = self._groups.length
}
if (i === index + 1) {
// break loop
break
}
}
} else if (direction > 0) {
const last = self._groups[self.drag._wrapLast].target
const max = Xt.dataStorage.get(last, `${self.ns}GroupLeft`)
if (self.drag._final <= max) {
return self.drag._wrapLast
}
for (let i = index; i < self._groups.length; i++) {
const tr = self._groups[i].target
const left = Xt.dataStorage.get(tr, `${self.ns}GroupLeft`)
const width = Xt.dataStorage.get(tr, `${self.ns}GroupWidth`)
// last inside dragger on the right
if (self.drag._final > left - width / 2) {
return i
}
if (i === self._groups.length - 1) {
// continue loop
i = -1
}
if (i === index - 1) {
// break loop
break
}
}
}
return null
}
/**
* drag position logic
*/
_logicDragposition() {
const self = this
const options = self.options
// dragposition
if (!options.dragposition && options.mode !== 'absolute') {
self.drag._instant ? self.dragger.classList.remove('on') : self.dragger.classList.add('on')
// set internal position to resume animation mid dragging
self.drag._position = self.drag._final
self.dragger.style.transform = `translateX(${self.drag._final}px)`
}
}
//
// wheel
//
/**
* wheelEvent public
* @param {Object} params
* @param {Number} params.deltaFactor Multiply factor
* @param {Number} params.timeout End Timeout
* @param {Event} e
* @return {Boolean} If not overflowing
*/
wheelEvent({ factor = -1, timeout = 250, threshold = 10 } = {}, e) {
const self = this
const options = self.options
// logic
let clientX = e.deltaY * factor
if (Math.abs(clientX) < threshold) {
// if small value (touchpad smooth)
if (self.wheel.deltaY) {
// if running stop
clearTimeout(self.wheel.timeout)
self._wheelStop({ clientX })
}
} else {
if (!self.wheel.deltaY) {
// if first or resetted wheel start and move
self._wheelStart()
self._wheelMove({ clientX, setup: true })
self._wheelMove({ clientX })
} else {
// sequential w