Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,35 @@
},
{
"path": "./dist/css/bootstrap.css",
"maxSize": "35.75 kB"
"maxSize": "36.0 kB"
},
{
"path": "./dist/css/bootstrap.min.css",
"maxSize": "32.25 kB"
"maxSize": "32.5 kB"
},
{
"path": "./dist/js/bootstrap.bundle.js",
"maxSize": "48.5 kB"
"maxSize": "49.75 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
"maxSize": "25.25 kB"
"maxSize": "26.0 kB"
},
{
"path": "./dist/js/bootstrap.esm.js",
"maxSize": "34.75 kB"
"maxSize": "36.0 kB"
},
{
"path": "./dist/js/bootstrap.esm.min.js",
"maxSize": "21.0 kB"
"maxSize": "22.25 kB"
},
{
"path": "./dist/js/bootstrap.js",
"maxSize": "35.25 kB"
"maxSize": "36.5 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
"maxSize": "19.0 kB"
"maxSize": "19.75 kB"
}
],
"ci": {
Expand Down
1 change: 1 addition & 0 deletions js/index.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { default as Dialog } from './src/dialog.js'
export { default as Dropdown } from './src/dropdown.js'
export { default as Offcanvas } from './src/offcanvas.js'
export { default as Strength } from './src/strength.js'
export { default as OtpInput } from './src/otp-input.js'
export { default as Popover } from './src/popover.js'
export { default as ScrollSpy } from './src/scrollspy.js'
export { default as Tab } from './src/tab.js'
Expand Down
2 changes: 2 additions & 0 deletions js/index.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Dialog from './src/dialog.js'
import Dropdown from './src/dropdown.js'
import Offcanvas from './src/offcanvas.js'
import Strength from './src/strength.js'
import OtpInput from './src/otp-input.js'
import Popover from './src/popover.js'
import ScrollSpy from './src/scrollspy.js'
import Tab from './src/tab.js'
Expand All @@ -29,6 +30,7 @@ export default {
Dropdown,
Offcanvas,
Strength,
OtpInput,
Popover,
ScrollSpy,
Tab,
Expand Down
250 changes: 250 additions & 0 deletions js/src/otp-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* --------------------------------------------------------------------------
* Bootstrap otp-input.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/

import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'

/**
* Constants
*/

const NAME = 'otpInput'
const DATA_KEY = 'bs.otp-input'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'

const EVENT_COMPLETE = `complete${EVENT_KEY}`
const EVENT_INPUT = `input${EVENT_KEY}`

const SELECTOR_DATA_OTP = '[data-bs-otp]'
const SELECTOR_INPUT = 'input'

const Default = {
length: 6,
mask: false
}

const DefaultType = {
length: 'number',
mask: 'boolean'
}

/**
* Class definition
*/

class OtpInput extends BaseComponent {
constructor(element, config) {
super(element, config)

this._inputs = SelectorEngine.find(SELECTOR_INPUT, this._element)
this._setupInputs()
this._addEventListeners()
}

// Getters
static get Default() {
return Default
}

static get DefaultType() {
return DefaultType
}

static get NAME() {
return NAME
}

// Public
getValue() {
return this._inputs.map(input => input.value).join('')
}

setValue(value) {
const chars = String(value).split('')
for (const [index, input] of this._inputs.entries()) {
input.value = chars[index] || ''
}

this._checkComplete()
}

clear() {
for (const input of this._inputs) {
input.value = ''
}

this._inputs[0]?.focus()
}

focus() {
// Focus first empty input, or last input if all filled
const emptyInput = this._inputs.find(input => !input.value)
if (emptyInput) {
emptyInput.focus()
} else {
this._inputs.at(-1)?.focus()
}
}

// Private
_setupInputs() {
for (const input of this._inputs) {
// Set attributes for proper OTP handling
input.setAttribute('maxlength', '1')
input.setAttribute('inputmode', 'numeric')
input.setAttribute('pattern', '\\d*')

// First input gets autocomplete for browser OTP autofill
if (input === this._inputs[0]) {
input.setAttribute('autocomplete', 'one-time-code')
} else {
input.setAttribute('autocomplete', 'off')
}

// Mask input if configured
if (this._config.mask) {
input.setAttribute('type', 'password')
}
}
}

_addEventListeners() {
for (const [index, input] of this._inputs.entries()) {
EventHandler.on(input, 'input', event => this._handleInput(event, index))
EventHandler.on(input, 'keydown', event => this._handleKeydown(event, index))
EventHandler.on(input, 'paste', event => this._handlePaste(event))
EventHandler.on(input, 'focus', event => this._handleFocus(event))
}
}

_handleInput(event, index) {
const input = event.target

// Only allow digits
if (!/^\d*$/.test(input.value)) {
input.value = input.value.replace(/\D/g, '')
}

const { value } = input

// Handle multi-character input (some browsers/autofill)
if (value.length > 1) {
// Distribute characters across inputs
const chars = value.split('')
input.value = chars[0] || ''

for (let i = 1; i < chars.length && index + i < this._inputs.length; i++) {
this._inputs[index + i].value = chars[i]
}

// Focus appropriate input
const nextIndex = Math.min(index + chars.length, this._inputs.length - 1)
this._inputs[nextIndex].focus()
} else if (value && index < this._inputs.length - 1) {
// Auto-advance to next input
this._inputs[index + 1].focus()
}

EventHandler.trigger(this._element, EVENT_INPUT, {
value: this.getValue(),
index
})

this._checkComplete()
}

_handleKeydown(event, index) {
const { key } = event

switch (key) {
case 'Backspace': {
if (!this._inputs[index].value && index > 0) {
// Move to previous input and clear it
event.preventDefault()
this._inputs[index - 1].value = ''
this._inputs[index - 1].focus()
}

break
}

case 'Delete': {
// Clear current and shift remaining values left
event.preventDefault()
for (let i = index; i < this._inputs.length - 1; i++) {
this._inputs[i].value = this._inputs[i + 1].value
}

this._inputs.at(-1).value = ''
break
}

case 'ArrowLeft': {
if (index > 0) {
event.preventDefault()
this._inputs[index - 1].focus()
}

break
}

case 'ArrowRight': {
if (index < this._inputs.length - 1) {
event.preventDefault()
this._inputs[index + 1].focus()
}

break
}

// No default
}
}

_handlePaste(event) {
event.preventDefault()
const pastedData = (event.clipboardData || window.clipboardData).getData('text')
const digits = pastedData.replace(/\D/g, '').slice(0, this._inputs.length)

if (digits) {
this.setValue(digits)

// Focus last filled input or last input
const lastIndex = Math.min(digits.length, this._inputs.length) - 1
this._inputs[lastIndex].focus()
}
}

_handleFocus(event) {
// Select the content on focus for easy replacement
event.target.select()
}

_checkComplete() {
const value = this.getValue()
const isComplete = value.length === this._inputs.length &&
this._inputs.every(input => input.value !== '')

if (isComplete) {
EventHandler.trigger(this._element, EVENT_COMPLETE, { value })
}
}
}

/**
* Data API implementation
*/

EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => {
for (const element of SelectorEngine.find(SELECTOR_DATA_OTP)) {
OtpInput.getOrCreateInstance(element)
}
})

export default OtpInput
Loading