refactor(input): pattern -> allow-input

This commit is contained in:
07akioni 2022-06-12 17:07:09 +08:00
parent 599d524b35
commit 014ddea7de
8 changed files with 56 additions and 62 deletions

View File

@ -15,6 +15,7 @@
### Feats ### Feats
- 🌟 `n-pagination` adds dropdown menu for fast jump button. - 🌟 `n-pagination` adds dropdown menu for fast jump button.
- `n-input` adds `allow-input` prop.
- `n-tree-select` adds `arrow` slot, closes [#3084](https://github.com/TuSimple/naive-ui/issues/3084). - `n-tree-select` adds `arrow` slot, closes [#3084](https://github.com/TuSimple/naive-ui/issues/3084).
- `n-cascader` will show corresponding submenu after checkbox is clicked, closes [#3079](https://github.com/TuSimple/naive-ui/issues/3079). - `n-cascader` will show corresponding submenu after checkbox is clicked, closes [#3079](https://github.com/TuSimple/naive-ui/issues/3079).
- `n-upload` will disable dragger when maximum number of files was reached. - `n-upload` will disable dragger when maximum number of files was reached.
@ -22,7 +23,6 @@
- `n-popselect` adds `node-props` prop. - `n-popselect` adds `node-props` prop.
- `n-popselect` adds `virtual-scroll` prop. - `n-popselect` adds `virtual-scroll` prop.
- `n-data-table` adds `scrollTo` method, closes [#2570](https://github.com/TuSimple/naive-ui/issues/2570). - `n-data-table` adds `scrollTo` method, closes [#2570](https://github.com/TuSimple/naive-ui/issues/2570).
- `n-input` adds `pattern` props.
## 2.30.3 ## 2.30.3

View File

@ -22,7 +22,7 @@
- `n-popselect` 新增 `node-props` 属性 - `n-popselect` 新增 `node-props` 属性
- `n-popselect` 新增 `virtual-scroll` 属性 - `n-popselect` 新增 `virtual-scroll` 属性
- `n-data-table` 新增 `scrollTo` 方法,关闭 [#2570](https://github.com/TuSimple/naive-ui/issues/2570) - `n-data-table` 新增 `scrollTo` 方法,关闭 [#2570](https://github.com/TuSimple/naive-ui/issues/2570)
- `n-input` 新增 `pattern` 属性 - `n-input` 新增 `allow-input` 属性
## 2.30.3 ## 2.30.3

View File

@ -31,6 +31,7 @@ pattern.vue
| Name | Type | Default | Description | Version | | Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| allow-input | `(value: string) => boolean` | `undefined` | Check the incoming value, if it returns `false`, input will not be accepted. | NEXT_VERSION |
| autofocus | `boolean` | `false` | Whether to autofocus. | | | autofocus | `boolean` | `false` | Whether to autofocus. | |
| autosize | `boolean \| { minRows?: number, maxRows?: number }` | `false` | Sizing property for when the input is of type `textarea`. e.g. `{ minRows: 1, maxRows: 3 }`. | | | autosize | `boolean \| { minRows?: number, maxRows?: number }` | `false` | Sizing property for when the input is of type `textarea`. e.g. `{ minRows: 1, maxRows: 3 }`. | |
| clearable | `boolean` | `false` | Whether the input is clearable. | | | clearable | `boolean` | `false` | Whether the input is clearable. | |
@ -51,7 +52,6 @@ pattern.vue
| show-password-on | `'click' \| 'mousedown'` | `undefined` | The event to show the password. | | | show-password-on | `'click' \| 'mousedown'` | `undefined` | The event to show the password. | |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | Input size. | | | size | `'small' \| 'medium' \| 'large'` | `'medium'` | Input size. | |
| status | `'success' \| 'warning' \| 'error'` | `undefined` | Validaiton status. | 2.25.0 | | status | `'success' \| 'warning' \| 'error'` | `undefined` | Validaiton status. | 2.25.0 |
| pattern | `(value: string) => boolean` | `undefined` | Check the incoming value, if it returns `false`, input will not be accepted. | NEXT_VERSION |
| type | `'text' \| 'password' \| 'textarea'` | `'text'` | Input type. | | | type | `'text' \| 'password' \| 'textarea'` | `'text'` | Input type. | |
| value | `string \| [string, string] \| null` | `undefined` | Manually set the input value. When `pair` is `true`, this is an array. | | | value | `string \| [string, string] \| null` | `undefined` | Manually set the input value. When `pair` is `true`, this is an array. | |
| on-blur | `() => void` | `undefined` | Callback triggered when the input is blurred. | | | on-blur | `() => void` | `undefined` | Callback triggered when the input is blurred. | |

View File

@ -1,20 +1,20 @@
<markdown> <markdown>
# Input intercept # Limit input format
Control the entry format of the input. Use `allow-input` to limit input value to desired format. You can use it to achieve trim effect.
</markdown> </markdown>
<template> <template>
<n-space vertical> <n-space vertical>
<n-input <n-input
type="text" type="text"
:pattern="validateOnlyNumber" :allow-input="onlyAllowNumber"
placeholder="Only enter the number." placeholder="Only allow number"
/> />
<n-input <n-input
type="textarea" type="textarea"
:pattern="validateEmpty" :allow-input="noSideSpace"
placeholder="Can't enter space." placeholder="No leading or trailing space"
/> />
</n-space> </n-space>
</template> </template>
@ -25,8 +25,8 @@ import { defineComponent } from 'vue'
export default defineComponent({ export default defineComponent({
setup () { setup () {
return { return {
validateOnlyNumber: (value: string) => !value || /^\d+$/.test(value), onlyAllowNumber: (value: string) => !value || /^\d+$/.test(value),
validateEmpty: (value: string) => !/ /g.test(value) noSideSpace: (value: string) => !/ /g.test(value)
} }
} }
}) })

View File

@ -32,6 +32,7 @@ rtl-debug.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 | | 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| allow-input | `(value: string) => false` | `undefined` | 校验当前的输入是否合法,如果返回 `false` 输入框便不会响应此次的输入 | NEXT_VERSION |
| autofocus | `boolean` | `false` | 是否自动获取焦点 | | | autofocus | `boolean` | `false` | 是否自动获取焦点 | |
| autosize | `boolean \| { minRows?: number, maxRows?: number }` | `false` | 自适应内容高度,只对 `type="textarea"` 有效,可传入对象,如 `{ minRows: 1, maxRows: 3 }` | | | autosize | `boolean \| { minRows?: number, maxRows?: number }` | `false` | 自适应内容高度,只对 `type="textarea"` 有效,可传入对象,如 `{ minRows: 1, maxRows: 3 }` | |
| clearable | `boolean` | `false` | 是否可清空 | | | clearable | `boolean` | `false` | 是否可清空 | |
@ -52,7 +53,6 @@ rtl-debug.vue
| show-password-on | `'click' \| 'mousedown'` | `undefined` | 显示密码的时机 | | | show-password-on | `'click' \| 'mousedown'` | `undefined` | 显示密码的时机 | |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 输入框尺寸 | | | size | `'small' \| 'medium' \| 'large'` | `'medium'` | 输入框尺寸 | |
| status | `'success' \| 'warning' \| 'error'` | `undefined` | 验证状态 | 2.25.0 | | status | `'success' \| 'warning' \| 'error'` | `undefined` | 验证状态 | 2.25.0 |
| pattern | `(value: string) => false` | `undefined` | 校验当前的输入是否合法,如果返回 `false` 输入框便不会响应此次的输入 | NEXT_VERSION |
| type | `'text' \| 'password' \| 'textarea'` | `'text'` | 输入框类型 | | | type | `'text' \| 'password' \| 'textarea'` | `'text'` | 输入框类型 | |
| value | `string \| [string, string] \| null` | `undefined` | 文本输入的值。如果 `pair``true``value` 是一个数组 | | | value | `string \| [string, string] \| null` | `undefined` | 文本输入的值。如果 `pair``true``value` 是一个数组 | |
| on-blur | `() => void` | `undefined` | 输入框失去焦点时触发 | | | on-blur | `() => void` | `undefined` | 输入框失去焦点时触发 | |

View File

@ -1,20 +1,20 @@
<markdown> <markdown>
# 输入校验 # 输入校验
限制输入框的输入格式 使用 `allow-input` 限制输入框的输入格式你可以使用它来达到 `trim` 的效果
</markdown> </markdown>
<template> <template>
<n-space vertical> <n-space vertical>
<n-input <n-input
type="text" type="text"
:pattern="validateOnlyNumber" :allow-input="onlyAllowNumber"
placeholder="只能输入数字" placeholder="只能输入数字"
/> />
<n-input <n-input
type="textarea" type="textarea"
:pattern="validateEmpty" :allow-input="noSideSpace"
placeholder="不能输入空格" placeholder="没有前后空格"
/> />
</n-space> </n-space>
</template> </template>
@ -25,8 +25,9 @@ import { defineComponent } from 'vue'
export default defineComponent({ export default defineComponent({
setup () { setup () {
return { return {
validateOnlyNumber: (value: string) => !value || /^\d+$/.test(value), onlyAllowNumber: (value: string) => !value || /^\d+$/.test(value),
validateEmpty: (value: string) => !/ /g.test(value) noSideSpace: (value: string) =>
!value.startsWith(' ') && !value.endsWith(' ')
} }
} }
}) })

View File

@ -24,6 +24,7 @@ import { VResizeObserver } from 'vueuc'
import { off, on } from 'evtd' import { off, on } from 'evtd'
import type { FormValidationStatus } from '../../form/src/interface' import type { FormValidationStatus } from '../../form/src/interface'
import { EyeIcon, EyeOffIcon } from '../../_internal/icons' import { EyeIcon, EyeOffIcon } from '../../_internal/icons'
import useRtl from '../../_mixins/use-rtl'
import { import {
NBaseClear, NBaseClear,
NBaseIcon, NBaseIcon,
@ -57,10 +58,9 @@ import type {
InputWrappedRef InputWrappedRef
} from './interface' } from './interface'
import { inputInjectionKey } from './interface' import { inputInjectionKey } from './interface'
import { isEmptyValue, useCursor } from './utils' import { isEmptyInputValue, useCursor } from './utils'
import WordCount from './WordCount' import WordCount from './WordCount'
import style from './styles/input.cssr' import style from './styles/input.cssr'
import useRtl from '../../_mixins/use-rtl'
const inputProps = { const inputProps = {
...(useTheme.props as ThemeProps<InputTheme>), ...(useTheme.props as ThemeProps<InputTheme>),
@ -120,7 +120,7 @@ const inputProps = {
type: Boolean, type: Boolean,
default: undefined default: undefined
}, },
pattern: Function as PropType<(value: string) => boolean>, allowInput: Function as PropType<(value: string) => boolean>,
onMousedown: Function as PropType<(e: MouseEvent) => void>, onMousedown: Function as PropType<(e: MouseEvent) => void>,
onKeydown: Function as PropType<(e: KeyboardEvent) => void>, onKeydown: Function as PropType<(e: KeyboardEvent) => void>,
onKeyup: Function as PropType<(e: KeyboardEvent) => void>, onKeyup: Function as PropType<(e: KeyboardEvent) => void>,
@ -200,7 +200,7 @@ export default defineComponent({
const currentFocusedInputRef = ref< const currentFocusedInputRef = ref<
HTMLInputElement | HTMLTextAreaElement | null HTMLInputElement | HTMLTextAreaElement | null
>(null) >(null)
const focusedInputCorsurControl = useCursor(currentFocusedInputRef) const focusedInputCursorControl = useCursor(currentFocusedInputRef)
const textareaScrollbarInstRef = ref<ScrollbarInst | null>(null) const textareaScrollbarInstRef = ref<ScrollbarInst | null>(null)
// local // local
const { localeRef } = useLocale('Input') const { localeRef } = useLocale('Input')
@ -242,8 +242,8 @@ export default defineComponent({
const { value: mergedPlaceholder } = mergedPlaceholderRef const { value: mergedPlaceholder } = mergedPlaceholderRef
return ( return (
!isComposing && !isComposing &&
(isEmptyValue(mergedValue) || (isEmptyInputValue(mergedValue) ||
(Array.isArray(mergedValue) && isEmptyValue(mergedValue[0]))) && (Array.isArray(mergedValue) && isEmptyInputValue(mergedValue[0]))) &&
mergedPlaceholder[0] mergedPlaceholder[0]
) )
}) })
@ -254,8 +254,8 @@ export default defineComponent({
return ( return (
!isComposing && !isComposing &&
mergedPlaceholder[1] && mergedPlaceholder[1] &&
(isEmptyValue(mergedValue) || (isEmptyInputValue(mergedValue) ||
(Array.isArray(mergedValue) && isEmptyValue(mergedValue[1]))) (Array.isArray(mergedValue) && isEmptyInputValue(mergedValue[1])))
) )
}) })
// focus // focus
@ -437,7 +437,6 @@ export default defineComponent({
} else { } else {
handleInput(e, 0) handleInput(e, 0)
} }
focusedInputCorsurControl.recordCursor()
} }
function handleInput ( function handleInput (
e: InputEvent | CompositionEvent | Event, e: InputEvent | CompositionEvent | Event,
@ -446,7 +445,6 @@ export default defineComponent({
): void { ): void {
const targetValue = (e.target as HTMLInputElement).value const targetValue = (e.target as HTMLInputElement).value
syncMirror(targetValue) syncMirror(targetValue)
focusedInputCorsurControl.recordCursor()
if (props.type === 'textarea') { if (props.type === 'textarea') {
const { value: textareaScrollbarInst } = textareaScrollbarInstRef const { value: textareaScrollbarInst } = textareaScrollbarInstRef
if (textareaScrollbarInst) { if (textareaScrollbarInst) {
@ -455,8 +453,9 @@ export default defineComponent({
} }
syncSource = targetValue syncSource = targetValue
if (isComposingRef.value) return if (isComposingRef.value) return
const isValidInputValue = matches(targetValue) focusedInputCursorControl.recordCursor()
if (isValidInputValue) { const isIncomingValueValid = allowInput(targetValue)
if (isIncomingValueValid) {
if (!props.pair) { if (!props.pair) {
event === 'input' ? doUpdateValue(targetValue) : doChange(targetValue) event === 'input' ? doUpdateValue(targetValue) : doChange(targetValue)
} else { } else {
@ -464,7 +463,7 @@ export default defineComponent({
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
value = ['', ''] value = ['', '']
} else { } else {
value = [...value] value = [value[0], value[1]]
} }
value[index] = targetValue value[index] = targetValue
event === 'input' ? doUpdateValue(value) : doChange(value) event === 'input' ? doUpdateValue(value) : doChange(value)
@ -473,14 +472,14 @@ export default defineComponent({
// force update to sync input's view with value // force update to sync input's view with value
// if not set, after input, input value won't sync with dom input value // if not set, after input, input value won't sync with dom input value
vm.$forceUpdate() vm.$forceUpdate()
if (!isValidInputValue) { if (!isIncomingValueValid) {
void nextTick(focusedInputCorsurControl.restoreCursor) void nextTick(focusedInputCursorControl.restoreCursor)
} }
} }
function matches (value: string): boolean { function allowInput (value: string): boolean {
const { pattern } = props const { allowInput } = props
if (typeof pattern === 'function') { if (typeof allowInput === 'function') {
return pattern(value) return allowInput(value)
} }
return true return true
} }
@ -502,7 +501,7 @@ export default defineComponent({
dealWithEvent(e, 'blur') dealWithEvent(e, 'blur')
currentFocusedInputRef.value = null currentFocusedInputRef.value = null
} }
function handleInputFocus (e: FocusEvent, index?: number): void { function handleInputFocus (e: FocusEvent, index: number): void {
doUpdateValueFocus(e) doUpdateValueFocus(e)
focusedRef.value = true focusedRef.value = true
activatedRef.value = true activatedRef.value = true
@ -512,7 +511,7 @@ export default defineComponent({
currentFocusedInputRef.value = inputElRef.value currentFocusedInputRef.value = inputElRef.value
} else if (index === 1) { } else if (index === 1) {
currentFocusedInputRef.value = inputEl2Ref.value currentFocusedInputRef.value = inputEl2Ref.value
} else { } else if (index === 2) {
currentFocusedInputRef.value = textareaElRef.value currentFocusedInputRef.value = textareaElRef.value
} }
} }
@ -1059,7 +1058,7 @@ export default defineComponent({
scrollContainerWidthStyle scrollContainerWidthStyle
]} ]}
onBlur={this.handleInputBlur} onBlur={this.handleInputBlur}
onFocus={this.handleInputFocus} onFocus={(e) => this.handleInputFocus(e, 2)}
onInput={this.handleInput} onInput={this.handleInput}
onChange={this.handleChange} onChange={this.handleChange}
onScroll={this.handleTextAreaScroll} onScroll={this.handleTextAreaScroll}

View File

@ -10,18 +10,17 @@ export function len (s: string): number {
return count return count
} }
export function isEmptyValue (value: any): boolean { export function isEmptyInputValue (value: unknown): boolean {
return ['', undefined, null].includes(value) return value === '' || value == null
} }
export interface UseCursorControl { export interface UseCursorControl {
recordCursor: () => void recordCursor: () => void
restoreCursor: () => void restoreCursor: () => void
clearRecord: () => void
} }
export function useCursor ( export function useCursor (
inputRef: Ref<HTMLInputElement | HTMLTextAreaElement | null> inputElRef: Ref<HTMLInputElement | HTMLTextAreaElement | null>
): UseCursorControl { ): UseCursorControl {
const selectionRef = ref<{ const selectionRef = ref<{
start: number start: number
@ -31,35 +30,32 @@ export function useCursor (
} | null>(null) } | null>(null)
function recordCursor (): void { function recordCursor (): void {
const { value: input } = inputRef const { value: input } = inputElRef
if (!input || !input.focus) { if (!input || !input.focus) {
clearRecord() reset()
return return
} }
const { selectionStart, selectionEnd, value } = input const { selectionStart, selectionEnd, value } = input
// eslint-disable-next-line eqeqeq if (selectionStart == null || selectionEnd == null) {
if (selectionStart == void 0 || selectionEnd == void 0) { reset()
clearRecord()
return return
} }
selectionRef.value = { selectionRef.value = {
start: selectionStart, start: selectionStart,
end: selectionEnd, end: selectionEnd,
beforeText: value.substring(0, selectionStart), beforeText: value.slice(0, selectionStart),
afterText: value.substring(selectionEnd) afterText: value.slice(selectionEnd)
} }
} }
function restoreCursor (): void { function restoreCursor (): void {
const { value: selection } = selectionRef const { value: selection } = selectionRef
const { value: input } = inputRef const { value: inputEl } = inputElRef
if (!selection || !input || !input.focus) { if (!selection || !inputEl) {
return return
} }
const { value } = inputEl
const { value } = input
const { start, beforeText, afterText } = selection const { start, beforeText, afterText } = selection
let startPos = value.length let startPos = value.length
if (value.endsWith(afterText)) { if (value.endsWith(afterText)) {
startPos = value.length - afterText.length startPos = value.length - afterText.length
@ -72,18 +68,16 @@ export function useCursor (
startPos = newIndex + 1 startPos = newIndex + 1
} }
} }
inputEl.setSelectionRange?.(startPos, startPos)
input.setSelectionRange?.(startPos, startPos)
} }
function clearRecord (): void { function reset (): void {
selectionRef.value = null selectionRef.value = null
} }
watch(inputRef, clearRecord) watch(inputElRef, reset)
return { return {
recordCursor, recordCursor,
restoreCursor, restoreCursor
clearRecord
} }
} }