fix: input composing error, close #7516

pull/7519/head
tangjinzhou 2024-04-22 15:11:10 +08:00
parent 9b0f0e71e7
commit 752686e334
9 changed files with 190 additions and 150 deletions

View File

@ -1,51 +1,145 @@
import { defineComponent, shallowRef, withDirectives } from 'vue'; import type { PropType } from 'vue';
import antInput from './antInputDirective'; import { defineComponent, shallowRef, ref, watch } from 'vue';
import PropTypes from './vue-types'; import PropTypes from './vue-types';
export interface BaseInputExpose {
focus: () => void;
blur: () => void;
input: HTMLInputElement | HTMLTextAreaElement | null;
setSelectionRange: (
start: number,
end: number,
direction?: 'forward' | 'backward' | 'none',
) => void;
select: () => void;
getSelectionStart: () => number | null;
getSelectionEnd: () => number | null;
getScrollTop: () => number | null;
setScrollTop: (scrollTop: number) => void;
}
const BaseInput = defineComponent({ const BaseInput = defineComponent({
compatConfig: { MODE: 3 }, compatConfig: { MODE: 3 },
inheritAttrs: false,
props: { props: {
value: PropTypes.string.def(''), disabled: PropTypes.looseBool,
type: PropTypes.string,
value: PropTypes.any,
lazy: PropTypes.bool.def(true),
tag: {
type: String as PropType<'input' | 'textarea'>,
default: 'input',
},
size: PropTypes.string,
}, },
emits: ['change', 'input'], emits: [
setup(_p, { emit }) { 'change',
'input',
'blur',
'keydown',
'focus',
'compositionstart',
'compositionend',
'keyup',
],
setup(props, { emit, attrs, expose }) {
const inputRef = shallowRef(null); const inputRef = shallowRef(null);
const renderValue = ref();
const isComposing = ref(false);
watch(
[() => props.value, isComposing],
() => {
if (isComposing.value) return;
renderValue.value = props.value;
},
{ immediate: true },
);
const handleChange = (e: Event) => { const handleChange = (e: Event) => {
const { composing } = e.target as any; emit('change', e);
if ((e as any).isComposing || composing) { };
emit('input', e); const onCompositionstart = (e: CompositionEvent) => {
} else { isComposing.value = true;
emit('input', e); (e.target as any).composing = true;
emit('change', e); emit('compositionstart', e);
};
const onCompositionend = (e: CompositionEvent) => {
isComposing.value = false;
(e.target as any).composing = false;
emit('compositionend', e);
const event = document.createEvent('HTMLEvents');
event.initEvent('input', true, true);
e.target.dispatchEvent(event);
};
const handleInput = (e: Event) => {
if (isComposing.value && props.lazy) {
renderValue.value = (e.target as HTMLInputElement).value;
return;
}
emit('input', e);
};
const handleBlur = (e: Event) => {
emit('blur', e);
};
const handleFocus = (e: Event) => {
emit('focus', e);
};
const focus = () => {
if (inputRef.value) {
inputRef.value.focus();
} }
}; };
return { const blur = () => {
inputRef, if (inputRef.value) {
focus: () => { inputRef.value.blur();
if (inputRef.value) { }
inputRef.value.focus();
}
},
blur: () => {
if (inputRef.value) {
inputRef.value.blur();
}
},
handleChange,
}; };
}, const handleKeyDown = (e: KeyboardEvent) => {
render() { emit('keydown', e);
return withDirectives( };
( const handleKeyUp = (e: KeyboardEvent) => {
<input emit('keyup', e);
{...this.$props} };
{...this.$attrs} const setSelectionRange = (
onInput={this.handleChange} start: number,
onChange={this.handleChange} end: number,
ref="inputRef" direction?: 'forward' | 'backward' | 'none',
) => {
inputRef.value?.setSelectionRange(start, end, direction);
};
const select = () => {
inputRef.value?.select();
};
expose({
focus,
blur,
input: inputRef,
setSelectionRange,
select,
getSelectionStart: () => inputRef.value?.selectionStart,
getSelectionEnd: () => inputRef.value?.selectionEnd,
getScrollTop: () => inputRef.value?.scrollTop,
});
return () => {
const { tag: Tag, ...restProps } = props;
return (
<Tag
{...restProps}
{...attrs}
onInput={handleInput}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
ref={inputRef}
value={renderValue.value}
onCompositionstart={onCompositionstart}
onCompositionend={onCompositionend}
onKeyup={handleKeyUp}
onKeydown={handleKeyDown}
/> />
) as any, );
[[antInput]], };
);
}, },
}); });

