Skip to content

Commit

Permalink
feat: masks (#4332)
Browse files Browse the repository at this point in the history
* raw

* feat(masks): remove junk

* feat(masks): add docs

* feat(masks): improve suggestion when option group

* feat(masks): add static tokens if only one possible solution is left

* docs(masks): improve phone extended demo

* feat(masks): correct suggestion when only one possible solution is left
  • Loading branch information
m0ksem authored Jul 1, 2024
1 parent 1e99407 commit 2ded0a2
Show file tree
Hide file tree
Showing 22 changed files with 1,597 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<VaInput v-model="text" ref="vaInput" placeholder="#### #### #### ####" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VaInput, useInputMask, createMaskFromRegex } from 'vuestic-ui'
const text = ref('')
const vaInput = ref()
useInputMask(createMaskFromRegex(/\d\d\d\d \d\d\d\d \d\d\d\d \d\d\d\d/), vaInput)
</script>
13 changes: 13 additions & 0 deletions packages/docs/page-config/composables/input-mask/examples/Date.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<VaInput v-model="text" ref="vaInput" placeholder="YYYY/MM/DD" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VaInput, useInputMask, createMaskDate } from 'vuestic-ui'
const text = ref('')
const vaInput = ref()
useInputMask(createMaskDate(), vaInput)
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<VaInput v-model="text" ref="vaInput" placeholder="Vuestic - 0000 - Vue" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VaInput, useInputMask, createMaskFromRegex } from 'vuestic-ui'
const text = ref('')
const vaInput = ref()
useInputMask(createMaskFromRegex(/Vuestic - \d\d\d\d - Vue/), vaInput)
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<VaInput v-model="text" ref="vaInput" placeholder="0000:0000:0000:0000:0000:0000:0000:0000" class="w-full" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VaInput, useInputMask, createMaskFromRegex } from 'vuestic-ui'
const text = ref('')
const vaInput = ref()
const ipv6Regex = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))/
useInputMask(createMaskFromRegex(ipv6Regex), vaInput)
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<VaInput v-model="text" ref="vaInput" placeholder="12304.4213" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VaInput, useInputMask, createNumeralMask } from 'vuestic-ui'
const text = ref('')
const vaInput = ref()
useInputMask(createNumeralMask(), vaInput)
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<VaInput v-model="text" ref="vaInput" placeholder="+380 (93) 000-00-00" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VaInput, useInputMask, createMaskFromRegex } from 'vuestic-ui'
const text = ref('')
const vaInput = ref()
useInputMask(createMaskFromRegex(/(\+380 \(\d{2}\)|\d{3}) (\d){3}-\d\d-\d\d/), vaInput)
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<VaInput ref="vaInput" v-model="text" placeholder="+380 (93) 000-00-00" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VaInput, useInputMask, createMaskFromRegex } from 'vuestic-ui'
const text = ref('')
const vaInput = ref()
const ukrPhoneMask = createMaskFromRegex(/\+380 \(\d{2}\) (\d){3}-\d\d-\d\d/)
const usPhoneMask = createMaskFromRegex(/\+1 \(\d{3}\) (\d){3}-\d\d-\d\d/)
const nationalPhoneMask = createMaskFromRegex(/\+(380|1) /)
useInputMask({
format(text) {
if (text.startsWith('+380')) {
return ukrPhoneMask.format(text)
}
if (text.startsWith('+1')) {
return usPhoneMask.format(text)
}
return nationalPhoneMask.format(text)
},
unformat(text, tokens) {
if (text.startsWith('+380')) {
return ukrPhoneMask.unformat(text, tokens)
}
if (text.startsWith('+1')) {
return usPhoneMask.unformat(text, tokens)
}
return nationalPhoneMask.unformat(text, tokens)
},
handleCursor(selectionStart, selectionEnd, oldTokens, newTokens, text, data) {
if (text.startsWith('+380')) {
return ukrPhoneMask.handleCursor(selectionStart, selectionEnd, oldTokens, newTokens, text, data)
}
if (text.startsWith('+1')) {
return usPhoneMask.handleCursor(selectionStart, selectionEnd, oldTokens, newTokens, text, data)
}
return nationalPhoneMask.handleCursor(selectionStart, selectionEnd, oldTokens, newTokens, text, data)
},
reverse: false,
}, vaInput)
</script>
69 changes: 69 additions & 0 deletions packages/docs/page-config/composables/input-mask/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export default definePageConfig({
blocks: [
block.title('Input Mask'),
block.paragraph('Masks are used to format the input value. Instead of having built-in masks we provide a composable, that you can use with any input.'),
block.paragraph('Vuestic UI comes with a few predefined masks. You need to manually import them and use with `useInputMask` composable.'),
block.paragraph('`useInputMask` composable is designed to work with Regex based masks. It is a flexible solution that allows you to create any mask you need.'),

block.subtitle('Examples'),

block.example('DefaultRegex', {
title: 'Regex mask',
description: 'When creating a mask we use regex syntax, that allows you to deeply customize the mask.',
}),

block.collapse('Regex basic syntax', [
block.paragraph('Here is a list of regex tokens that you can use to create a mask:'),

block.list([
'`\\d` - any digit',
'`\\w` - any word character',
'`.` - any character',
'`[a-z]` - any character from a to z. Notice, can be also `[a-f]` - any character from a to f',
'`[0-9]` - any digit',
]),

block.paragraph('Mask support quantifiers syntax, allowing you to specify how many times the character should appear. Here are some examples:'),

block.list([
'`\\d?` - any digit or nothing',
'`\\d*` - any number of digits',
'`\\d+` - at least one digit',
'`\\d{3}` - exactly 3 digits',
'`\\d{3,}` - at least 3 digits',
'`\\d{3,5}` - from 3 to 5 digits',
]),

block.paragraph('Obviously, you can replace `\\d` with any token. You can also use groups to group characters, for example `(\\d - \\d)?`'),
block.alert('Notice that maximum repetition is 10.', 'warning'),

block.paragraph('In case you need to use brackets in the mask, you need to escape them: `\\(\\d{3}\\)`'),

block.paragraph('You can also use `|` to separate different options: `\\d{3}|\\w{3}`'),
]),

block.example('CreditCard', {
title: 'Credit card',
description: 'You can easily define credit card mask with regex',
}),

block.example('Date', {
title: 'Date mask',
description: 'Date mask is a predefined mask that allows you to format the date input.',
}),

block.example('Numeral', {
title: 'Numeral mask',
description: 'Numeral mask is a predefined mask that allows you to format the number input. You can decide if decimal is allowed and how many decimal places are allowed.',
}),

block.example('Phone', {
title: 'Phone mask',
description: 'There is no predefined phone mask. You can create your own mask using regex syntax. This is an example for Ukrainian phone number in internationl',
}),

block.paragraph('You can also create regex masks for any other phone format and use format functions based on user input. You can also write format function in plain JS and we will handle the rest for you.'),

block.example('PhoneExtended')
]
})
14 changes: 14 additions & 0 deletions packages/docs/page-config/navigationRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ export const navigationRoutes: NavigationRoute[] = [
},
],
},
{
name: 'composables',
displayName: 'Composables',
disabled: true,
children: [
{
name: 'input-mask',
displayName: 'Input Mask',
meta: {
badge: navigationBadge.new('1.10.0'),
},
}
]
},
{
name: "ui-elements",
displayName: "UI Elements",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ export * from './useElementTextColor'
export * from './useElementBackground'
export * from './useImmediateFocus'
export * from './useNumericProp'
export * from './useInputMask'
export * from './useElementRect'
97 changes: 97 additions & 0 deletions packages/ui/src/composables/useInputMask/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { MaskToken } from './mask'

export enum CursorPosition {
BeforeChar = -1,
Any = 0,
AfterChar = 1
}

export class Cursor<Token extends MaskToken> extends Number {
constructor (public position: number, private tokens: Token[], private reversed: boolean = false) {
super(position)
}

private move (direction: -1 | 1, amount: number, cursorPosition = CursorPosition.Any) {
if (this.tokens.every((t) => t.static)) {
if (direction === 1) {
this.position = this.tokens.length
return this.position
} else {
this.position = 0
return this.position
}
}

for (let i = this.position; i <= this.tokens.length && i >= -1; i += direction) {
const current = this.tokens[i]
const next = this.tokens[i + direction]
const prev = this.tokens[i - direction]

if (amount < 0) {
this.position = i
return this.position
}

if (!current?.static) {
amount--
}

if (cursorPosition <= CursorPosition.Any) {
if (direction === -1 && !next?.static && current?.static) {
amount--
}
if (direction === 1 && !prev?.static && current?.static) {
amount--
}
}

if (cursorPosition >= CursorPosition.Any) {
if (direction === 1 && !prev?.static && current === undefined) {
amount--
} else if (direction === 1 && current === undefined && next?.static) {
amount--
} else if (direction === 1 && current === undefined && next === undefined) {
amount--
}
}

if (amount < 0) {
this.position = i
return this.position
}
}

return this.position
}

moveBack (amount: number, cursorPosition = CursorPosition.Any) {
return this.move(-1, amount, cursorPosition)
}

moveForward (amount: number, cursorPosition = CursorPosition.Any) {
return this.move(1, amount, cursorPosition)
}

updateTokens (newTokens: Token[], fromEnd: boolean = false) {
if (fromEnd) {
// When reversed, we need to update position from the end
this.position = this.tokens.length - this.position
this.tokens = newTokens
this.position = this.tokens.length - this.position
} else {
this.tokens = newTokens
}
}

valueOf () {
if (this.position < 0) {
return 0
}

if (this.position > this.tokens.length) {
return this.tokens.length
}

return this.position
}
}
4 changes: 4 additions & 0 deletions packages/ui/src/composables/useInputMask/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { useInputMask } from './useInputMask'
export { createMaskFromRegex, compareWithMask } from './masks/regex'
export { createNumeralMask } from './masks/numeral'
export { createMaskDate } from './masks/date'
16 changes: 16 additions & 0 deletions packages/ui/src/composables/useInputMask/mask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Cursor } from './cursor'

export type MaskToken = {
static: boolean,
}

export type Mask<Token extends MaskToken = MaskToken, Data = any> = {
format: (text: string) => {
text: string,
tokens: Token[]
data?: Data,
},
handleCursor: (selectionStart: Cursor<Token>, selectionEnd: Cursor<Token>, oldTokens: Token[], newTokens: Token[], text: string, data?: Data) => any,
unformat: (text: string, tokens: Token[]) => string,
reverse: boolean
}
Loading

0 comments on commit 2ded0a2

Please sign in to comment.