Skip to content

Commit d929217

Browse files
committed
Fix Input.TextArea cut text logic when maxLength configured.
1 parent ab26180 commit d929217

File tree

4 files changed

+86
-8
lines changed

4 files changed

+86
-8
lines changed

components/input/ClearableLabeledInput.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export default defineComponent({
3333
focused: PropTypes.looseBool,
3434
bordered: PropTypes.looseBool.def(true),
3535
triggerFocus: { type: Function as PropType<() => void> },
36+
hidden: Boolean,
3637
},
3738
setup(props, { slots, attrs }) {
3839
const containerRef = ref();
@@ -91,6 +92,7 @@ export default defineComponent({
9192
direction,
9293
readonly,
9394
bordered,
95+
hidden,
9496
addonAfter = slots.addonAfter,
9597
addonBefore = slots.addonBefore,
9698
} = props;
@@ -121,6 +123,7 @@ export default defineComponent({
121123
class={affixWrapperCls}
122124
style={attrs.style}
123125
onMouseup={onInputMouseUp}
126+
hidden={hidden}
124127
>
125128
{prefixNode}
126129
{cloneElement(element, {
@@ -139,6 +142,7 @@ export default defineComponent({
139142
addonAfter = slots.addonAfter?.(),
140143
size,
141144
direction,
145+
hidden,
142146
} = props;
143147
// Not wrap when there is not addons
144148
if (!hasAddon({ addonBefore, addonAfter })) {
@@ -169,7 +173,7 @@ export default defineComponent({
169173
// Need another wrapper for changing display:table to display:inline-block
170174
// and put style prop in wrapper
171175
return (
172-
<span class={mergedGroupClassName} style={attrs.style}>
176+
<span class={mergedGroupClassName} style={attrs.style} hidden={hidden}>
173177
<span class={mergedWrapperClassName}>
174178
{addonBeforeNode}
175179
{cloneElement(labeledElement, { style: null })}
@@ -185,6 +189,7 @@ export default defineComponent({
185189
allowClear,
186190
direction,
187191
bordered,
192+
hidden,
188193
addonAfter = slots.addonAfter,
189194
addonBefore = slots.addonBefore,
190195
} = props;
@@ -204,7 +209,7 @@ export default defineComponent({
204209
},
205210
);
206211
return (
207-
<span class={affixWrapperCls} style={attrs.style}>
212+
<span class={affixWrapperCls} style={attrs.style} hidden={hidden}>
208213
{cloneElement(element, {
209214
style: null,
210215
value,

components/input/TextArea.tsx

+75-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ function fixEmojiLength(value: string, maxLength: number) {
2424
return [...(value || '')].slice(0, maxLength).join('');
2525
}
2626

27+
function setTriggerValue(
28+
isCursorInEnd: boolean,
29+
preValue: string,
30+
triggerValue: string,
31+
maxLength: number,
32+
) {
33+
let newTriggerValue = triggerValue;
34+
if (isCursorInEnd) {
35+
// 光标在尾部,直接截断
36+
newTriggerValue = fixEmojiLength(triggerValue, maxLength!);
37+
} else if (
38+
[...(preValue || '')].length < triggerValue.length &&
39+
[...(triggerValue || '')].length > maxLength!
40+
) {
41+
// 光标在中间,如果最后的值超过最大值,则采用原先的值
42+
newTriggerValue = preValue;
43+
}
44+
return newTriggerValue;
45+
}
46+
2747
export default defineComponent({
2848
name: 'ATextarea',
2949
inheritAttrs: false,
@@ -40,6 +60,40 @@ export default defineComponent({
4060
// Max length value
4161
const hasMaxLength = computed(() => Number(props.maxlength) > 0);
4262
const compositing = ref(false);
63+
64+
const oldCompositionValueRef = ref<string>();
65+
const oldSelectionStartRef = ref<number>(0);
66+
const onInternalCompositionStart = (e: CompositionEvent) => {
67+
compositing.value = true;
68+
// 拼音输入前保存一份旧值
69+
oldCompositionValueRef.value = mergedValue.value as string;
70+
// 保存旧的光标位置
71+
oldSelectionStartRef.value = (e.currentTarget as any).selectionStart;
72+
emit('compositionstart', e);
73+
};
74+
75+
const onInternalCompositionEnd = (e: CompositionEvent) => {
76+
compositing.value = false;
77+
let triggerValue = (e.currentTarget as any).value;
78+
if (hasMaxLength.value) {
79+
const isCursorInEnd =
80+
oldSelectionStartRef.value >= props.maxlength + 1 ||
81+
oldSelectionStartRef.value === oldCompositionValueRef.value?.length;
82+
triggerValue = setTriggerValue(
83+
isCursorInEnd,
84+
oldCompositionValueRef.value as string,
85+
triggerValue,
86+
props.maxlength,
87+
);
88+
}
89+
// Patch composition onChange when value changed
90+
if (triggerValue !== mergedValue.value) {
91+
setValue(triggerValue);
92+
resolveOnChange(e.currentTarget as any, e, triggerChange, triggerValue);
93+
}
94+
95+
emit('compositionend', e);
96+
};
4397
const instance = getCurrentInstance();
4498
watch(
4599
() => props.value,
@@ -103,12 +157,24 @@ export default defineComponent({
103157
};
104158

105159
const handleChange = (e: Event) => {
106-
const { value, composing } = e.target as any;
107-
compositing.value = (e as any).isComposing || composing;
108-
if ((compositing.value && props.lazy) || stateValue.value === value) return;
109-
let triggerValue = (e.currentTarget as any).value;
160+
const { composing } = e.target as any;
161+
let triggerValue = (e.target as any).value;
162+
compositing.value = !!((e as any).isComposing || composing);
163+
if ((compositing.value && props.lazy) || stateValue.value === triggerValue) return;
164+
110165
if (hasMaxLength.value) {
111-
triggerValue = fixEmojiLength(triggerValue, props.maxlength!);
166+
// 1. 复制粘贴超过maxlength的情况 2.未超过maxlength的情况
167+
const target = e.target as any;
168+
const isCursorInEnd =
169+
target.selectionStart >= props.maxlength! + 1 ||
170+
target.selectionStart === triggerValue.length ||
171+
!target.selectionStart;
172+
triggerValue = setTriggerValue(
173+
isCursorInEnd,
174+
mergedValue.value as string,
175+
triggerValue,
176+
props.maxlength!,
177+
);
112178
}
113179
resolveOnChange(e.currentTarget as any, e, triggerChange, triggerValue);
114180
setValue(triggerValue);
@@ -132,6 +198,8 @@ export default defineComponent({
132198
onChange: handleChange,
133199
onBlur,
134200
onKeydown: handleKeyDown,
201+
onCompositionstart: onInternalCompositionStart,
202+
onCompositionend: onInternalCompositionEnd,
135203
};
136204
if (props.valueModifiers?.lazy) {
137205
delete resizeProps.onInput;
@@ -172,7 +240,7 @@ export default defineComponent({
172240
mergedValue.value = val;
173241
});
174242
return () => {
175-
const { maxlength, bordered = true } = props;
243+
const { maxlength, bordered = true, hidden } = props;
176244
const { style, class: customClass } = attrs;
177245

178246
const inputProps: any = {
@@ -204,6 +272,7 @@ export default defineComponent({
204272
}
205273
textareaNode = (
206274
<div
275+
hidden={hidden}
207276
class={classNames(
208277
`${prefixCls.value}-textarea`,
209278
{

components/input/demo/textarea.vue

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ For multi-line input.
1717
</docs>
1818
<template>
1919
<a-textarea v-model:value="value" placeholder="Basic usage" :rows="4" />
20+
<br />
21+
<br />
22+
<a-textarea :rows="4" placeholder="maxLength is 6" :maxlength="6" />
2023
</template>
2124
<script lang="ts">
2225
import { defineComponent, ref } from 'vue';

components/input/inputProps.ts

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const inputProps = {
7272
onInput: PropTypes.func,
7373
'onUpdate:value': PropTypes.func,
7474
valueModifiers: Object,
75+
hidden: Boolean,
7576
};
7677
export default inputProps;
7778
export type InputProps = Partial<ExtractPropTypes<typeof inputProps>>;

0 commit comments

Comments
 (0)