compositionstart
2023年09月14日
一、认识
compositionstart
文本合成系统如 input method editor
(即输入法编辑器)开始新的输入合成时会触发 compositionstart
事件。例如,当用户使用拼音输入法开始输入汉字时,这个事件就会被触发。
二、语法
2.1 input
<input id="input" />
<script>
let isChinesseInput = false;
const input = document.querySelector('#input');
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
input.addEventListener('change', handleChange);
input.addEventListener('compositionend', handleCompositionend);
input.addEventListener('compositionstart', handleCompositionstart);
function handleChange(e) {
if (!isChinesseInput) {
const { value } = e.target;
console.log('value', value);
}
}
function handleCompositionend(e) {
isChinesseInput = false;
if (isChrome) {
handleChange(e);
}
}
function handleCompositionstart() {
isChinesseInput = true;
}
</script>
三、场景
3.1 input 中文输入
问题描述:
在使用 React
绑定 input
输入框的 onChange
方法时,如果使用中文输入法(或者其他输入法),会出现一个问题:还在输入拼音的时候,onChange
方法已经触发了,如下,即输入过程就已经触发了多次 onChange
方法。如果 onChange
方法有较为复杂的逻辑,就可能会带来一些用户体验或者逻辑的问题。
解决方案:
通过使用 compositionEvent
事件控制中文输入标识, 根据中文输入标识决定是否触发 onChange
。具体为: 通过监听输入法开始输入到结束的事件,即是去监听compositionstart
和compositionend
方法,通过设置一个变量,在两个方法里面设置 true/false
,来判断是否处在中文输入拼音这个过程当中,如果是,则不触发 onChange
后续事件。
兼容处理:
-
谷歌浏览器:
compositionstart -> onChange -> compositionend
,onChange
事件先于compositionend
事件触发, 因此我们需要针对谷歌浏览器的compositionend
结束后手动调用onChange
处理函数。 -
其他浏览器:
compositionstart -> compositionend -> onChange
具体实现
- App.tsx
- Input.tsx
import { useState } from 'react';
import Input, { calculateChineseLength } from './components/Input/input';
function App() {
const [value, setValue] = useState('');
const onChange = (val: string) => {
setValue(val);
console.log('中文长度为:' + calculateChineseLength(val));
};
return (
<>
<Input value={value} onChange={onChange} maxChineseLength={5} />
</>
);
}
export default App;
import { useRef } from 'react';
type InputProps = {
value: string;
maxLength?: number;
placeholder?: string;
maxChineseLength?: number;
onChange: (val: string) => void;
};
const ChineseReg = /[\u4e00-\u9fa5]/;
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
export function calculateCharLength(str: string) {
const strList = str.split('');
return strList.reduce((prev, curr) => {
if (ChineseReg.test(curr)) {
return prev + 2;
}
return prev + 1;
}, 0);
}
export function calculateChineseLength(str: string) {
const strList = str.split('');
const result = strList.reduce((prev, curr) => {
if (ChineseReg.test(curr)) {
return prev + 1;
}
return prev + 0.5;
}, 0);
return Math.floor(result);
}
export function sliceChar(str: string, maxLength: number) {
const strLen = calculateCharLength(str);
if (strLen > maxLength) {
let len = 0;
let result = '';
for (let i = 0; i < str.length; i++) {
const charLen = ChineseReg.test(str[i]) ? 2 : 1;
if (len + charLen <= maxLength) {
len += charLen;
result += str[i];
} else {
return result;
}
}
return result;
} else {
return str;
}
}
function Input(props: InputProps) {
const isInChinese = useRef(false);
const { maxLength, onChange, placeholder, maxChineseLength } = props;
const limitInputValueLength = (e: React.ChangeEvent<HTMLInputElement>) => {
let { value } = e.target;
if (maxLength && value.length > maxLength) {
value = value.slice(0, maxLength);
e.target.value = value;
} else if (
maxChineseLength &&
calculateCharLength(value) > maxChineseLength * 2
) {
value = sliceChar(value, maxChineseLength * 2);
e.target.value = value;
}
onChange(value);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isInChinese.current) {
limitInputValueLength(e);
}
};
const handleCompositionStart = () => {
isInChinese.current = true;
};
const handleCompositionEnd = (
e: React.CompositionEvent<HTMLInputElement>
) => {
isInChinese.current = false;
if (isChrome) {
handleInputChange(e as any);
}
};
return (
<input
placeholder={placeholder}
onChange={handleInputChange}
onCompositionEnd={handleCompositionEnd}
onCompositionStart={handleCompositionStart}
/>
);
}
export default Input;