Skip to content

Commit 1ce86ca

Browse files
committed
feat(alternations): add support for alternations / pipe
Use a pipe character to indicate alternative static characters ie: A|B-### BREAKING CHANGE: The pipe character must be escaped if used as a static string.
1 parent 577d264 commit 1ce86ca

File tree

5 files changed

+90
-5
lines changed

5 files changed

+90
-5
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default {
6565
+ `\` = escape any of the above characters
6666
+ `?` = mark the preceding character as optional [0 or 1]
6767
+ `*` = mark the preceding character as optional & repeating [0 or more]
68+
+ `|` = used for alternative static characters: A|B|C will accept A or B or C
6869

6970
See the [token source file](https://github.com/RonaldJerez/vue-input-facade/blob/master/src/tokens.js) for definition signature
7071

docs/component.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ let masked = true
5656
<display :value="value" />
5757
```
5858

59+
### Alternation (Pipe)
60+
61+
Use a pipe symbol to indicate altarnative **static** values that can be used in the mask. This is case insensitive and can match letters irregarless of accents. For example å = A. Android webview and Opera dont fully support that type of matching.
62+
> *Note that because this only works with static values there is no need to escape characters that are also used as tokens.*
63+
64+
```js
65+
let value = ''
66+
let masked = true
67+
68+
<example label="ID Code">
69+
<input-facade mask="A|B|C-####" v-model="value" :masked="masked" />
70+
</example>
71+
72+
<checkbox v-model="masked" />
73+
<display :value="value" />
74+
```
75+
5976
### Dynamic Masks
6077

6178
Accepts an array of masking pattern and dynamically chooses the appropriate one based on the number of characters in the field.

src/masker.js

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import defaultTokens from './tokens'
33

44
let tokenDefinitions = defaultTokens
55

6+
let isLocaleCompareSupported = false
7+
try {
8+
// if supported this will throw a RangeError because 'i' is not a valid locale
9+
'a'.localeCompare('b', 'i')
10+
} catch (e) {
11+
isLocaleCompareSupported = e.name === 'RangeError'
12+
}
13+
614
/**
715
* Overrides the default global token definitions
816
*
@@ -81,7 +89,13 @@ export function formatter(value, config) {
8189
return {
8290
escape: !!masker?.escape,
8391
optional: !!nextMasker?.optional,
84-
repeat: !!nextMasker?.repeat
92+
repeat: !!nextMasker?.repeat,
93+
...(nextMasker?.pipe && {
94+
pipe: mask
95+
.substring(maskIndex)
96+
.match(/^(.\|)+./g)[0]
97+
.split('|')
98+
})
8599
}
86100
}
87101

@@ -90,9 +104,9 @@ export function formatter(value, config) {
90104
const masker = tokens[maskChar]
91105
let char = value[valueIndex]
92106

93-
if (masker && !escaped) {
94-
const meta = getMetaData(masker)
107+
const meta = getMetaData(masker)
95108

109+
if (masker && !escaped && !meta.pipe) {
96110
// when is escape char, do not mask, just continue
97111
if (meta.escape) {
98112
escaped = true
@@ -118,10 +132,24 @@ export function formatter(value, config) {
118132
continue
119133
}
120134

135+
valueIndex++
136+
} else if (meta.pipe) {
137+
if (!char) break
138+
139+
const pipeMatch = meta.pipe.find(looselyStringMatch.bind(null, char))
140+
141+
if (pipeMatch) {
142+
output.unmasked += pipeMatch
143+
output.masked += accumulator + pipeMatch
144+
145+
maskIndex += meta.pipe.length * 2 - 1
146+
accumulator = ''
147+
}
148+
121149
valueIndex++
122150
} else {
123151
accumulator += maskChar
124-
if (char?.toLocaleLowerCase() === maskChar?.toLocaleLowerCase()) {
152+
if (looselyStringMatch(char, maskChar)) {
125153
// user typed the same char as static mask char
126154
valueIndex++
127155
output.masked += accumulator
@@ -142,6 +170,26 @@ export function formatter(value, config) {
142170
return output
143171
}
144172

173+
/**
174+
* Loosely compare two strings and returns if they are equal ignoring case and locale
175+
* specific accents. Some browsers do not fully support this (Android webview and opera)
176+
* so we fallback to just ignoring casing in those cases.
177+
*
178+
* @see [MDM - LocaleCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)
179+
*
180+
* @param {String} str1 String one
181+
* @param {String} str2 String two
182+
* @returns Boolean
183+
*/
184+
export function looselyStringMatch(str1, str2) {
185+
/* istanbul ignore else */
186+
if (isLocaleCompareSupported) {
187+
return str1?.localeCompare(str2, undefined, { sensitivity: 'base' }) === 0
188+
} else {
189+
return str1?.toLocaleLowerCase() === str2?.toLocaleLowerCase()
190+
}
191+
}
192+
145193
/**
146194
* Facade to formatter/dynamic when mask is String or Array
147195
*

src/tokens.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export default {
66
a: { pattern: /[a-z]/i, transform: (v) => v.toLocaleLowerCase() },
77
'\\': { escape: true },
88
'?': { optional: true },
9-
'*': { repeat: true }
9+
'*': { repeat: true },
10+
'|': { pipe: true }
1011
}

tests/formatter.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,21 @@ test('France IBAN', () => {
146146
unmasked: '7630006000011234567890189'
147147
})
148148
})
149+
150+
describe('Alternations', () => {
151+
test('When matching an alternation', () => {
152+
expect(formatter('df1234', { mask: 'A|D###' })).toMatchObject({ masked: 'D123', unmasked: 'D123' })
153+
})
154+
155+
test('When not matching an alternation', () => {
156+
expect(formatter('1234', { mask: 'A|D###' })).toMatchObject({ masked: '', unmasked: '' })
157+
})
158+
159+
test('When having multiple alternations', () => {
160+
expect(formatter('D123455F', { mask: 'A|D###D|F|G' })).toMatchObject({ masked: 'D123F', unmasked: 'D123F' })
161+
})
162+
163+
test('When having characters with accents or different casing', () => {
164+
expect(formatter('é123455F', { mask: 'A|E###e|f' })).toMatchObject({ masked: 'E123f', unmasked: 'E123f' })
165+
})
166+
})

0 commit comments

Comments
 (0)