View File

@ -1,42 +0,0 @@
import type { Directive } from 'vue';
function onCompositionStart(e: any) {
e.target.composing = true;
}
function onCompositionEnd(e: any) {
// prevent triggering an input event for no reason
if (!e.target.composing) return;
e.target.composing = false;
trigger(e.target, 'input');
}
function trigger(el, type) {
const e = document.createEvent('HTMLEvents');
e.initEvent(type, true, true);
el.dispatchEvent(e);
}
export function addEventListener(
el: HTMLElement,
event: string,
handler: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
) {
el.addEventListener(event, handler, options);
}
const antInput: Directive = {
created(el, binding) {
if (!binding.modifiers || !binding.modifiers.lazy) {
addEventListener(el, 'compositionstart', onCompositionStart);
addEventListener(el, 'compositionend', onCompositionEnd);
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
addEventListener(el, 'change', onCompositionEnd);
}
},
};
export default antInput;

View File

@ -1,4 +1,4 @@
import type { VNode, CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { import {
computed, computed,
watchEffect, watchEffect,
@ -7,16 +7,16 @@ import {
onBeforeUnmount, onBeforeUnmount,
ref, ref,
defineComponent, defineComponent,
withDirectives,
} from 'vue'; } from 'vue';
import ResizeObserver from '../vc-resize-observer'; import ResizeObserver from '../vc-resize-observer';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import raf from '../_util/raf'; import raf from '../_util/raf';
import warning from '../_util/warning'; import warning from '../_util/warning';
import antInput from '../_util/antInputDirective';
import omit from '../_util/omit'; import omit from '../_util/omit';
import { textAreaProps } from './inputProps'; import { textAreaProps } from './inputProps';
import calculateAutoSizeStyle from './calculateNodeHeight'; import calculateAutoSizeStyle from './calculateNodeHeight';
import type { BaseInputExpose } from '../_util/BaseInput';
import BaseInput from '../_util/BaseInput';
const RESIZE_START = 0; const RESIZE_START = 0;
const RESIZE_MEASURING = 1; const RESIZE_MEASURING = 1;
@ -30,7 +30,7 @@ const ResizableTextArea = defineComponent({
setup(props, { attrs, emit, expose }) { setup(props, { attrs, emit, expose }) {
let nextFrameActionId: any; let nextFrameActionId: any;
let resizeFrameId: any; let resizeFrameId: any;
const textAreaRef = ref(); const textAreaRef = ref<BaseInputExpose>();
const textareaStyles = ref({}); const textareaStyles = ref({});
const resizeStatus = ref(RESIZE_STABLE); const resizeStatus = ref(RESIZE_STABLE);
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -41,12 +41,12 @@ const ResizableTextArea = defineComponent({
// https://github.com/ant-design/ant-design/issues/21870 // https://github.com/ant-design/ant-design/issues/21870
const fixFirefoxAutoScroll = () => { const fixFirefoxAutoScroll = () => {
try { try {
if (document.activeElement === textAreaRef.value) { if (textAreaRef.value && document.activeElement === textAreaRef.value.input) {
const currentStart = textAreaRef.value.selectionStart; const currentStart = textAreaRef.value.getSelectionStart();
const currentEnd = textAreaRef.value.selectionEnd; const currentEnd = textAreaRef.value.getSelectionEnd();
const scrollTop = textAreaRef.value.scrollTop; const scrollTop = textAreaRef.value.getScrollTop();
textAreaRef.value.setSelectionRange(currentStart, currentEnd); textAreaRef.value.setSelectionRange(currentStart, currentEnd);
textAreaRef.value.scrollTop = scrollTop; textAreaRef.value.setScrollTop(scrollTop);
} }
} catch (e) { } catch (e) {
// Fix error in Chrome: // Fix error in Chrome:
@ -88,7 +88,7 @@ const ResizableTextArea = defineComponent({
resizeStatus.value = RESIZE_MEASURING; resizeStatus.value = RESIZE_MEASURING;
} else if (resizeStatus.value === RESIZE_MEASURING) { } else if (resizeStatus.value === RESIZE_MEASURING) {
const textareaStyles = calculateAutoSizeStyle( const textareaStyles = calculateAutoSizeStyle(
textAreaRef.value, textAreaRef.value.input as HTMLTextAreaElement,
false, false,
minRows.value, minRows.value,
maxRows.value, maxRows.value,
@ -127,7 +127,7 @@ const ResizableTextArea = defineComponent({
expose({ expose({
resizeTextarea, resizeTextarea,
textArea: textAreaRef, textArea: computed(() => textAreaRef.value?.input),
instance, instance,
}); });
warning( warning(
@ -146,7 +146,6 @@ const ResizableTextArea = defineComponent({
'defaultValue', 'defaultValue',
'allowClear', 'allowClear',
'type', 'type',
'lazy',
'maxlength', 'maxlength',
'valueModifiers', 'valueModifiers',
]); ]);
@ -175,9 +174,7 @@ const ResizableTextArea = defineComponent({
} }
return ( return (
<ResizeObserver onResize={onInternalResize} disabled={!needAutoSize.value}> <ResizeObserver onResize={onInternalResize} disabled={!needAutoSize.value}>
{withDirectives((<textarea {...textareaProps} ref={textAreaRef} />) as VNode, [ <BaseInput {...textareaProps} ref={textAreaRef} tag="textarea"></BaseInput>
[antInput],
])}
</ResizeObserver> </ResizeObserver>
); );
}; };

View File

@ -170,10 +170,8 @@ export default defineComponent({
}; };
const handleChange = (e: Event) => { const handleChange = (e: Event) => {
const { composing } = e.target as any;
let triggerValue = (e.target as any).value; let triggerValue = (e.target as any).value;
compositing.value = !!((e as any).isComposing && composing); if (stateValue.value === triggerValue) return;
if ((compositing.value && props.lazy) || stateValue.value === triggerValue) return;
if (hasMaxLength.value) { if (hasMaxLength.value) {
// 1. maxlength 2.maxlength // 1. maxlength 2.maxlength
@ -227,6 +225,7 @@ export default defineComponent({
id={resizeProps?.id ?? formItemContext.id.value} id={resizeProps?.id ?? formItemContext.id.value}
ref={resizableTextArea} ref={resizableTextArea}
maxlength={props.maxlength} maxlength={props.maxlength}
lazy={props.lazy}
/> />
); );
}; };

View File

@ -1,6 +1,6 @@
// base 0.0.1-alpha.7 // base 0.0.1-alpha.7
import type { ComponentPublicInstance, VNode } from 'vue'; import type { ComponentPublicInstance } from 'vue';
import { onMounted, defineComponent, nextTick, shallowRef, watch, withDirectives } from 'vue'; import { computed, onMounted, defineComponent, nextTick, shallowRef, watch } from 'vue';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import type { ChangeEvent, FocusEventHandler } from '../_util/EventInterface'; import type { ChangeEvent, FocusEventHandler } from '../_util/EventInterface';
import omit from '../_util/omit'; import omit from '../_util/omit';
@ -14,8 +14,8 @@ import {
resolveOnChange, resolveOnChange,
triggerFocus, triggerFocus,
} from './utils/commonUtils'; } from './utils/commonUtils';
import antInputDirective from '../_util/antInputDirective';
import BaseInput from './BaseInput'; import BaseInput from './BaseInput';
import BaseInputCore from '../_util/BaseInput';
export default defineComponent({ export default defineComponent({
name: 'VCInput', name: 'VCInput',
@ -65,7 +65,7 @@ export default defineComponent({
expose({ expose({
focus, focus,
blur, blur,
input: inputRef, input: computed(() => (inputRef.value as any)?.input),
stateValue, stateValue,
setSelectionRange, setSelectionRange,
select, select,
@ -91,9 +91,8 @@ export default defineComponent({
}); });
}; };
const handleChange = (e: ChangeEvent) => { const handleChange = (e: ChangeEvent) => {
const { value, composing } = e.target as any; const { value } = e.target as any;
// https://github.com/vueComponent/ant-design-vue/issues/2203 if (stateValue.value === value) return;
if (((e as any).isComposing && composing && props.lazy) || stateValue.value === value) return;
const newVal = e.target.value; const newVal = e.target.value;
resolveOnChange(inputRef.value, e, triggerChange); resolveOnChange(inputRef.value, e, triggerChange);
setValue(newVal); setValue(newVal);
@ -184,6 +183,7 @@ export default defineComponent({
key: 'ant-input', key: 'ant-input',
size: htmlSize, size: htmlSize,
type, type,
lazy: props.lazy,
}; };
if (valueModifiers.lazy) { if (valueModifiers.lazy) {
delete inputProps.onInput; delete inputProps.onInput;
@ -191,8 +191,8 @@ export default defineComponent({
if (!inputProps.autofocus) { if (!inputProps.autofocus) {
delete inputProps.autofocus; delete inputProps.autofocus;
} }
const inputNode = <input {...omit(inputProps, ['size'])} />; const inputNode = <BaseInputCore {...omit(inputProps, ['size'])} />;
return withDirectives(inputNode as VNode, [[antInputDirective]]); return inputNode;
}; };
const getSuffix = () => { const getSuffix = () => {
const { maxlength, suffix = slots.suffix?.(), showCount, prefixCls } = props; const { maxlength, suffix = slots.suffix?.(), showCount, prefixCls } = props;

View File

@ -4,7 +4,6 @@ import {
watchEffect, watchEffect,
defineComponent, defineComponent,
provide, provide,
withDirectives,
ref, ref,
reactive, reactive,
onUpdated, onUpdated,
@ -24,9 +23,10 @@ import KeywordTrigger from './KeywordTrigger';
import { vcMentionsProps, defaultProps } from './mentionsProps'; import { vcMentionsProps, defaultProps } from './mentionsProps';
import type { OptionProps } from './Option'; import type { OptionProps } from './Option';
import MentionsContextKey from './MentionsContext'; import MentionsContextKey from './MentionsContext';
import antInputDirective from '../../_util/antInputDirective';
import omit from '../../_util/omit'; import omit from '../../_util/omit';
import type { EventHandler } from '../../_util/EventInterface'; import type { EventHandler } from '../../_util/EventInterface';
import type { BaseInputExpose } from '../../_util/BaseInput';
import BaseInput from '../../_util/BaseInput';
export type MentionsProps = Partial<ExtractPropTypes<typeof vcMentionsProps>>; export type MentionsProps = Partial<ExtractPropTypes<typeof vcMentionsProps>>;
@ -40,7 +40,7 @@ export default defineComponent({
emits: ['change', 'select', 'search', 'focus', 'blur', 'pressenter'], emits: ['change', 'select', 'search', 'focus', 'blur', 'pressenter'],
setup(props, { emit, attrs, expose, slots }) { setup(props, { emit, attrs, expose, slots }) {
const measure = ref(null); const measure = ref(null);
const textarea = ref(null); const textarea = ref<BaseInputExpose>(null);
const focusId = ref(); const focusId = ref();
const state = reactive({ const state = reactive({
value: props.value || '', value: props.value || '',
@ -60,8 +60,7 @@ export default defineComponent({
emit('change', val); emit('change', val);
}; };
const onChange: EventHandler = ({ target: { value, composing }, isComposing }) => { const onChange: EventHandler = ({ target: { value } }) => {
if (isComposing || composing) return;
triggerChange(value); triggerChange(value);
}; };
@ -194,13 +193,13 @@ export default defineComponent({
measureLocation: state.measureLocation, measureLocation: state.measureLocation,
targetText: mentionValue, targetText: mentionValue,
prefix: state.measurePrefix, prefix: state.measurePrefix,
selectionStart: textarea.value.selectionStart, selectionStart: textarea.value.getSelectionStart(),
split, split,
}); });
triggerChange(text); triggerChange(text);
stopMeasure(() => { stopMeasure(() => {
// We need restore the selection position // We need restore the selection position
setInputSelection(textarea.value, selectionLocation); setInputSelection(textarea.value.input as HTMLTextAreaElement, selectionLocation);
}); });
emit('select', option, state.measurePrefix); emit('select', option, state.measurePrefix);
@ -243,7 +242,7 @@ export default defineComponent({
onUpdated(() => { onUpdated(() => {
nextTick(() => { nextTick(() => {
if (state.measuring) { if (state.measuring) {
measure.value.scrollTop = textarea.value.scrollTop; measure.value.scrollTop = textarea.value.getScrollTop();
} }
}); });
}); });
@ -279,7 +278,7 @@ export default defineComponent({
}; };
return ( return (
<div class={classNames(prefixCls, className)} style={style as CSSProperties}> <div class={classNames(prefixCls, className)} style={style as CSSProperties}>
{withDirectives(<textarea ref={textarea} {...textareaProps} />, [[antInputDirective]])} <BaseInput {...textareaProps} ref={textarea} tag="textarea"></BaseInput>
{measuring && ( {measuring && (
<div ref={measure} class={`${prefixCls}-measure`}> <div ref={measure} class={`${prefixCls}-measure`}>
{state.value.slice(0, measureLocation)} {state.value.slice(0, measureLocation)}

View File

@ -1,8 +1,8 @@
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import KEYCODE from './KeyCode'; import KEYCODE from './KeyCode';
import { computed, defineComponent, ref, withDirectives } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import antInput from '../_util/antInputDirective';
import type { EventHandler } from '../_util/EventInterface'; import type { EventHandler } from '../_util/EventInterface';
import BaseInput from '../_util/BaseInput';
export default defineComponent({ export default defineComponent({
compatConfig: { MODE: 3 }, compatConfig: { MODE: 3 },
@ -32,8 +32,8 @@ export default defineComponent({
return `${opt.value} ${props.locale.items_per_page}`; return `${opt.value} ${props.locale.items_per_page}`;
}; };
const handleChange: EventHandler = e => { const handleChange: EventHandler = e => {
const { value, composing } = e.target; const { value } = e.target;
if (e.isComposing || composing || goInputText.value === value) return; if (goInputText.value === value) return;
goInputText.value = value; goInputText.value = value;
}; };
const handleBlur: EventHandler = e => { const handleBlur: EventHandler = e => {
@ -147,18 +147,15 @@ export default defineComponent({
goInput = ( goInput = (
<div class={`${prefixCls}-quick-jumper`}> <div class={`${prefixCls}-quick-jumper`}>
{locale.jump_to} {locale.jump_to}
{withDirectives( <BaseInput
<input disabled={disabled}
disabled={disabled} type="text"
type="text" value={goInputText.value}
value={goInputText.value} onInput={handleChange}
onInput={handleChange} onChange={handleChange}
onChange={handleChange} onKeyup={go}
onKeyup={go} onBlur={handleBlur}
onBlur={handleBlur} ></BaseInput>
/>,
[[antInput]],
)}
{locale.page} {locale.page}
{gotoButton} {gotoButton}
</div> </div>

View File

@ -6,10 +6,10 @@ import Options from './Options';
import LOCALE from './locale/zh_CN'; import LOCALE from './locale/zh_CN';
import KEYCODE from './KeyCode'; import KEYCODE from './KeyCode';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { defineComponent, withDirectives } from 'vue'; import { defineComponent } from 'vue';
import antInput from '../_util/antInputDirective';
import { cloneElement } from '../_util/vnode'; import { cloneElement } from '../_util/vnode';
import firstNotUndefined from '../_util/firstNotUndefined'; import firstNotUndefined from '../_util/firstNotUndefined';
import BaseInput from '../_util/BaseInput';
// //
function isInteger(value) { function isInteger(value) {
@ -181,7 +181,6 @@ export default defineComponent({
} }
}, },
handleKeyUp(e) { handleKeyUp(e) {
if (e.isComposing || e.target.composing) return;
const value = this.getValidValue(e); const value = this.getValidValue(e);
const stateCurrentInputValue = this.stateCurrentInputValue; const stateCurrentInputValue = this.stateCurrentInputValue;
@ -423,19 +422,16 @@ export default defineComponent({
title={showTitle ? `${stateCurrent}/${allPages}` : null} title={showTitle ? `${stateCurrent}/${allPages}` : null}
class={`${prefixCls}-simple-pager`} class={`${prefixCls}-simple-pager`}
> >
{withDirectives( <BaseInput
<input type="text"
type="text" value={this.stateCurrentInputValue}
value={this.stateCurrentInputValue} disabled={disabled}
disabled={disabled} onKeydown={this.handleKeyDown}
onKeydown={this.handleKeyDown} onKeyup={this.handleKeyUp}
onKeyup={this.handleKeyUp} onInput={this.handleKeyUp}
onInput={this.handleKeyUp} onChange={this.handleKeyUp}
onChange={this.handleKeyUp} size="3"
size="3" ></BaseInput>
/>,
[[antInput]],
)}
<span class={`${prefixCls}-slash`}></span> <span class={`${prefixCls}-slash`}></span>
{allPages} {allPages}
</li> </li>

View File

@ -1,8 +1,7 @@
import { cloneElement } from '../../_util/vnode'; import { cloneElement } from '../../_util/vnode';
import type { ExtractPropTypes, PropType, VNode } from 'vue'; import type { ExtractPropTypes, PropType, VNode } from 'vue';
import { defineComponent, inject, withDirectives } from 'vue'; import { defineComponent, inject } from 'vue';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import antInput from '../../_util/antInputDirective';
import classNames from '../../_util/classNames'; import classNames from '../../_util/classNames';
import type { import type {
FocusEventHandler, FocusEventHandler,
@ -12,6 +11,7 @@ import type {
CompositionEventHandler, CompositionEventHandler,
ClipboardEventHandler, ClipboardEventHandler,
} from '../../_util/EventInterface'; } from '../../_util/EventInterface';
import BaseInput from '../../_util/BaseInput';
export const inputProps = { export const inputProps = {
inputRef: PropTypes.any, inputRef: PropTypes.any,
@ -74,7 +74,7 @@ const Input = defineComponent({
attrs, attrs,
} = props; } = props;
let inputNode: any = inputElement || withDirectives((<input />) as VNode, [[antInput]]); let inputNode: any = inputElement || <BaseInput></BaseInput>;
const inputProps = inputNode.props || {}; const inputProps = inputNode.props || {};
const { const {