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

feat: cleave package update #4222

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
Draft
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/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@floating-ui/vue": "^1.0.1",
"@types/lodash": "^4.14.161",
"cleave.js": "^1.6.0",
"cleave-zen": "^0.0.17",
"colortranslator": "^1.9.2",
"lodash": "^4.17.21"
},
Expand Down Expand Up @@ -167,4 +167,4 @@
]
}
}
}
}
1 change: 0 additions & 1 deletion packages/ui/src/components/va-input/VaInput.demo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,6 @@ import { VaButton } from './../va-button'
import { VaIcon } from './../va-icon'
import VaInputValidation from './VaInput-validation.vue'
import { VaCheckbox } from '../va-checkbox'
import 'cleave.js/dist/addons/cleave-phone.us'

export default defineComponent({
components: {
Expand Down
99 changes: 84 additions & 15 deletions packages/ui/src/components/va-input/VaInput.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,39 +144,108 @@ export const Mask = () => ({
components: { VaInput },
data () {
return {
dateValue: '',
creditCardValue: '',
dateValue: '',
timeValue: '',
numeralValue: '',
generalValue: '',
}
},
template: `
[dateValue]: {{ dateValue }}
<VaInput v-model="dateValue" :mask="{
date: true,
datePattern: ['m', 'y'],
}" />

[creditCardValue]: {{ creditCardValue }}
<br />
<VaInput v-model="creditCardValue" mask="creditCard" />
<br />
<br />

[dateValue]: {{ dateValue }}
<br />
<VaInput v-model="dateValue" mask="date" />
<br />
<br />

[timeValue]: {{ timeValue }}
<br />
<VaInput v-model="timeValue" mask="time" />
<br />
<br />

[numeralValue]: {{ numeralValue }}
<br />
<VaInput v-model="numeralValue" mask="numeral" />
<br />
<br />

[generalValue]: {{ generalValue }}
<br />
<VaInput v-model="generalValue" :mask="{ type: 'general', options: { blocks: [3, 3, 3], delimiter: '·' }}" />
`,
})

export const MaskFormattedValue = () => ({
export const MaskRawValue = () => ({
components: { VaInput },
data () {
return {
dateValue: '',
creditCardValue: '',
creditCardValueRaw: '',
dateValue: '',
dateValueRaw: '',
timeValue: '',
timeValueRaw: '',
numeralValue: '',
numeralValueRaw: '',
generalValue: '',
generalValueRaw: '',
phoneValue: '',
phoneValueRaw: '',
}
},
template: `
[creditCardValue]: {{ creditCardValue }}
<br />
[creditCardValueRaw]: {{ creditCardValueRaw }}
<br />
<VaInput v-model="creditCardValue" mask="creditCard" @update:raw-value="v => creditCardValueRaw = v" />
<br />
<br />

[dateValue]: {{ dateValue }}
<VaInput v-model="dateValue" :mask="{
date: true,
datePattern: ['m', 'y'],
}" :return-raw="false" />
<br />
[dateValueRaw]: {{ dateValueRaw }}
<br />
<VaInput v-model="dateValue" mask="date" @update:raw-value="v => dateValueRaw = v" />
<br />
<br />

[creditCardValue]: {{ creditCardValue }}
<VaInput v-model="creditCardValue" mask="creditCard" :return-raw="false" />
[timeValue]: {{ timeValue }}
<br />
[timeValueRaw]: {{ timeValueRaw }}
<br />
<VaInput v-model="timeValue" mask="time" @update:raw-value="v => timeValueRaw = v" />
<br />
<br />

[numeralValue]: {{ numeralValue }}
<br />
[numeralValueRaw]: {{ numeralValueRaw }}
<br />
<VaInput v-model="numeralValue" mask="numeral" @update:raw-value="v => numeralValueRaw = v" />
<br />
<br />

[generalValue]: {{ generalValue }}
<br />
[generalValueRaw]: {{ generalValueRaw }}
<br />
<VaInput v-model="generalValue" :mask="{ type: 'general', options: { blocks: [3, 3, 3], delimiter: '·' }}" @update:raw-value="v => generalValueRaw = v" />
<br />
<br />

[phoneValue]: {{ phoneValue }}
<br />
[phoneValueRaw]: {{ phoneValueRaw }}
<br />
<VaInput v-model="phoneValue" type="tel" mask="phone" @update:raw-value="v => phoneValueRaw = v" />
`,
})

Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/components/va-input/VaInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
useFocusable, useFocusableProps, useEvent,
} from '../../composables'
import type { ValidationProps } from '../../composables/useValidation'
import { useCleave, useCleaveProps } from './hooks/useCleave'
import { useCleaveProps, useCleave } from './hooks/useCleave'

import type { AnyStringPropType } from '../../utils/types/prop-type'

Expand Down Expand Up @@ -123,6 +123,7 @@ const props = defineProps({

const emit = defineEmits([
'update:modelValue',
'update:rawValue',
...useValidationEmits,
...useClearableEmits,
...createInputEmits(),
Expand Down Expand Up @@ -174,7 +175,7 @@ const {
clearIconProps,
} = useClearable(props, modelValue, input, computedError)

const { computedValue, onInput } = useCleave(input, props, valueComputed)
const { computedValue, onInput } = useCleave(input, props, valueComputed, emit)

const inputListeners = createInputListeners(emit)

Expand Down
177 changes: 115 additions & 62 deletions packages/ui/src/components/va-input/hooks/useCleave.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,150 @@
import { computed, onBeforeUnmount, PropType, ref, Ref, watchEffect, type ExtractPropTypes, type WritableComputedRef } from 'vue'
import Cleave from 'cleave.js'
import { type CleaveOptions } from 'cleave.js/options'
import {
computed,
PropType,
Ref,
type ExtractPropTypes,
type WritableComputedRef,
} from 'vue'
import {
formatDate,
formatTime,
formatGeneral,
formatNumeral,
formatCreditCard,
unformatGeneral,
unformatNumeral,
unformatCreditCard,
type FormatGeneralOptions,
type FormatTimeOptions,
type FormatNumeralOptions,
type FormatCreditCardOptions,
type FormatDateOptions,
} from 'cleave-zen'

interface MaskOptions extends FormatGeneralOptions, FormatDateOptions, FormatNumeralOptions, FormatCreditCardOptions, FormatTimeOptions {}
interface MaskProp {
type: 'date' | 'time' | 'creditCard' | 'numeral' | 'general' | 'phone'
options: MaskOptions
}

const DEFAULT_MASK_TOKENS: Record<string, Record<string, unknown>> = {
const DEFAULT_MASK_TOKENS: Record<string, Record<'formatter' | 'transcriber' | 'options', any>> = {
creditCard: {
creditCard: true,
formatter: formatCreditCard,
transcriber: unformatCreditCard,
options: {} as FormatCreditCardOptions,
},
date: {
date: true,
datePattern: ['d', 'm', 'Y'],
formatter: formatDate,
transcriber: (value: string, options?: FormatDateOptions): string => {
if (options!.delimiter) {
return value.replaceAll(options!.delimiter, '')
}
return value
},
options: {
delimiter: '/',
} as FormatDateOptions,
},
time: {
time: true,
timePattern: ['h', 'm'],
timeFormat: '24',
formatter: formatTime,
transcriber: (value: string, options?: FormatTimeOptions): string => {
if (options!.delimiter) {
return value.replaceAll(options!.delimiter, '')
}
return value
},
options: {
timePattern: ['h', 'm'],
timeFormat: '24',
delimiter: ':',
} as FormatTimeOptions,
},
numeral: {
numeral: true,
numeralThousandsGroupStyle: 'thousand',
formatter: formatNumeral,
transcriber: unformatNumeral,
options: {} as FormatNumeralOptions,
},
general: {
formatter: formatGeneral,
transcriber: unformatGeneral,
options: {} as FormatGeneralOptions,
},
phone: {
formatter: (value: string, options: any) => {
// TODO: dynamic delimiter & prefix from options
const maxLength = options.blocks.reduce((acc: number, cv: number) => acc + cv, 0)
let newValue = value.replaceAll('+', '').replaceAll(' ', '').replaceAll(/[^0-9]/g, '').slice(0, maxLength)
if (!newValue) {
return newValue
}

const blockIndexes = options.blocks.reduce((acc: number[], cv: number) => [...acc, +acc.slice(-1) + cv], [])
const valuesArray: string[] = []
blockIndexes.forEach((blockIndex: number, idx: number) => {
const startIndex = idx ? blockIndexes[idx - 1] : 0
const endIndex = idx === (blockIndexes.length - 1) ? newValue.length : blockIndex
valuesArray.push(newValue.slice(startIndex, endIndex))
})

newValue = valuesArray.filter(v => !!v).join(' ')

if (newValue.charAt(0) !== '+') {
newValue = `+${newValue}`
}
return newValue
},
transcriber: (value: string) => {
return value.replaceAll('+', '').replaceAll(' ', '')
},
options: {
prefix: '+',
blocks: [3, 2, 4],
delimiter: '-',
},
},
}

export const useCleaveProps = {
mask: { type: [String, Object] as PropType<string | Record<string, number[]> | CleaveOptions>, default: '' },
mask: { type: [String, Object] as PropType<string | MaskProp>, default: '' },
returnRaw: { type: Boolean, default: true },
}

const useMask = (mask: string | MaskProp) => {
const maskType = typeof mask === 'string' ? mask : mask.type
const maskOptions = typeof mask === 'string' ? null : mask.options
const { formatter = (v: string) => v, transcriber = (v: string) => v, options = {} } = DEFAULT_MASK_TOKENS[maskType] || {}
return {
formatter: (value: string) => formatter(value, { ...options, ...maskOptions }),
transcriber: (value: string) => transcriber(value, { ...options, ...maskOptions }),
}
}

export const useCleave = (
element: Ref<HTMLInputElement | undefined>,
props: ExtractPropTypes<typeof useCleaveProps>,
syncValue: WritableComputedRef<string | number>,
emit: (event: any, ...args: any[]) => void,
) => {
const cleave = ref<Cleave>()

const getMask = (mask: CleaveOptions | string) => {
if (typeof mask === 'string') {
return DEFAULT_MASK_TOKENS[mask] ? { ...DEFAULT_MASK_TOKENS[mask] } : null
}
return { ...mask }
}

const destroyCleave = () => {
if (cleave.value) { cleave.value.destroy() }
}

const mask = computed(() => getMask(props.mask))

const cleaveEnabled = computed(() => {
return mask.value && Object.keys(mask.value).length
})

watchEffect(() => {
destroyCleave()

if (!element.value) { return }

// Do not create cleave instance if mask is not defined
if (!cleaveEnabled.value || !mask.value) { return }

cleave.value = new Cleave(element.value, mask.value)

cleave.value!.properties.onValueChanged = ({ target: { rawValue, value } }) => {
if (props.returnRaw) {
syncValue.value = rawValue
} else {
syncValue.value = value
}
}
})

onBeforeUnmount(() => { destroyCleave() })
const { formatter, transcriber } = useMask(props.mask)

const computedValue = computed<string | number>(() => {
if (cleave.value) {
if (props.returnRaw && syncValue.value === cleave.value.getRawValue()) {
return cleave.value.getFormattedValue()
}
}

return syncValue.value
})

const onInput = (event: Event) => {
const value = (event.target as HTMLInputElement).value

if (!cleaveEnabled.value) {
if (!props.mask) {
syncValue.value = value
emit('update:rawValue', value)
} else {
const formattedValue = formatter(value)
syncValue.value = formattedValue
element.value!.value = formattedValue
emit('update:rawValue', transcriber(formattedValue))
}
}

return {
cleave,
cleaveEnabled,
computedValue,
onInput,
}
Expand Down
Loading