Browse Source

fix: input composing error, close #7516

pull/7519/head
tangjinzhou 7 months ago
parent
commit
752686e334
  1. 168
      components/_util/BaseInput.tsx
  2. 42
      components/_util/antInputDirective.ts
  3. 27
      components/input/ResizableTextArea.tsx
  4. 5
      components/input/TextArea.tsx
  5. 18
      components/vc-input/Input.tsx
  6. 17
      components/vc-mentions/src/Mentions.tsx
  7. 29
      components/vc-pagination/Options.tsx
  8. 28
      components/vc-pagination/Pagination.tsx
  9. 6
      components/vc-select/Selector/Input.tsx

168
components/_util/BaseInput.tsx

@ -1,51 +1,145 @@
import { defineComponent, shallowRef, withDirectives } from 'vue';
import antInput from './antInputDirective';
import type { PropType } from 'vue';
import { defineComponent, shallowRef, ref, watch } from 'vue';
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({
compatConfig: { MODE: 3 },
inheritAttrs: false,
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'],
setup(_p, { emit }) {
emits: [
'change',
'input',
'blur',
'keydown',
'focus',
'compositionstart',
'compositionend',
'keyup',
],
setup(props, { emit, attrs, expose }) {
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 { composing } = e.target as any;
if ((e as any).isComposing || composing) {
emit('input', e);
} else {
emit('input', e);
emit('change', e);
emit('change', e);
};
const onCompositionstart = (e: CompositionEvent) => {
isComposing.value = true;
(e.target as any).composing = true;
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);
};
return {
inputRef,
focus: () => {
if (inputRef.value) {
inputRef.value.focus();
}
},
blur: () => {
if (inputRef.value) {
inputRef.value.blur();
}
},
handleChange,
const handleBlur = (e: Event) => {
emit('blur', e);
};
},
render() {
return withDirectives(
(
<input
{...this.$props}
{...this.$attrs}
onInput={this.handleChange}
onChange={this.handleChange}
ref="inputRef"
const handleFocus = (e: Event) => {
emit('focus', e);
};
const focus = () => {
if (inputRef.value) {
inputRef.value.focus();
}
};
const blur = () => {
if (inputRef.value) {
inputRef.value.blur();
}
};
const handleKeyDown = (e: KeyboardEvent) => {
emit('keydown', e);
};
const handleKeyUp = (e: KeyboardEvent) => {
emit('keyup', e);
};
const setSelectionRange = (
start: number,
end: number,
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]],
);
);
};
},
});

42
components/_util/antInputDirective.ts

@ -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;

27
components/input/ResizableTextArea.tsx

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

5
components/input/TextArea.tsx

@ -170,10 +170,8 @@ export default defineComponent({
};
const handleChange = (e: Event) => {
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 (stateValue.value === triggerValue) return;
if (hasMaxLength.value) {
// 1. maxlength 2.maxlength
@ -227,6 +225,7 @@ export default defineComponent({
id={resizeProps?.id ?? formItemContext.id.value}
ref={resizableTextArea}
maxlength={props.maxlength}
lazy={props.lazy}
/>
);
};

18
components/vc-input/Input.tsx

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

17
components/vc-mentions/src/Mentions.tsx

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

29
components/vc-pagination/Options.tsx

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

28
components/vc-pagination/Pagination.tsx

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

6
components/vc-select/Selector/Input.tsx

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

Loading…
Cancel
Save