Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of autocomplete values in X-Mask #4260

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions packages/mask/builds/module.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import mask, { stripDown } from '../src/index.js'
import mask, { formatInput } from '../src/index.js'

export default mask

export { mask, stripDown }
export { mask, formatInput }
100 changes: 31 additions & 69 deletions packages/mask/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export default function (Alpine) {
evaluator(value => {
result = typeof value === 'function' ? value(input) : value
}, { scope: {
// These are "magics" we'll make available to the x-mask:function:
'$input': input,
'$money': formatMoney.bind({ el }),
}})
// These are "magics" we'll make available to the x-mask:function:
'$input': input,
'$money': formatMoney.bind({ el }),
}})
})

return result
Expand Down Expand Up @@ -74,7 +74,7 @@ export default function (Alpine) {
}

let setInput = () => {
lastInputValue = el.value = formatInput(input, template)
lastInputValue = el.value = formatInput(template,input)
}

if (shouldRestoreCursor) {
Expand All @@ -88,16 +88,6 @@ export default function (Alpine) {
setInput()
}
}

function formatInput(input, template) {
// Let empty inputs be empty inputs.
if (input === '') return ''

let strippedDownInput = stripDown(template, input)
let rebuiltInput = buildUp(template, strippedDownInput)

return rebuiltInput
}
}).before('model')
}

