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({