Skip to content

Commit

Permalink
Fix Input.TextArea cut text logic when maxLength configured.
Browse files Browse the repository at this point in the history
  • Loading branch information
tangjinzhou committed Mar 17, 2022
1 parent ab26180 commit d929217
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 8 deletions.
9 changes: 7 additions & 2 deletions components/input/ClearableLabeledInput.tsx
Expand Up @@ -33,6 +33,7 @@ export default defineComponent({
focused: PropTypes.looseBool,
bordered: PropTypes.looseBool.def(true),
triggerFocus: { type: Function as PropType<() => void> },
hidden: Boolean,
},
setup(props, { slots, attrs }) {
const containerRef = ref();
Expand Down Expand Up @@ -91,6 +92,7 @@ export default defineComponent({
direction,
readonly,
bordered,
hidden,
addonAfter = slots.addonAfter,
addonBefore = slots.addonBefore,
} = props;
Expand Down Expand Up @@ -121,6 +123,7 @@ export default defineComponent({
class={affixWrapperCls}
style={attrs.style}
onMouseup={onInputMouseUp}
hidden={hidden}
>
{prefixNode}
{cloneElement(element, {
Expand All @@ -139,6 +142,7 @@ export default defineComponent({
addonAfter = slots.addonAfter?.(),
size,
direction,
hidden,
} = props;
// Not wrap when there is not addons
if (!hasAddon({ addonBefore, addonAfter })) {
Expand Down Expand Up @@ -169,7 +173,7 @@ export default defineComponent({
// Need another wrapper for changing display:table to display:inline-block
// and put style prop in wrapper
return (
<span class={mergedGroupClassName} style={attrs.style}>
<span class={mergedGroupClassName} style={attrs.style} hidden={hidden}>
<span class={mergedWrapperClassName}>
{addonBeforeNode}
{cloneElement(labeledElement, { style: null })}
Expand All @@ -185,6 +189,7 @@ export default defineComponent({
allowClear,
direction,
bordered,
hidden,
addonAfter = slots.addonAfter,
addonBefore = slots.addonBefore,
} = props;
Expand All @@ -204,7 +209,7 @@ export default defineComponent({
},
);
return (
<span class={affixWrapperCls} style={attrs.style}>
<span class={affixWrapperCls} style={attrs.style} hidden={hidden}>
{cloneElement(element, {
style: null,
value,
Expand Down
81 changes: 75 additions & 6 deletions components/input/TextArea.tsx
Expand Up @@ -24,6 +24,26 @@ function fixEmojiLength(value: string, maxLength: number) {
return [...(value || '')].slice(0, maxLength).join('');
}

function setTriggerValue(
isCursorInEnd: boolean,
preValue: string,
triggerValue: string,
maxLength: number,
) {
let newTriggerValue = triggerValue;
if (isCursorInEnd) {
// 光标在尾部,直接截断
newTriggerValue = fixEmojiLength(triggerValue, maxLength!);
} else if (
[...(preValue || '')].length < triggerValue.length &&
[...(triggerValue || '')].length > maxLength!
) {
// 光标在中间,如果最后的值超过最大值,则采用原先的值
newTriggerValue = preValue;
}
return newTriggerValue;
}

export default defineComponent({
name: 'ATextarea',
inheritAttrs: false,
Expand All @@ -40,6 +60,40 @@ export default defineComponent({
// Max length value
const hasMaxLength = computed(() => Number(props.maxlength) > 0);
const compositing = ref(false);

const oldCompositionValueRef = ref<string>();
const oldSelectionStartRef = ref<number>(0);
const onInternalCompositionStart = (e: CompositionEvent) => {
compositing.value = true;
// 拼音输入前保存一份旧值
oldCompositionValueRef.value = mergedValue.value as string;
// 保存旧的光标位置
oldSelectionStartRef.value = (e.currentTarget as any).selectionStart;
emit('compositionstart', e);
};

const onInternalCompositionEnd = (e: CompositionEvent) => {
compositing.value = false;
let triggerValue = (e.currentTarget as any).value;
if (hasMaxLength.value) {
const isCursorInEnd =
oldSelectionStartRef.value >= props.maxlength + 1 ||
oldSelectionStartRef.value === oldCompositionValueRef.value?.length;
triggerValue = setTriggerValue(
isCursorInEnd,
oldCompositionValueRef.value as string,
triggerValue,
props.maxlength,
);
}
// Patch composition onChange when value changed
if (triggerValue !== mergedValue.value) {
setValue(triggerValue);
resolveOnChange(e.currentTarget as any, e, triggerChange, triggerValue);
}

emit('compositionend', e);
};
const instance = getCurrentInstance();
watch(
() => props.value,
Expand Down Expand Up @@ -103,12 +157,24 @@ export default defineComponent({
};

const handleChange = (e: Event) => {
const { value, composing } = e.target as any;
compositing.value = (e as any).isComposing || composing;
if ((compositing.value && props.lazy) || stateValue.value === value) return;
let triggerValue = (e.currentTarget as any).value;
const { composing } = e.target as any;
let triggerValue = (e.target as any).value;
compositing.value = !!((e as any).isComposing || composing);
if ((compositing.value && props.lazy) || stateValue.value === triggerValue) return;

if (hasMaxLength.value) {
triggerValue = fixEmojiLength(triggerValue, props.maxlength!);
// 1. 复制粘贴超过maxlength的情况 2.未超过maxlength的情况
const target = e.target as any;
const isCursorInEnd =
target.selectionStart >= props.maxlength! + 1 ||
target.selectionStart === triggerValue.length ||
!target.selectionStart;
triggerValue = setTriggerValue(
isCursorInEnd,
mergedValue.value as string,
triggerValue,
props.maxlength!,
);
}
resolveOnChange(e.currentTarget as any, e, triggerChange, triggerValue);
setValue(triggerValue);
Expand All @@ -132,6 +198,8 @@ export default defineComponent({
onChange: handleChange,
onBlur,
onKeydown: handleKeyDown,
onCompositionstart: onInternalCompositionStart,
onCompositionend: onInternalCompositionEnd,
};
if (props.valueModifiers?.lazy) {
delete resizeProps.onInput;
Expand Down Expand Up @@ -172,7 +240,7 @@ export default defineComponent({
mergedValue.value = val;
});
return () => {
const { maxlength, bordered = true } = props;
const { maxlength, bordered = true, hidden } = props;
const { style, class: customClass } = attrs;

const inputProps: any = {
Expand Down Expand Up @@ -204,6 +272,7 @@ export default defineComponent({
}
textareaNode = (
<div
hidden={hidden}
class={classNames(
`${prefixCls.value}-textarea`,
{
Expand Down
3 changes: 3 additions & 0 deletions components/input/demo/textarea.vue
Expand Up @@ -17,6 +17,9 @@ For multi-line input.
</docs>
<template>
<a-textarea v-model:value="value" placeholder="Basic usage" :rows="4" />
<br />
<br />
<a-textarea :rows="4" placeholder="maxLength is 6" :maxlength="6" />
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
Expand Down
1 change: 1 addition & 0 deletions components/input/inputProps.ts
Expand Up @@ -72,6 +72,7 @@ const inputProps = {
onInput: PropTypes.func,
'onUpdate:value': PropTypes.func,
valueModifiers: Object,
hidden: Boolean,
};
export default inputProps;
export type InputProps = Partial<ExtractPropTypes<typeof inputProps>>;
Expand Down

0 comments on commit d929217

Please sign in to comment.