Expand All @@ -109,74 +99,46 @@ export function restoreCursorPosition(el, template, callback) {

let beforeLeftOfCursorBeforeFormatting = unformattedValue.slice(0, cursorPosition)

let newPosition = buildUp(
template, stripDown(
let newPosition = formatInput(
template, beforeLeftOfCursorBeforeFormatting
)
).length

el.setSelectionRange(newPosition, newPosition)
}

export function stripDown(template, input) {
let inputToBeStripped = input
let output = ''
let regexes = {
'9': /[0-9]/,
'a': /[a-zA-Z]/,
'*': /[a-zA-Z0-9]/,
}

let wildcardTemplate = ''

// Strip away non wildcard template characters.
for (let i = 0; i < template.length; i++) {
if (['9', 'a', '*'].includes(template[i])) {
wildcardTemplate += template[i]
continue;
}

for (let j = 0; j < inputToBeStripped.length; j++) {
if (inputToBeStripped[j] === template[i]) {
inputToBeStripped = inputToBeStripped.slice(0, j) + inputToBeStripped.slice(j+1)
let regexes = {
'9': /[0-9]/,
'a': /[a-zA-Z]/,
'*': /[a-zA-Z0-9]/,
}

break;
}
}
}
export function formatInput(template, input) {
let templateMark = 0
let inputMark = 0
let output = ''

for (let i = 0; i < wildcardTemplate.length; i++) {
let found = false
// Walk the template and input chars simultaneously one by one...
while (templateMark < template.length && inputMark < input.length) {
let templateChar = template[templateMark]
let inputChar = input[inputMark]

for (let j = 0; j < inputToBeStripped.length; j++) {
if (regexes[wildcardTemplate[i]].test(inputToBeStripped[j])) {
output += inputToBeStripped[j]
inputToBeStripped = inputToBeStripped.slice(0, j) + inputToBeStripped.slice(j+1)
// We've encountered a template placeholder...
if (templateChar in regexes) {
// If the input is "allowed" based on the placeholder...
if (regexes[templateChar].test(inputChar)) {
output += inputChar

found = true
break;
templateMark++
}
}

if (! found) break;
}
inputMark++
} else { // We've encountered a template literal...
output += templateChar

return output
}

export function buildUp(template, input) {
let clean = Array.from(input)
let output = ''
templateMark++

for (let i = 0; i < template.length; i++) {
if (! ['9', 'a', '*'].includes(template[i])) {
output += template[i]
continue;
if (templateChar === input[inputMark]) inputMark++
}

if (clean.length === 0) break;

output += clean.shift()
}

return output
Expand Down Expand Up @@ -211,7 +173,7 @@ export function formatMoney(input, delimiter = '.', thousands, precision = 2) {

let minus = input.startsWith('-') ? '-' : ''
let strippedInput = input.replaceAll(new RegExp(`[^0-9\\${delimiter}]`, 'g'), '')
let template = Array.from({ length: strippedInput.split(delimiter)[0].length }).fill('9').join('')
let template = Array.from({length: strippedInput.split(delimiter)[0].length}).fill('9').join('')

template = `${minus}${addThousands(template, thousands)}`

Expand Down
36 changes: 30 additions & 6 deletions tests/cypress/integration/plugins/mask.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ test('x-mask',
({ get }) => {
// Type a phone number:
get('input').type('12').should(haveValue('(12'))
get('input').type('3').should(haveValue('(123) '))
get('input').type('3 ').should(haveValue('(123) '))
get('input').type('4567890').should(haveValue('(123) 456-7890'))
// Clear it & paste formatted version in:
get('input').type('{selectAll}{backspace}')
Expand All @@ -32,6 +32,30 @@ test('x-mask',
},
)

test('x-mask autocomplete',
[html`<input x-data x-mask="+1 (999) 999-9999">`],
({ get }) => {
// Type a phone number:
get('input').type('21').should(haveValue('+1 (21'))
get('input').type('3 ').should(haveValue('+1 (213) '))
get('input').type('4567890').should(haveValue('+1 (213) 456-7890'))
// Clear it & paste formatted version in:
get('input').type('{selectAll}{backspace}')
get('input').invoke('val', '+1 (213) 456-7890').trigger('blur')
get('input').should(haveValue('+1 (213) 456-7890'))
// Clear it & paste un-formatted version in:
get('input').type('{selectAll}{backspace}')
get('input').invoke('val', '2134567890').trigger('blur')
get('input').should(haveValue('+1 (213) 456-7890'))
// Clear it and start with an area code starting with 1:
get('input').type('{selectAll}{backspace}')
get('input').type('1 ').should(haveValue('+1 '))
get('input').type('2').should(haveValue('+1 (2'))
get('input').type('13 ').should(haveValue('+1 (213) '))
get('input').type('456 78-90').should(haveValue('+1 (213) 456-7890'))
},
)

test('x-mask with x-model',
[html`
<div x-data="{ value: '' }">
Expand All @@ -43,7 +67,7 @@ test('x-mask with x-model',
// Type a phone number:
get('#1').type('12').should(haveValue('(12'))
get('#2').should(haveValue('(12'))
get('#1').type('3').should(haveValue('(123) '))
get('#1').type('3 ').should(haveValue('(123) '))
get('#2').should(haveValue('(123) '))
get('#1').type('4567890').should(haveValue('(123) 456-7890'))
get('#2').should(haveValue('(123) 456-7890'))
Expand Down Expand Up @@ -119,15 +143,15 @@ test('x-mask with non wildcard alpha-numeric characters (b)',
get('input').type('a').should(haveValue('ba'))
get('input').type('a').should(haveValue('ba'))
get('input').type('3').should(haveValue('ba3'))
get('input').type('z').should(haveValue('ba3zb'))
get('input').type('{backspace}{backspace}4').should(haveValue('ba34b'))
get('input').type('z ').should(haveValue('ba3zb'))
get('input').type('{backspace}{backspace}4 ').should(haveValue('ba34b'))
}
)

test('x-mask:dynamic',
[html`<input x-data x-mask:dynamic="'(999)'">`],
({ get }) => {
get('input').type('123').should(haveValue('(123)'))
get('input').type('123 ').should(haveValue('(123)'))
}
)

Expand All @@ -146,7 +170,7 @@ test('$money',
get('input').type('{leftArrow}7').should(haveValue('1,234,567.87'))
get('input').type('{leftArrow}{leftArrow}{leftArrow}89').should(haveValue('123,456,789.87'))
get('input').type('{leftArrow}{leftArrow}{leftArrow}{leftArrow}12').should(haveValue('12,345,612,789.87'))
get('input').type('{leftArrow}3').should(haveValue('123,456,123,789.87'))
get('input').type('3').should(haveValue('123,456,123,789.87'))
// Clear it & paste formatted version in:
get('input').type('{selectAll}{backspace}')
get('input').invoke('val', '123,456,132,789.87').trigger('blur')
Expand Down
78 changes: 39 additions & 39 deletions tests/jest/mask.spec.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
let { stripDown, formatMoney } = require('../../packages/mask/dist/module.cjs');
let { formatInput } = require('../../packages/mask/dist/module.cjs');

test('strip-down functionality', async () => {
expect(stripDown('(***) ***-****', '7162256108')).toEqual('7162256108')
expect(stripDown('(999) 999-9999', '7162256108')).toEqual('7162256108')
expect(stripDown('999) 999-9999', '7162256108')).toEqual('7162256108')
expect(stripDown('999 999-9999', '7162256108')).toEqual('7162256108')
expect(stripDown('999999-9999', '7162256108')).toEqual('7162256108')
expect(stripDown('9999999999', '7162256108')).toEqual('7162256108')
expect(stripDown('9999999999', '7162256108')).toEqual('7162256108')
expect(stripDown('(999) 999-9999', '716 2256108')).toEqual('7162256108')
expect(stripDown('(999) 999-9999', '(716) 2256108')).toEqual('7162256108')
expect(stripDown('(999) 999-9999', '(716) 2-25--6108')).toEqual('7162256108')
})
test('format-input functionality', async () => {

test('formatMoney functionality', async () => {
// Default arguments implicit and explicit
expect(formatMoney('123456')).toEqual('123,456');
expect(formatMoney('9900900')).toEqual('9,900,900');
expect(formatMoney('5600.40')).toEqual('5,600.40');
expect(formatMoney('123456', '.')).toEqual('123,456');
expect(formatMoney('9900900', '.')).toEqual('9,900,900');
expect(formatMoney('5600.40', '.')).toEqual('5,600.40');
expect(formatMoney('123456', '.', ',')).toEqual('123,456');
expect(formatMoney('9900900', '.', ',')).toEqual('9,900,900');
expect(formatMoney('5600.40', '.', ',')).toEqual('5,600.40');
expect(formatInput('(***) ***-****', '7162256108')).toEqual('(716) 225-6108')
expect(formatInput('(999) 999-9999', '7162256108')).toEqual('(716) 225-6108')
expect(formatInput('999) 999-9999', '7162256108')).toEqual('716) 225-6108')
expect(formatInput('999 999-9999', '7162256108')).toEqual('716 225-6108')
expect(formatInput('999999-9999', '7162256108')).toEqual('716225-6108')
expect(formatInput('9999999999', '7162256108')).toEqual('7162256108')
expect(formatInput('(999) 999-9999', '716 2256108')).toEqual('(716) 225-6108')
expect(formatInput('(999) 999-9999', '(716) 2256108')).toEqual('(716) 225-6108')
expect(formatInput('(999) 999-9999', '(716) 2-25--6108')).toEqual('(716) 225-6108')
expect(formatInput('+1 (999) 999-9999', '7162256108')).toEqual('+1 (716) 225-6108')
expect(formatInput('+1 (999) 999-9999', '+1 (716) 225-6108')).toEqual('+1 (716) 225-6108')
expect(formatInput('ABC (999) 999-9999', '7162256108')).toEqual('ABC (716) 225-6108')
expect(formatInput('ABC (999) 999-9999', 'ABC (716) 225-6108')).toEqual('ABC (716) 225-6108')
expect(formatInput('999.999.9999', '7162256108')).toEqual('716.225.6108')
expect(formatInput('999.999.9999', '716.2256108')).toEqual('716.225.6108')
expect(formatInput('999.999.9999', '716.225 6108')).toEqual('716.225.6108')

// Switch decimal separator
expect(formatMoney('123456', ',')).toEqual('123.456');
expect(formatMoney('9900900', ',')).toEqual('9.900.900');
expect(formatMoney('5600.40', ',')).toEqual('5.600,40');
expect(formatMoney('123456', '/')).toEqual('123.456');
expect(formatMoney('9900900', '/')).toEqual('9.900.900');
expect(formatMoney('5600.40', '/')).toEqual('5.600/40');
expect(formatInput('aaaa aaaa aaaa aaaa', 'abcd9abcd9abcd9abcd9')).toEqual('abcd abcd abcd abcd')
expect(formatInput('aaaa aaaa aaaa aaaa', 'abcd abcd abcd abcd')).toEqual('abcd abcd abcd abcd')
expect(formatInput('aaaa aaaa aaaa aaaa', 'abcdabcdabcdabcd')).toEqual('abcd abcd abcd abcd')
expect(formatInput('aaaa aaaa aaaa aaaa', 'abcd9abcd9abcd9abc')).toEqual('abcd abcd abcd abc')
expect(formatInput('### aaaa aaaa aaaa aaaa', 'abcd abcd abcd abcd')).toEqual('### abcd abcd abcd abcd')
expect(formatInput('### aaaa aaaa aaaa aaaa', '### abcd abcd abcd abcd')).toEqual('### abcd abcd abcd abcd')
expect(formatInput('### aaaa aaaa aaaa aaaa', '### abcd abcd #### abcd')).toEqual('### abcd abcd abcd')

// Switch thousands separator
expect(formatMoney('123456', '.', ' ')).toEqual('123 456');
expect(formatMoney('9900900', '.', ' ')).toEqual('9 900 900');
expect(formatMoney('5600.40', '.', ' ')).toEqual('5 600.40');
expect(formatInput('#### #### #### 9999', '1234 5678 9101 2345')).toEqual('#### #### #### 1234')

expect(formatInput('ba9*b', 'a')).toEqual('ba')
expect(formatInput('ba9*b', 'aa')).toEqual('ba')
expect(formatInput('ba9*b', 'aa3')).toEqual('ba3')
expect(formatInput('ba9*b', 'aa3z')).toEqual('ba3z')
expect(formatInput('ba9*b', 'aa3z4')).toEqual('ba3zb')

expect(formatInput('a', 'a9a')).toEqual('a')
expect(formatInput('aa', 'b9a')).toEqual('ba')
expect(formatInput('aa', 'bb')).toEqual('bb')
expect(formatInput('aab', 'aba')).toEqual('abb')
expect(formatInput('abb', 'ab')).toEqual('ab')
expect(formatInput('abb', 'ab9')).toEqual('abb')

// Switch decimal and thousands separator
expect(formatMoney('123456', '#', ' ')).toEqual('123 456');
expect(formatMoney('9900900', '#', ' ')).toEqual('9 900 900');
expect(formatMoney('5600.40', '#', ' ')).toEqual('5 600#40');
});
Loading