From 367ef3df2d3ba24f6b3a09aa13858490588bc613 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sat, 29 Oct 2022 21:52:16 +0800 Subject: [PATCH] feat(input): adds `count-graphemes` prop, closes #3967 --- CHANGELOG.en-US.md | 1 + CHANGELOG.zh-CN.md | 1 + package.json | 1 + src/input/demos/enUS/graphemes.demo.vue | 35 ++++++++++++++++++++ src/input/demos/enUS/index.demo-entry.md | 2 ++ src/input/demos/zhCN/graphemes.demo.vue | 35 ++++++++++++++++++++ src/input/demos/zhCN/index.demo-entry.md | 2 ++ src/input/src/Input.tsx | 41 +++++++++++++++++++----- src/input/src/WordCount.tsx | 9 ++++-- src/input/src/interface.ts | 1 + vite.config.js | 6 ++++ 11 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/input/demos/enUS/graphemes.demo.vue create mode 100644 src/input/demos/zhCN/graphemes.demo.vue diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 19238989a..7f517613c 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -15,6 +15,7 @@ - `n-tree-select` adds `blur` method. - `n-cascader` adds `getCheckedKeys` method. - `n-cascader` adds `getIndeterminateKeys` method. +- `n-input` adds `count-graphemes` prop, closes [#3967](https://github.com/tusen-ai/naive-ui/issues/3967). ### i18n diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 8f9a03f28..48e61b573 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -15,6 +15,7 @@ - `n-tree-select` 新增 `blur` 方法 - `n-cascader` 新增 `getCheckedKeys` 方法 - `n-cascader` 新增 `getIndeterminateKeys` 方法 +- `n-input` 新增 `count-graphemes` 属性,关闭 [#3967](https://github.com/tusen-ai/naive-ui/issues/3967) ### i18n diff --git a/package.json b/package.json index 96f5bba3f..8fc369e1c 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "express": "^4.17.3", "fast-glob": "^3.2.11", "fs-extra": "^10.0.1", + "grapheme-splitter": "^1.0.4", "husky": "^8.0.1", "inquirer": "^9.1.0", "jest": "^28.0.3", diff --git a/src/input/demos/enUS/graphemes.demo.vue b/src/input/demos/enUS/graphemes.demo.vue new file mode 100644 index 000000000..570d6d693 --- /dev/null +++ b/src/input/demos/enUS/graphemes.demo.vue @@ -0,0 +1,35 @@ + +# Count graphemes + +Browser's default `maxlength` and `minlength` and naive-ui's builtin character count method can't split all string correctly. You can use `count-graphemes` count to count graphemes correctly. + + + + + diff --git a/src/input/demos/enUS/index.demo-entry.md b/src/input/demos/enUS/index.demo-entry.md index 200011407..451ffdfaf 100644 --- a/src/input/demos/enUS/index.demo-entry.md +++ b/src/input/demos/enUS/index.demo-entry.md @@ -23,6 +23,7 @@ event.vue input-props.vue status.vue pattern.vue +graphemes.vue ``` ## API @@ -35,6 +36,7 @@ pattern.vue | 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 }`. | | | clearable | `boolean` | `false` | Whether the input is clearable. | | +| count-graphemes | `(value: string) => number` | `undefined` | Count graphemes of input value. If it's set, native `maxlength` and `minlength` won't be used. | NEXT_VERSION | | default-value | `string \| [string, string] \| null` | `null` | Default value when not manually set. | | | disabled | `boolean` | `false` | Whether to disable the input. | | | input-props | `HTMLInputAttributes` | `undefined` | The dom props of the input element inside the component. This is disabled if the `pair` property is true. For avaiable attributes, [see here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). Warning:It won't override internal props with the same name (except `type`). | | diff --git a/src/input/demos/zhCN/graphemes.demo.vue b/src/input/demos/zhCN/graphemes.demo.vue new file mode 100644 index 000000000..100a7625d --- /dev/null +++ b/src/input/demos/zhCN/graphemes.demo.vue @@ -0,0 +1,35 @@ + +# 字素计数 + +浏览器默认的 `maxlength` 和 `minlength` 以及 naive-ui 自带的字数统计功能并不能准确的拆分所有的字符串,你可以使用 `count-graphemes` 属性来精确的测量文字长度。 + + + + + diff --git a/src/input/demos/zhCN/index.demo-entry.md b/src/input/demos/zhCN/index.demo-entry.md index 252b4b688..c5ad38752 100644 --- a/src/input/demos/zhCN/index.demo-entry.md +++ b/src/input/demos/zhCN/index.demo-entry.md @@ -23,6 +23,7 @@ event.vue input-props.vue status.vue pattern.vue +graphemes.vue rtl-debug.vue prefix-debug.vue modal-debug.vue @@ -40,6 +41,7 @@ textarea-resize-debug.vue | autosize | `boolean \| { minRows?: number, maxRows?: number }` | `false` | 自适应内容高度,只对 `type="textarea"` 有效,可传入对象,如 `{ minRows: 1, maxRows: 3 }` | | | clearable | `boolean` | `false` | 是否可清空 | | | default-value | `string \| [string, string] \| null` | `null` | 输入框默认值 | | +| count-graphemes | `(value: string) => number` | `undefined` | 计算输入的字数。如果设定了,那么原生的 `maxlength` 和 `minlength` 属性将不再被使用 | NEXT_VERSION | | disabled | `boolean` | `false` | 是否禁用 | | | input-props | `HTMLInputAttributes` | `undefined` | Input 组件内部 input 元素的属性,对 `pair` 类型不生效,[在这里查看原生属性](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)。注意:input-props 不会覆盖内部 input 元素的已经存在的属性(除了 `type`) | | | loading | `boolean` | `undefined` | 是否展示加载图标,设为非 `undefined` 会占据空间 | | diff --git a/src/input/src/Input.tsx b/src/input/src/Input.tsx index f6edede18..45cc7d5a3 100644 --- a/src/input/src/Input.tsx +++ b/src/input/src/Input.tsx @@ -134,6 +134,7 @@ export const inputProps = { onClick: [Function, Array] as PropType void>>, onChange: [Function, Array] as PropType, onClear: [Function, Array] as PropType void>>, + countGraphemes: Function as PropType<(value: string) => number>, status: String as PropType, 'onUpdate:value': [Function, Array] as PropType>, onUpdateValue: [Function, Array] as PropType>, @@ -487,6 +488,22 @@ export default defineComponent({ } } function allowInput (value: string): boolean { + const { countGraphemes, maxlength, minlength } = props + if (countGraphemes) { + let graphemesCount: number | undefined + if (maxlength !== undefined) { + if (graphemesCount === undefined) { + graphemesCount = countGraphemes(value) + } + if (graphemesCount > Number(maxlength)) return false + } + if (minlength !== undefined) { + if (graphemesCount === undefined) { + graphemesCount = countGraphemes(value) + } + if (graphemesCount < Number(maxlength)) return false + } + } const { allowInput } = props if (typeof allowInput === 'function') { return allowInput(value) @@ -780,7 +797,8 @@ export default defineComponent({ provide(inputInjectionKey, { mergedValueRef, maxlengthRef, - mergedClsPrefixRef + mergedClsPrefixRef, + countGraphemesRef: toRef(props, 'countGraphemes') }) const exposedProps: InputWrappedRef = { @@ -978,7 +996,14 @@ export default defineComponent({ } }, render () { - const { mergedClsPrefix, mergedStatus, themeClass, type, onRender } = this + const { + mergedClsPrefix, + mergedStatus, + themeClass, + type, + countGraphemes, + onRender + } = this const $slots = this.$slots as { prefix?: () => VNode[] suffix?: () => VNode[] @@ -1069,8 +1094,8 @@ export default defineComponent({ placeholder={this.placeholder as string | undefined} value={this.mergedValue as string | undefined} disabled={this.mergedDisabled} - maxlength={this.maxlength as any} - minlength={this.minlength as any} + maxlength={countGraphemes ? undefined : this.maxlength} + minlength={countGraphemes ? undefined : this.minlength} readonly={this.readonly as any} tabindex={ this.passivelyActivated && !this.activated @@ -1145,8 +1170,8 @@ export default defineComponent({ } placeholder={this.mergedPlaceholder[0]} disabled={this.mergedDisabled} - maxlength={this.maxlength as any} - minlength={this.minlength as any} + maxlength={countGraphemes ? undefined : this.maxlength} + minlength={countGraphemes ? undefined : this.minlength} value={ Array.isArray(this.mergedValue) ? this.mergedValue[0] @@ -1267,8 +1292,8 @@ export default defineComponent({ } placeholder={this.mergedPlaceholder[1]} disabled={this.mergedDisabled} - maxlength={this.maxlength as any} - minlength={this.minlength as any} + maxlength={countGraphemes ? undefined : this.maxlength} + minlength={countGraphemes ? undefined : this.minlength} value={ Array.isArray(this.mergedValue) ? this.mergedValue[1] diff --git a/src/input/src/WordCount.tsx b/src/input/src/WordCount.tsx index 368429e86..32238f445 100644 --- a/src/input/src/WordCount.tsx +++ b/src/input/src/WordCount.tsx @@ -6,13 +6,18 @@ import { len } from './utils' export default defineComponent({ name: 'InputWordCount', setup (_, { slots }) { - const { mergedValueRef, maxlengthRef, mergedClsPrefixRef } = + const { + mergedValueRef, + maxlengthRef, + mergedClsPrefixRef, + countGraphemesRef + } = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion inject(inputInjectionKey)! const wordCountRef = computed(() => { const { value: mergedValue } = mergedValueRef if (mergedValue === null || Array.isArray(mergedValue)) return 0 - return len(mergedValue) + return (countGraphemesRef.value || len)(mergedValue) }) return () => { const { value: maxlength } = maxlengthRef diff --git a/src/input/src/interface.ts b/src/input/src/interface.ts index 4c7f937aa..77bd88f30 100644 --- a/src/input/src/interface.ts +++ b/src/input/src/interface.ts @@ -22,6 +22,7 @@ export interface InputWrappedRef { export type InputInst = UnwrapRef export const inputInjectionKey = createInjectionKey<{ + countGraphemesRef: Ref<((input: string) => number) | undefined> mergedValueRef: Ref maxlengthRef: Ref mergedClsPrefixRef: Ref diff --git a/vite.config.js b/vite.config.js index 1e692088b..e36d5b56b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -48,6 +48,7 @@ module.exports = { '@css-render/vue3-ssr', 'date-fns-tz', 'codesandbox/lib/api/define', + 'grapheme-splitter', 'highlight.js/lib/core', 'highlight.js/lib/languages/javascript', 'highlight.js/lib/languages/python', @@ -82,6 +83,11 @@ module.exports = { }, build: { outDir: 'site', + output: { + manualChunks: { + 'grapheme-splitter': ['grapheme-splitter'] + } + }, rollupOptions: { plugins: [ babel({