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.
426 lines (404 loc) • 11.2 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'
/**
* GroupnumberInit
*/
export class GroupnumberInit {
//
// init
//
/**
* init
*/
_init() {
const self = this
// init
self._initVars()
self._initLogic()
}
/**
* init vars
*/
_initVars() {
const self = this
// options
self._optionsDefault = Xt.merge([self.constructor.optionsDefault, Xt.options[self.componentName]])
self._optionsInitial = self.options = Xt.merge([self._optionsDefault, self._optionsCustom])
}
/**
* init logic
*/
_initLogic() {
const self = this
const options = self.options
// namespace
self.ns = self.ns ?? Xt.uniqueId()
// enable first for proper initial activation
self.enable()
// matches
Xt._initMatches({ self, optionsInitial: self._optionsInitial })
// vars
self.initial = true
// elements
self.inputs = self.container.querySelectorAll(options.inputs)
self.steps = self.container.querySelectorAll(options.steps)
// voidable
for (const input of self.inputs) {
Xt.dataStorage.set(input, `voidable/${self.ns}`, input.value === '')
}
// steps
const eventsSteps = options.events?.steps ? options.events.steps.split(' ') : []
if (eventsSteps.length) {
for (const step of self.steps) {
const stepHandler = Xt.dataStorage.put(
step,
`${options.events.steps}/${self.ns}`,
self._eventChange.bind(self, { button: step }),
)
for (const event of eventsSteps) {
step.addEventListener(event, stepHandler)
}
}
}
// inputs
const eventsInput = options.events?.input ? options.events.input.split(' ') : []
if (eventsInput.length) {
for (const input of self.inputs) {
const inputHandler = Xt.dataStorage.put(
input,
`${options.events.input}/${self.ns}`,
self._eventChange.bind(self, {}),
)
for (const event of eventsInput) {
input.addEventListener(event, inputHandler)
}
}
}
// ally
self._initA11y()
// initial
self._initStart()
// setup
// dispatch event
self.container.dispatchEvent(new CustomEvent(`setup.${self._componentNs}`))
// needs frameDouble after ondone
Xt.frameDouble({
el: self.container,
ns: `${self.ns}Init`,
func: () => {
// initialized class
self.container.setAttribute(`data-${self.componentName}-init`, '')
// dispatch event
self.container.dispatchEvent(new CustomEvent(`init.${self._componentNs}`))
self.initial = false
// debug
if (options.debug) {
// eslint-disable-next-line no-console
console.debug(`${self.componentName} init`, self)
}
},
})
// disable last for proper options.disableDeactivate
if (self.options.disabled) {
self.disable()
}
}
/**
* init start
*/
_initStart() {
const self = this
// disabled
if (self.disabled) {
return
}
// logic
self._eventChange.bind(self)()
}
//
// methods
//
/**
* change
* @param {Object} params
* @param {Node|HTMLElement|EventTarget|Window} params.button
* @param {Event} e
*/
_eventChange({ button } = {}, e) {
const self = this
const options = self.options
// disabled
if (self.disabled) {
return
}
// trigger external events and skip internal events
if (!e?.detail?.skip) {
if (e) {
e.preventDefault()
// prevent same listener because we retrigger 'change' after
e.stopImmediatePropagation()
}
// inputs
for (const input of self.inputs) {
const inputStep = input.getAttribute('step')
const buttonStep = button?.getAttribute('data-xt-step')
const step =
inputStep && inputStep !== ''
? parseFloat(inputStep) * Math.sign(parseFloat(buttonStep))
: (parseFloat(buttonStep) ?? 1)
let val = parseFloat(input.value)
val = isNaN(val) ? 0 : val
if (step) {
val += step
// remove floating point up to step
val = parseFloat(val.toFixed(self._countDecimals({ num: step })))
}
self._validate({ val, input })
// aria live
if (!self.initial) {
self._initA11yLive({ input })
}
}
// disabled
if (options.limit) {
for (const button of self.steps) {
const disabled = Xt.dataStorage.get(button, `${self.ns}ButtonDisabled`)
if (disabled) {
button.setAttribute('disabled', 'disabled')
} else {
button.removeAttribute('disabled')
}
Xt.dataStorage.remove(button, `${self.ns}ButtonDisabled`)
}
}
}
}
/**
* validate
* @param {Object} params
* @param {Number} params.val
* @param {Node|HTMLElement|EventTarget|Window} params.input
*/
_validate({ val, input } = {}) {
const self = this
const options = self.options
// step
const inputStep = input.getAttribute('step')
const step = inputStep && inputStep !== '' ? parseFloat(inputStep) : null
if (options.validate) {
val = options.validate({ val, step })
}
// limit
if (options.limit) {
const voidable = Xt.dataStorage.get(input, `voidable/${self.ns}`)
const inputMin = input.getAttribute('min')
const inputMax = input.getAttribute('max')
const min = inputMin && inputMin !== '' ? parseFloat(inputMin) : options.min
const max = inputMax && inputMax !== '' ? parseFloat(inputMax) : options.max
for (const button of self.steps) {
const buttonStep = button.getAttribute('data-xt-step')
let voidMin
let voidMax
if (voidable) {
if (val === 0) {
val = ''
}
if (min >= 0) {
voidMin = true
if (val < min) {
val = ''
}
}
if (max <= 0) {
voidMax = true
if (val > max) {
val = ''
}
}
}
if (buttonStep < 0) {
if (val < min || (!voidMin && val === min) || (voidMin && val === '')) {
val = val !== '' ? min : val
Xt.dataStorage.set(button, `${self.ns}ButtonDisabled`, true)
}
} else if (buttonStep > 0) {
if (val > max || (!voidMax && val === max) || (voidMax && val === '')) {
val = val !== '' ? max : val
Xt.dataStorage.set(button, `${self.ns}ButtonDisabled`, true)
}
}
}
}
// set value
input.value = val
// trigger external events and skip internal events
if (!self.initial) {
input.dispatchEvent(new Event('input', { detail: { skip: true } })) // needed to change value (e.g. x-model)
input.dispatchEvent(new CustomEvent('change', { detail: { skip: true } }))
}
}
//
// status
//
/**
* enable
*/
enable() {
const self = this
if (self.disabled) {
// enable
self.disabled = false
// dispatch event
self.container.dispatchEvent(new CustomEvent(`status.${self._componentNs}`))
}
}
/**
* disable
* @param {Object} params
* @param {Boolean} params.skipEvent Skip dispatch event
*/
disable({ skipEvent = false } = {}) {
const self = this
if (!self.disabled) {
// disable
self.disabled = true
// dispatch event
if (!skipEvent) {
self.container.dispatchEvent(new CustomEvent(`status.${self._componentNs}`))
}
}
}
//
// a11y
//
/**
* init a11y
*/
_initA11y() {
const self = this
const options = self.options
// a11y
if (options.a11y) {
// init
self._initA11yId()
self._initA11ySetup()
}
}
/**
* init a11y id
* @param {Object} params
*/
_initA11yId() {
const self = this
const options = self.options
// id
if (options.a11y.controls) {
for (const input of self.inputs) {
const id = input.getAttribute('id')
if (!id) {
input.setAttribute('id', Xt.uniqueId())
}
}
}
}
/**
* init a11y setup
*/
_initA11ySetup() {
const self = this
const options = self.options
// aria-controls
if (options.a11y.controls) {
let str = ''
for (const input of self.inputs) {
str += `${input.getAttribute('id')} `
}
for (const step of self.steps) {
step.setAttribute('aria-controls', str.trim())
}
}
}
/**
* init a11y live
*/
_initA11yLive({ input }) {
const self = this
const options = self.options
// aria-controls
if (options.a11y?.live) {
self.container.querySelector('[aria-live]')?.remove()
clearTimeout(self.container.dataset.liveTimeout)
self.container.dataset.liveTimeout = setTimeout(() => {
const ariaLive = self.container.appendChild(
Xt.node({ str: `<span class="sr-only" aria-live="${options.a11y.live}" aria-atomic="true"></span>` }),
)
self.container.dataset.liveTimeout = setTimeout(() => {
const ariaLivePrepend = input.getAttribute('aria-label') ?? ''
if (ariaLivePrepend) {
ariaLive.innerHTML = `${ariaLivePrepend} ${input.value}`
} else {
ariaLive.innerHTML = input.value
}
}, 500)
}, 500)
}
}
//
// util
//
/**
* countDecimals
* @param {Object} params
* @param {Boolean} params.num
*/
_countDecimals({ num } = {}) {
if (Math.floor(num) === num) return 0
return num.toString().split('.')[1].length || 0
}
/**
* reinit
*/
reinit() {
const self = this
// reinit
self._initLogic()
}
/**
* destroy
*/
destroy() {
const self = this
const options = self.options
// remove events
const eventsSteps = options.events?.steps ? options.events.steps.split(' ') : []
if (eventsSteps.length) {
for (const step of self.steps) {
const stepHandler = Xt.dataStorage.get(step, `${options.events.steps}/${self.ns}`)
for (const event of eventsSteps) {
step.removeEventListener(event, stepHandler)
}
}
}
const eventsInput = options.events?.input ? options.events.input.split(' ') : []
if (eventsInput.length) {
for (const input of self.inputs) {
const inputHandler = Xt.dataStorage.get(input, `${options.events.input}/${self.ns}`)
for (const event of eventsInput) {
input.removeEventListener(event, inputHandler)
}
}
}
// initialized class
self.container.removeAttribute(`data-${self.componentName}-init`)
// set self
Xt._remove({ name: self.componentName, el: self.container })
// dispatch event
self.container.dispatchEvent(new CustomEvent(`destroy.${self._componentNs}`))
// delete
delete this
}
}