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,531 lines (1,471 loc) • 48.3 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 { Xt } from './xt'
import './toggle'
import RJSON from 'relaxed-json'
Xt.RJSON = RJSON
/**
* Slider
*/
class Slider extends Xt.Toggle {
/**
* constructor
* @param {Node|HTMLElement|EventTarget|Window} object Base node
* @param {Object} optionsCustom User options
* @constructor
*/
constructor(object, optionsCustom = {}) {
super(object, optionsCustom)
}
//
// init
//
/**
* init
*/
_init() {
// super after
super._init()
}
/**
* init elements, targets and currents
*/
_initScope() {
const self = this
const options = self.options
// dragger
self.dragger = self.container.querySelector(options.drag.dragger)
self._destroyElements.push(self.dragger)
// dragger initial
self.dragger.classList.add('initial')
// @PERF
self.drag._wrapDir = 0
self.drag._wrapIndex = null
self.drag._size = self.dragger.offsetWidth
// fix when dragger not :visible (offsetWidth === 0) do not initialize
if (self.drag._size === 0) {
return
}
// grab
if (!self.disabled) {
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._destroyDrag()
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) {
let trLeft
let trWidth
if (options.mode === 'absolute') {
trLeft = 0
trWidth = self.drag._size
} else {
trLeft = tr.offsetLeft
trWidth = tr.offsetWidth
}
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)
}
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 !== false && options.mode !== 'absolute' && 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`, Math.floor(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 aria
*/
_initAriaRole() {
const self = this
const options = self.options
// aria
if (options.aria) {
// role
if (options.aria === true || options.aria.role) {
for (const tr of self.targets) {
tr.setAttribute('role', 'group')
tr.setAttribute('aria-roledescription', 'slide')
}
self.container.setAttribute('aria-roledescription', 'carousel')
}
}
}
/**
* init events
*/
_initEvents() {
super._initEvents()
const self = this
// init
const initHandler = Xt.dataStorage.put(self.container, `init/${self.ns}`, self._eventInitHandler.bind(self))
self.container.addEventListener('init.xt.slider', initHandler)
// dragposition
const dragpositionHandler = Xt.dataStorage.put(
self.dragger,
`dragposition/${self.ns}`,
self._eventDragpositionHandler.bind(self)
)
self.dragger.addEventListener('dragposition.xt.slider', dragpositionHandler)
// drag start
const dragstartHandler = Xt.dataStorage.put(
window,
`mousedown touchstart/drag/${self.ns}`,
self._eventDragstartHandler.bind(self)
)
const events = ['mousedown', 'touchstart']
for (const event of events) {
addEventListener(event, dragstartHandler, { passive: false })
}
// fix prevent dragging links and images
const dragstartFixHandler = Xt.dataStorage.put(window, `dragstart/drag/${self.ns}`, self._eventDragstartFix)
self.dragger.addEventListener('dragstart', dragstartFixHandler)
// resize
const reinitHandler = Xt.dataStorage.put(window, `resize/reinit/${self.ns}`, Xt._eventReinit.bind(null, { self }))
addEventListener('resize', reinitHandler)
}
/**
* init start
* @param {Object} params
* @param {Boolean} params.save Save currents
*/
_initStart({ save = false } = {}) {
const self = this
// init drag
Xt.frame({
el: self.container,
ns: `${self.ns}InitDrag`,
func: () => {
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
},
})
// super after
super._initStart({ save })
}
//
// handler
//
/**
* init handler
* @param {Event} e
*/
_eventInitHandler() {
const self = this
// dragger initial
self.dragger.classList.remove('initial')
}
/**
* drag position handler
* @param {Event} e
*/
_eventDragpositionHandler() {
const self = this
const options = self.options
// dragposition
if (!options.dragposition && options.mode !== 'absolute') {
self.initial || 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)`
}
}
/**
* 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
// raf Drag and Dragend
Xt.frame({
el: self.dragger,
ns: `${self.ns}DragFrame`,
func: () => {
// logic
self._logicDrag(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)
}
// raf Drag and Dragend
Xt.frame({
el: self.dragger,
ns: `${self.ns}DragFrame`,
func: () => {
// 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
const isDrag = 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' && options.loop && !self.initial) {
if (tr === last && (self.drag._direction < 0 || self.drag._position >= min)) {
// val
self.drag._final = max - min + self.drag._position - maxCheck
// dispatch event
self.drag._instant = true
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
} else if (tr === first && (self.drag._direction > 0 || self.drag._position <= max)) {
self.drag._final = min - max + self.drag._position + maxCheck
// dispatch event
self.drag._instant = true
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
}
}
// fix absolute multi step activation (only if not already applied fix)
if (options.mode === 'absolute' && !self.drag._instant && self.direction) {
const diff = self.drag._initial - self.drag._position
if (Math.abs(diff) > maxCheck) {
// remainder add or remove maxCheck depending on direction
let remainder = diff % maxCheck
remainder += maxCheck * self.direction
// val
self.drag._final = self.drag._initial - remainder
// dispatch event
self.drag._instant = true
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
}
}
// val
self.drag._final = self.drag._initial
// 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.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
// fix keep self.drag._instant
self.drag._instant = isDrag
Xt.frame({
el: self.container,
ns: `${self.ns}isDrag`,
func: () => {
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 content = tr.children.length ? tr.children[0] : tr
const height = content.offsetHeight
groupHeight = height > groupHeight ? height : 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 })
}
}
//
// logic
//
/**
* 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._instant = true
self.drag._lock = 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 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 on the same slide as we started dragging
if (index !== self.drag._index || Math.abs(self.drag._distance) >= self.drag._size) {
// goToNum
self.goToNum({ index })
} else {
// 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)
) {
self.goToNext({ amount: 1 })
} else if (direction < 0 && self.drag._direction < 0 && (options.loop || self._wrap || index !== 0)) {
self.goToPrev({ amount: 1 })
} else {
self._logicDragreset()
}
}
} else {
self._logicDragreset()
}
// auto
self._eventAutostart()
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`dragend.${self._componentNs}`))
})
}
/**
* drag logic
* @param {Event} e
*/
_logicDrag(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
}
// check threshold
self.drag._distance = self.drag._start - self.drag._current
self.drag._distanceOther = self.drag._startOther - self.drag._currentOther
// check drag direction
if (Math.abs(self.drag._distanceOther) > Math.abs(self.drag._distance)) {
// prevent drag logic
return
} else {
// prevent page scroll
if (e.cancelable) {
e.preventDefault()
}
}
// only if dragging enough
self.drag._lock = self.drag._lock ? self.drag._lock : Math.abs(self.drag._distance) > options.drag.threshold
// disable interaction
if (self.drag._lock) {
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 (options.drag.overflow && !self._wrap && options.mode !== 'absolute') {
const direction = Math.sign(self.drag._distance)
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 + options.drag.overflow({ 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 - options.drag.overflow({ 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._ratioInverse = 1 - self.drag._ratio
// fix dragging furiously more than one
if (options.mode === 'absolute' && (self.drag._ratio > 1 || self.drag._ratio < -1)) {
return
}
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
// dispatch event
self.dragger.dispatchEvent(new CustomEvent(`drag.${self._componentNs}`))
// reset
self._inverse = null
// activation
if (options.mode !== 'absolute' && self.drag._lock) {
// 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 reset logic
*/
_logicDragreset() {
const self = this
// set direction
self.direction = self.direction * -1
self._inverse = self.direction < 0
// ratio
self.drag._ratio = self.drag._ratioInverse
self.drag._ratioInverse = 1 - self.drag._ratio
// val
self.drag._final = self.drag._initial
// dispatch event
self.drag._instant = false
self.dragger.dispatchEvent(new CustomEvent(`dragposition.${self._componentNs}`))
// 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) {
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) {
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
}
//
// status
//
/**
* disable
*/
enable() {
const self = this
const options = self.options
// enable
if (self.disabled) {
// grab
self.dragger.classList.add('xt-grab')
// hideDisable
if (options.hideDisable) {
const els = self.container.querySelectorAll(options.hideDisable)
for (const el of els) {
el.classList.remove('hidden')
}
}
}
// super after
super.enable()
}
/**
* disable
* @param {Object} params
* @param {Boolean} params.skipEvent Skip dispatch event
*/
disable({ skipEvent = false } = {}) {
const self = this
const options = self.options
// disable
if (!self.disabled) {
// clean
self._destroyGrab()
self._destroyIntraction()
// hideDisable
if (options.hideDisable) {
const els = self.container.querySelectorAll(options.hideDisable)
for (const el of els) {
el.classList.add('hidden')
}
}
}
// super after
super.disable({ skipEvent })
}
//
// util
//
/**
* destroy
* @param {Object} params
* @param {Boolean} params.weak Do not destroy component
*/
destroy({ weak = false } = {}) {
const self = this
// clean
self._destroyGrab()
self._destroyIntraction()
self._destroyNooverflow()
self._destroyDrag()
self._destroyAutoheight()
self._destroyPagination()
self._destroyWrap()
// super after
super.destroy({ weak })
}
/**
* destroy grab
*/
_destroyGrab() {
const self = this
// grab
if (self.dragger) {
self.dragger.classList.remove('xt-grab')
}
}
/**
* destroy intraction
*/
_destroyIntraction() {
const self = this
// disable interaction
for (const tr of self.targets) {
tr.classList.remove('pointer-events-none')
}
}
/**
* destroy drag
*/
_destroyDrag() {
const self = this
// drag position
self.dragger.style.transform = ''
}
/**
* destroy nooverflow
*/
_destroyNooverflow() {
const self = this
const options = self.options
// reset nooverflow
if (options.nooverflow) {
self.dragger.classList.remove(...options.nooverflow.split(' '))
self._disabledManual = false
}
}
/**
* destroy autoheight
*/
_destroyAutoheight() {
const self = this
// autoHeight
if (self._autoHeight || self._keepHeight) {
self._autoHeight.style.height = ''
}
}
/**
* destroy pagination
*/
_destroyPagination() {
const self = this
// clean pagination
if (self.pags && self.pags.length) {
for (const pag of self.pags) {
for (const oldPag of pag) {
oldPag.remove()
}
}
self.pags = []
}
}
/**
* destroy wrap
*/
_destroyWrap() {
const self = this
// clean wrap also if no wrap in options
for (const tr of self.targets) {
tr.style.transform = ''
}
}
//
}
//
// options
//
Slider.componentName = 'xt-slider'
Slider.optionsDefault = {
// slider
mode: 'relative',
group: false,
groupSame: true,
align: 'center',
contain: true,
wrap: false,
dragposition: false,
nooverflow: '!transform-none justify-center',
autoHeight: false,
keepHeight: false,
pagination: '[data-xt-slider-pagination]',
hideDisable: '[data-xt-slider-pagination], [data-xt-nav], [data-xt-slider-hide-disabled]',
drag: {
dragger: '[data-xt-slider-dragger]',
threshold: 15,
factor: 1,
overflow: ({ overflow }) => {
return Math.min(overflow, Math.log(1 + Math.pow(overflow, 10)))
},
},
// element
elementsTemplate: 'hidden',
elements: '[data-xt-slider-element]',
targets: '[data-xt-slider-target]',
elementsInner: ':scope > a, :scope > button',
targetsInner: false,
// quantity
min: 1,
max: 1,
// event
on: 'click',
off: 'click',
// timing
queue: false,
// other
visibleReinit: true,
jump: false,
navigation: '[data-xt-nav]',
keyboard: {
selector: '[data-xt-slider-dragger]',
},
zIndex: {
targets: {
start: 200, // same as options.zIndex.targets.start
factor: -1,
},
},
}
//
// export
//
Xt.Slider = Slider
//
// observe
//
if (typeof window !== 'undefined') {
Xt.mount({
matches: `[data-${Xt.Slider.componentName}]`,
mount: ({ ref }) => {
// vars
const optionsMarkup = ref.getAttribute(`data-${Xt.Slider.componentName}`)
const options = optionsMarkup ? RJSON.parse(optionsMarkup) : {}
// init
let self = new Xt.Slider(ref, options)
// unmount
return () => {
self.destroy()
self = null
}
},
})
}