before input event plugin
一、作用
二、 注册事件
备注
registerTwoPhaseEvent()由 EventRegistry#registerTwoPhaseEvent 提供
function registerEvents() {
registerTwoPhaseEvent('onBeforeInput', [
'compositionend',
'keypress',
'textInput',
'paste',
]);
registerTwoPhaseEvent('onCompositionEnd', [
'compositionend',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionStart', [
'compositionstart',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionUpdate', [
'compositionupdate',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
}
三、提取事件
/**
* Create an `onBeforeInput` event to match
* http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
*
* This event plugin is based on the native `textInput` event
* available in Chrome, Safari, Opera, and IE. This event fires after
* `onKeyPress` and `onCompositionEnd`, but before `onInput`.
*
* 该事件插件基于原生的 `textInput` 事件可在 Chrome、Safari、Opera 和 IE 中使用。该
* 事件在 `onKeyPress` 和 `onCompositionEnd` 之后触发,但在 `onInput` 之前触发。
*
* `beforeInput` is spec'd but not implemented in any browsers, and
* the `input` event does not provide any useful information about what has
* actually been added, contrary to the spec. Thus, `textInput` is the best
* available event to identify the characters that have actually been inserted
* into the target node.
*
* `beforeInput` 已有规范但在任何浏览器中都未实现,且 `input` 事件无法提供有关实际已添
* 加内容的有用信息,这与规范相悖。因此,`textInput` 是识别实际插入到目标节点中字符的最佳
* 可用事件。
*
* This plugin is also responsible for emitting `composition` events, thus
* allowing us to share composition fallback code for both `beforeInput` and
* `composition` event types.
*
* 这个插件也负责触发 `composition` 事件,从而允许我们为 `beforeInput`
* `composition` 事件类型共享组合回退代码。
*/
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
extractCompositionEvent(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
);
extractBeforeInputEvent(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
);
}
四、常量
1. 结束按键码
备注
源码 31 - 34 行
canUseDOM()由 ExecutionEnvironment#canUseDOM 实现
// 制表键, 回车键, 退出键, 空格键
// 结束按键码
const END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
// 开始按键码
const START_KEYCODE = 229;
// 可以使用组合事件
const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window;
2. 使用回退组合数据
备注
源码中 41 - 56 行
canUseDOM()由 ExecutionEnvironment#canUseDOM 实现
// Webkit offers a very useful `textInput` event that can be used to
// directly represent `beforeInput`. The IE `textinput` event is not as
// useful, so we don't use it.
// Webkit 提供了一个非常有用的 `textInput` 事件,可用于直接表示 `beforeInput`。
// IE 的 `textinput` 事件不那么有用,所以我们没有使用它。
const canUseTextInputEvent =
canUseDOM && 'TextEvent' in window && !documentMode;
// In IE9+, we have access to composition events, but the data supplied
// by the native compositionend event may be incorrect. Japanese ideographic
// spaces, for instance (\u3000) are not recorded correctly.
// 在 IE9 中,我们可以访问组合事件,但本地 compositionend 事件提供的数据可能不正
// 确。例如,日文全角空格(\u3000)不能被正确记录。
// 使用回退组合数据
const useFallbackCompositionData =
canUseDOM &&
(!canUseCompositionEvent ||
(documentMode && documentMode > 8 && documentMode <= 11));
// 空格键代码
const SPACEBAR_CODE = 32;
// 空格键
const SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
五、变量
1. 文档模式
备注
源码中 36 - 39 行
canUseDOM()由 ExecutionEnvironment#canUseDOM 实现
// 文档模式
let documentMode = null;
// 并未在标准找到 `document.documentMode` 属性,倒是在 README.md 文档中找到了 15.6.2 版本日志的说明:
// Fix a bug where modifying `document.documentMode` would trigger IE detection in other...
// 这就意味着 `document.documentMode` 大概是 IE 浏览器独有的属性。
if (canUseDOM && 'documentMode' in document) {
documentMode = document.documentMode;
}
2. 按下空格键
备注
源码中 91 - 92 行
// Track whether we've ever handled a keypress on the space key.
// 跟踪我们是否处理过空格键的按键事件。
// 按下空格键
let hasSpaceKeypress = false;
3. 正在撰写
备注
源码中 188 - 189 行
// Track the current IME composition status, if any.
// 跟踪当前的输入法编辑器 (IME) 组合状态(如果有的话)。
let isComposing = false;
六、工具
1. 是否按键命令
/**
* Return whether a native keypress event is assumed to be a command.
* This is required because Firefox fires `keypress` events for key commands
* (cut, copy, select-all, etc.) even though no character is inserted.
*
* 返回是否将本地按键事件视为命令。这是必需的,因为 Firefox 会为键盘命令触发
* `keypress` 事件(剪切、复制、全选等),即使没有插入任何字符。
*/
function isKeypressCommand(nativeEvent: any) {
return (
(nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
// ctrlKey && altKey is equivalent to AltGr, and is not a command.
// ctrlKey 和 altKey 相当于 AltGr,但不是一个命令。
!(nativeEvent.ctrlKey && nativeEvent.altKey)
);
}
2. 获取组合事件类型
/**
* Translate native top level events into event types.
* 将本地顶级事件转换为事件类型
*/
function getCompositionEventType(domEventName: DOMEventName) {
switch (domEventName) {
case 'compositionstart':
return 'onCompositionStart';
case 'compositionend':
return 'onCompositionEnd';
case 'compositionupdate':
return 'onCompositionUpdate';
}
}
3. 是否回退组合开始
/**
* Does our fallback best-guess model think this event signifies that
* composition has begun?
*
* 我们的回退最佳猜测模型认为这一事件是否意味着创作已经开始?
*/
function isFallbackCompositionStart(
domEventName: DOMEventName,
nativeEvent: any,
): boolean {
return domEventName === 'keydown' && nativeEvent.keyCode === START_KEYCODE;
}
4. 是否回退组合结束
/**
* Does our fallback mode think that this event is the end of composition?
* 我们的回退模式认为这个事件是组合的结束吗?
*/
function isFallbackCompositionEnd(
domEventName: DOMEventName,
nativeEvent: any,
): boolean {
switch (domEventName) {
case 'keyup':
// Command keys insert or clear IME input.
// 按键用于插入或清除输入法输入。
return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
case 'keydown':
// Expect IME keyCode on each keydown. If we get any other
// code we must have exited earlier.
// 每次按下键时都预期是 IME keyCode。如果我们得到其他代码,说明我们之前已经退出了。
return nativeEvent.keyCode !== START_KEYCODE;
case 'keypress':
case 'mousedown':
case 'focusout':
// Events are not possible without cancelling IME.
// 没有取消输入法编辑器(IME)事件是不可能的。
return true;
default:
return false;
}
}
5. 从自定义事件获取数据
/**
* Google Input Tools provides composition data via a CustomEvent,
* with the `data` property populated in the `detail` object. If this
* is available on the event object, use it. If not, this is a plain
* composition event and we have nothing special to extract.
*
* Google 输入工具通过 CustomEvent 提供组合数据,其 `data` 属性在 `detail` 对象
* 中填充。如果事件对象中有此属性,则使用它。如果没有,这就是一个普通的组合事件,我们没
* 有特别的内容需要提取。
*
* @param {object} nativeEvent
* @return {?string}
*/
function getDataFromCustomEvent(nativeEvent: any) {
const detail = nativeEvent.detail;
if (typeof detail === 'object' && 'data' in detail) {
return detail.data;
}
return null;
}
6. 正在使用韩语输入法
/**
* Check if a composition event was triggered by Korean IME.
* Our fallback mode does not work well with IE's Korean IME,
* so just use native composition events when Korean IME is used.
* Although CompositionEvent.locale property is deprecated,
* it is available in IE, where our fallback mode is enabled.
*
* 检查是否由韩文输入法触发了组合输入事件。我们的回退模式在 IE 的韩文输入法下表现不佳,
* 因此在使用韩文输入法时,只使用原生组合输入事件。虽然 CompositionEvent.locale 属性已
* 被弃用,但在启用回退模式的 IE 中仍然可用。
*
* @param {object} nativeEvent
* @return {boolean}
*/
function isUsingKoreanIME(nativeEvent: any) {
return nativeEvent.locale === 'ko';
}
7. 提取组合事件
备注
FallbackCompositionStateInitialize()由 FallbackCompositionState#FallbackCompositionStateInitialize 实现FallbackCompositionStateGetData()由 FallbackCompositionState#FallbackCompositionStateGetData 实现accumulateTwoPhaseListeners()由 DOMPluginEventSystem#accumulateTwoPhaseListeners 实现SyntheticCompositionEvent()由 SyntheticEvent#SyntheticCompositionEvent 实现
/**
* @return {?object} A SyntheticCompositionEvent.
* 一个合成组成事件
*/
function extractCompositionEvent(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
) {
let eventType;
let fallbackData;
if (canUseCompositionEvent) {
eventType = getCompositionEventType(domEventName);
} else if (!isComposing) {
if (isFallbackCompositionStart(domEventName, nativeEvent)) {
eventType = 'onCompositionStart';
}
} else if (isFallbackCompositionEnd(domEventName, nativeEvent)) {
eventType = 'onCompositionEnd';
}
if (!eventType) {
return null;
}
if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {
// The current composition is stored statically and must not be
// overwritten while composition continues.
// 当前的组合是静态存储的,在组合进行时不能被覆盖。
if (!isComposing && eventType === 'onCompositionStart') {
isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
} else if (eventType === 'onCompositionEnd') {
if (isComposing) {
fallbackData = FallbackCompositionStateGetData();
}
}
}
const listeners = accumulateTwoPhaseListeners(targetInst, eventType);
if (listeners.length > 0) {
const event: ReactSyntheticEvent = new SyntheticCompositionEvent(
eventType,
domEventName,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({ event, listeners });
if (fallbackData) {
// Inject data generated from fallback path into the synthetic event.
// This matches the property of native CompositionEventInterface.
// 将从回退路径生成的数据注入到合成事件中。
// 这与原生 CompositionEventInterface 的属性相匹配。
event.data = fallbackData;
} else {
const customData = getDataFromCustomEvent(nativeEvent);
if (customData !== null) {
event.data = customData;
}
}
}
}
8. 获取本地输入前字符
function getNativeBeforeInputChars(
domEventName: DOMEventName,
nativeEvent: any,
): ?string {
switch (domEventName) {
case 'compositionend':
return getDataFromCustomEvent(nativeEvent);
case 'keypress':
/**
* If native `textInput` events are available, our goal is to make
* use of them. However, there is a special case: the spacebar key.
* In Webkit, preventing default on a spacebar `textInput` event
* cancels character insertion, but it *also* causes the browser
* to fall back to its default spacebar behavior of scrolling the
* page.
*
* 如果本地 `textInput` 事件可用,我们的目标是利用它们。然而,有一个特殊情
* 况:空格键。在 Webkit 中,在空格键 `textInput` 事件上阻止默认操作会取消
* 字符插入,但它 *也* 会导致浏览器回退到其默认的空格键行为,即滚动页面。
*
* Tracking at:
* https://code.google.com/p/chromium/issues/detail?id=355103
*
* To avoid this issue, use the keypress event as if no `textInput`
* event is available.
* 为避免此问题,请使用 keypress 事件,就好像没有 `textInput` 事件可用一样。
*/
const which = nativeEvent.which;
if (which !== SPACEBAR_CODE) {
return null;
}
hasSpaceKeypress = true;
return SPACEBAR_CHAR;
case 'textInput':
// Record the characters to be added to the DOM.
// 记录要添加到 DOM 的字符。
const chars = nativeEvent.data;
// If it's a spacebar character, assume that we have already handled
// it at the keypress level and bail immediately. Android Chrome
// doesn't give us keycodes, so we need to ignore it.
// 如果是空格键字符,假设我们已经在按键事件层面处理过了,立即返回。
// Android Chrome 不提供 keycode,所以我们需要忽略它。
if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
return null;
}
return chars;
default:
// For other native event types, do nothing.
// 对于其他原生事件类型,不执行任何操作。
return null;
}
}
9. 在输入字符前获取回退
备注
FallbackCompositionStateGetData()由 FallbackCompositionState#FallbackCompositionStateGetData 实现FallbackCompositionStateReset()由 FallbackCompositionState#FallbackCompositionStateReset 实现
/**
* For browsers that do not provide the `textInput` event, extract the
* appropriate string to use for SyntheticInputEvent.
* 对于不提供 `textInput` 事件的浏览器,提取用于 SyntheticInputEvent 的适当字符串
*/
function getFallbackBeforeInputChars(
domEventName: DOMEventName,
nativeEvent: any,
): ?string {
// If we are currently composing (IME) and using a fallback to do so,
// try to extract the composed characters from the fallback object.
// If composition event is available, we extract a string only at
// compositionevent, otherwise extract it at fallback events.
// 如果我们当前正在输入法(IME)中进行文字组合,并使用了备用方案来实现,尝试从备用对象中
// 提取已组合的字符。如果组合事件可用,我们仅在组合事件中提取字符串,否则在备用事件中提取。
if (isComposing) {
if (
domEventName === 'compositionend' ||
(!canUseCompositionEvent &&
isFallbackCompositionEnd(domEventName, nativeEvent))
) {
const chars = FallbackCompositionStateGetData();
FallbackCompositionStateReset();
isComposing = false;
return chars;
}
return null;
}
switch (domEventName) {
case 'paste':
// If a paste event occurs after a keypress, throw out the input
// chars. Paste events should not lead to BeforeInput events.
// 如果在按键后发生粘贴事件,则丢弃输入的字符粘贴事件不应触发 BeforeInput 事件。
return null;
case 'keypress':
/**
* As of v27, Firefox may fire keypress events even when no character
* will be inserted. A few possibilities:
*
* 从 v27 版本开始,即使没有字符被插入,Firefox 也可能触发 keypress 事件。
* 有几种可能的情况:
*
* - `which` is `0`. Arrow keys, Esc key, etc.
*
* - `which` 是 `0`。箭头键、Esc 键等。
*
* - `which` is the pressed key code, but no char is available.
* Ex: 'AltGr + d` in Polish. There is no modified character for
* this key combination and no character is inserted into the
* document, but FF fires the keypress for char code `100` anyway.
* No `input` event will occur.
*
* - `which` 是按下的键码,但没有可用的字符。
* 例:在波兰语中按 'AltGr + d'。这个键组合没有对应的修改字符,也不会向文档插
* 入字符,但是 Firefox 仍会触发字符码为 `100` 的 keypress 事件。不会发生
* `input` 事件。
*
* - `which` is the pressed key code, but a command combination is
* being used. Ex: `Cmd+C`. No character is inserted, and no
* `input` event will occur.
*
* - `which` 是按下的键码,但正在使用组合命令。例如:`Cmd C`。不会插入字符,也不会触发 `input` 事件。
*/
if (!isKeypressCommand(nativeEvent)) {
// IE fires the `keypress` event when a user types an emoji via
// Touch keyboard of Windows. In such a case, the `char` property
// holds an emoji character like `\uD83D\uDE0A`. Because its length
// is 2, the property `which` does not represent an emoji correctly.
// In such a case, we directly return the `char` property instead of
// using `which`.
// 当用户通过 Windows 触摸键盘输入表情符号时,IE 会触发 `keypress` 事件。在这
// 种情况下,`char` 属性会包含像 `😊` 这样的表情符号。由于它的长度为 2,属性
// `which` 无法正确表示表情符号。在这种情况下,我们直接返回 `char` 属性,而不是
// 使用 `which`。
if (nativeEvent.char && nativeEvent.char.length > 1) {
return nativeEvent.char;
} else if (nativeEvent.which) {
return String.fromCharCode(nativeEvent.which);
}
}
return null;
case 'compositionend':
return useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)
? null
: nativeEvent.data;
default:
return null;
}
}
10. 在输入事件之前提取
备注
accumulateTwoPhaseListeners()由 DOMPluginEventSystem#accumulateTwoPhaseListeners 实现SyntheticInputEvent()由 SyntheticEvent#SyntheticCompositionEvent 实现
/**
* Extract a SyntheticInputEvent for `beforeInput`, based on either native
* `textInput` or fallback behavior.
* * 根据本地 `textInput` 或备用行为,为 `beforeInput` 提取合成输入事件
*
* @return {?object} A SyntheticInputEvent.
* 一个合成输入事件
*/
function extractBeforeInputEvent(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
) {
let chars;
if (canUseTextInputEvent) {
chars = getNativeBeforeInputChars(domEventName, nativeEvent);
} else {
chars = getFallbackBeforeInputChars(domEventName, nativeEvent);
}
// If no characters are being inserted, no BeforeInput event should
// be fired.
// 如果没有字符被插入,则不应触发 BeforeInput 事件。
if (!chars) {
return null;
}
const listeners = accumulateTwoPhaseListeners(targetInst, 'onBeforeInput');
if (listeners.length > 0) {
const event: ReactSyntheticEvent = new SyntheticInputEvent(
'onBeforeInput',
'beforeinput',
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({ event, listeners });
event.data = chars;
}
}