flyonui
Version:
The easiest, free and open-source Tailwind CSS component library with semantic classes.
287 lines (232 loc) • 8.47 kB
text/typescript
/*
* HSPinInput
* @version: 3.2.2
* @author: Preline Labs Ltd.
* @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
* Copyright 2024 Preline Labs Ltd.
*/
import { dispatch } from '../../utils'
import { IPinInputOptions, IPinInput } from './interfaces'
import HSBasePlugin from '../base-plugin'
import { ICollectionItem } from '../../interfaces'
class HSPinInput extends HSBasePlugin<IPinInputOptions> implements IPinInput {
private items: NodeListOf<HTMLElement> | null
private currentItem: HTMLInputElement | null
private currentValue: string[] | null
private readonly placeholders: string[] | null
private readonly availableCharsRE: RegExp | null
private onElementInputListener:
| {
el: HTMLElement
fn: (evt: Event) => void
}[]
| null
private onElementPasteListener:
| {
el: HTMLElement
fn: (evt: Event) => void
}[]
| null
private onElementKeydownListener:
| {
el: HTMLElement
fn: (evt: Event) => void
}[]
| null
private onElementFocusinListener:
| {
el: HTMLElement
fn: () => void
}[]
| null
private onElementFocusoutListener:
| {
el: HTMLElement
fn: () => void
}[]
| null
private elementInput(evt: Event, index: number) {
this.onInput(evt, index)
}
private elementPaste(evt: ClipboardEvent) {
this.onPaste(evt)
}
private elementKeydown(evt: KeyboardEvent, index: number) {
this.onKeydown(evt, index)
}
private elementFocusin(index: number) {
this.onFocusIn(index)
}
private elementFocusout(index: number) {
this.onFocusOut(index)
}
constructor(el: HTMLElement, options?: IPinInputOptions) {
super(el, options)
const data = el.getAttribute('data-pin-input')
const dataOptions: IPinInputOptions = data ? JSON.parse(data) : {}
const concatOptions = {
...dataOptions,
...options
}
this.items = this.el.querySelectorAll('[data-pin-input-item]')
this.currentItem = null
this.currentValue = new Array(this.items.length).fill('')
this.placeholders = []
this.availableCharsRE = new RegExp(concatOptions?.availableCharsRE || '^[a-zA-Z0-9]+$') // '^[0-9]+$'
this.onElementInputListener = []
this.onElementPasteListener = []
this.onElementKeydownListener = []
this.onElementFocusinListener = []
this.onElementFocusoutListener = []
this.init()
}
private init() {
this.createCollection(window.$hsPinInputCollection, this)
if (this.items.length) this.build()
}
private build() {
this.buildInputItems()
}
private buildInputItems() {
this.items.forEach((el, index) => {
this.placeholders.push(el.getAttribute('placeholder') || '')
if (el.hasAttribute('autofocus')) this.onFocusIn(index)
this.onElementInputListener.push({
el,
fn: (evt: Event) => this.elementInput(evt, index)
})
this.onElementPasteListener.push({
el,
fn: (evt: ClipboardEvent) => this.elementPaste(evt)
})
this.onElementKeydownListener.push({
el,
fn: (evt: KeyboardEvent) => this.elementKeydown(evt, index)
})
this.onElementFocusinListener.push({
el,
fn: () => this.elementFocusin(index)
})
this.onElementFocusoutListener.push({
el,
fn: () => this.elementFocusout(index)
})
el.addEventListener('input', this.onElementInputListener.find(elI => elI.el === el).fn)
el.addEventListener('paste', this.onElementPasteListener.find(elI => elI.el === el).fn)
el.addEventListener('keydown', this.onElementKeydownListener.find(elI => elI.el === el).fn)
el.addEventListener('focusin', this.onElementFocusinListener.find(elI => elI.el === el).fn)
el.addEventListener('focusout', this.onElementFocusoutListener.find(elI => elI.el === el).fn)
})
}
private checkIfNumber(value: string) {
return value.match(this.availableCharsRE)
}
private autoFillAll(text: string) {
Array.from(text).forEach((n, i) => {
if (!this?.items[i]) return false
;(this.items[i] as HTMLInputElement).value = n
this.items[i].dispatchEvent(new Event('input', { bubbles: true }))
})
}
private setCurrentValue() {
this.currentValue = Array.from(this.items).map(el => (el as HTMLInputElement).value)
}
private toggleCompleted() {
if (!this.currentValue.includes('')) this.el.classList.add('active')
else this.el.classList.remove('active')
}
private onInput(evt: Event, index: number) {
const originalValue = (evt.target as HTMLInputElement).value
this.currentItem = evt.target as HTMLInputElement
this.currentItem.value = ''
this.currentItem.value = originalValue[originalValue.length - 1]
if (!this.checkIfNumber(this.currentItem.value)) {
this.currentItem.value = this.currentValue[index] || ''
return false
}
this.setCurrentValue()
if (this.currentItem.value) {
if (index < this.items.length - 1) this.items[index + 1].focus()
if (!this.currentValue.includes('')) {
const payload = { currentValue: this.currentValue }
this.fireEvent('completed', payload)
dispatch('completed.pinInput', this.el, payload)
}
this.toggleCompleted()
} else {
if (index > 0) this.items[index - 1].focus()
}
}
private onKeydown(evt: KeyboardEvent, index: number) {
if (evt.key === 'Backspace' && index > 0) {
if ((this.items[index] as HTMLInputElement).value === '') {
;(this.items[index - 1] as HTMLInputElement).value = ''
;(this.items[index - 1] as HTMLInputElement).focus()
} else {
;(this.items[index] as HTMLInputElement).value = ''
}
}
this.setCurrentValue()
this.toggleCompleted()
}
private onFocusIn(index: number) {
this.items[index].setAttribute('placeholder', '')
}
private onFocusOut(index: number) {
this.items[index].setAttribute('placeholder', this.placeholders[index])
}
private onPaste(evt: ClipboardEvent) {
evt.preventDefault()
this.items.forEach(el => {
if (document.activeElement === el) this.autoFillAll(evt.clipboardData.getData('text'))
})
}
// Public methods
public destroy() {
// Remove classes
this.el.classList.remove('active')
// Remove listeners
if (this.items.length)
this.items.forEach(el => {
el.removeEventListener('input', this.onElementInputListener.find(elI => elI.el === el).fn)
el.removeEventListener('paste', this.onElementPasteListener.find(elI => elI.el === el).fn)
el.removeEventListener('keydown', this.onElementKeydownListener.find(elI => elI.el === el).fn)
el.removeEventListener('focusin', this.onElementFocusinListener.find(elI => elI.el === el).fn)
el.removeEventListener('focusout', this.onElementFocusoutListener.find(elI => elI.el === el).fn)
})
this.items = null
this.currentItem = null
this.currentValue = null
window.$hsPinInputCollection = window.$hsPinInputCollection.filter(({ element }) => element.el !== this.el)
}
// Static method
static getInstance(target: HTMLElement | string, isInstance?: boolean) {
const elInCollection = window.$hsPinInputCollection.find(
el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target)
)
return elInCollection ? (isInstance ? elInCollection : elInCollection.element) : null
}
static autoInit() {
if (!window.$hsPinInputCollection) window.$hsPinInputCollection = []
if (window.$hsPinInputCollection)
window.$hsPinInputCollection = window.$hsPinInputCollection.filter(({ element }) => document.contains(element.el))
document.querySelectorAll('[data-pin-input]:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => {
if (!window.$hsPinInputCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) new HSPinInput(el)
})
}
}
declare global {
interface Window {
HSPinInput: Function
$hsPinInputCollection: ICollectionItem<HSPinInput>[]
}
}
window.addEventListener('load', () => {
HSPinInput.autoInit()
// Uncomment for debug
// console.log('PIN input collection:', window.$hsPinInputCollection);
})
if (typeof window !== 'undefined') {
window.HSPinInput = HSPinInput
}
export default HSPinInput