Files
Dominic Gannaway 2f6274737c Move KeyHelpers to Outline package (#389)
* Move KeyHelpers to Outline package

* More tweaks

* More tweaks

* More fixes

* Fix test

* Fix test

* Fix test
2022-04-09 00:42:21 -07:00

1014 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
*/
import type {
OutlineEditor,
Selection,
OutlineNode,
ParsedNodeMap,
NodeKey,
TextNode,
View,
} from 'outline';
import {CAN_USE_BEFORE_INPUT, IS_FIREFOX, IS_SAFARI} from 'shared/environment';
import {
isDeleteBackward,
isDeleteForward,
isDeleteLineBackward,
isDeleteLineForward,
isDeleteWordBackward,
isDeleteWordForward,
isLineBreak,
isParagraph,
isBold,
isItalic,
isUnderline,
isTab,
isSelectAll,
isMoveWordBackward,
isMoveBackward,
isMoveForward,
isMoveWordForward,
} from 'outline/KeyHelpers';
import getDOMTextNodeFromElement from 'shared/getDOMTextNodeFromElement';
import isImmutableOrInertOrSegmented from 'shared/isImmutableOrInertOrSegmented';
import {
deleteBackward,
deleteForward,
deleteLineBackward,
deleteLineForward,
deleteWordBackward,
deleteWordForward,
insertParagraph,
formatText,
insertText,
removeText,
getNodesInRange,
insertNodes,
insertLineBreak,
selectAll,
moveWordBackward,
insertRichText,
moveBackward,
moveForward,
moveWordForward,
} from 'outline/SelectionHelpers';
// Safari triggers composition before keydown, meaning
// we need to account for this when handling key events.
let wasRecentlyComposing = false;
let lastKeyWasMaybeAndroidSoftKey = false;
const RESOLVE_DELAY = 20;
const BYTE_ORDER_MARK = '\uFEFF';
// TODO the Flow types here needs fixing
export type EventHandler = (
// $FlowFixMe: not sure how to handle this generic properly
event: Object,
editor: OutlineEditor,
state: EventHandlerState,
) => void;
export type EventHandlerState = {
isReadOnly: boolean,
compositionSelection: null | Selection,
isHandlingPointer: boolean,
};
function generateNodes(
nodeRange: {range: Array<NodeKey>, nodeMap: ParsedNodeMap},
view: View,
): Array<OutlineNode> {
const {range, nodeMap: parsedNodeMap} = nodeRange;
const nodes = [];
for (let i = 0; i < range.length; i++) {
const key = range[i];
const parsedNode = parsedNodeMap[key];
const node = view.createNodeFromParse(parsedNode, parsedNodeMap);
nodes.push(node);
}
return nodes;
}
function insertDataTransferForRichText(
dataTransfer: DataTransfer,
selection: Selection,
view: View,
): void {
const outlineNodesString = dataTransfer.getData(
'application/x-outline-nodes',
);
if (outlineNodesString) {
const nodeRange = JSON.parse(outlineNodesString);
const nodes = generateNodes(nodeRange, view);
insertNodes(selection, nodes);
return;
}
insertDataTransferForPlainText(dataTransfer, selection, view);
}
function insertDataTransferForPlainText(
dataTransfer: DataTransfer,
selection: Selection,
view: View,
): void {
const text = dataTransfer.getData('text/plain');
if (text != null) {
insertRichText(selection, text);
}
}
function shouldOverrideBrowserDefault(
selection: Selection,
isHoldingShift: boolean,
isBackward: boolean,
): boolean {
const anchorOffset = selection.anchorOffset;
const focusOffset = selection.focusOffset;
const anchorTextContentSize = selection.getAnchorNode().getTextContentSize();
const selectionAtBoundary = isBackward
? anchorOffset < 2 || focusOffset < 2
: anchorOffset > anchorTextContentSize - 2 ||
focusOffset > anchorTextContentSize - 2;
return selection.isCaret()
? isHoldingShift || selectionAtBoundary
: isHoldingShift && selectionAtBoundary;
}
function updateAndroidSoftKeyFlagIfAny(event: KeyboardEvent): void {
lastKeyWasMaybeAndroidSoftKey =
event.key === 'Unidentified' && event.isComposing && event.keyCode === 229;
}
function isTopLevelBlockRTL(selection: Selection) {
const anchorNode = selection.getAnchorNode();
const topLevelBlock = anchorNode.getTopParentBlockOrThrow();
const direction = topLevelBlock.getDirection();
return direction === 'rtl';
}
export function onKeyDownForPlainText(
event: KeyboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
updateAndroidSoftKeyFlagIfAny(event);
if (editor.isComposing() || wasRecentlyComposing) {
return;
}
editor.update((view) => {
const selection = view.getSelection();
if (selection === null) {
return;
}
const isHoldingShift = event.shiftKey;
const isRTL = isTopLevelBlockRTL(selection);
if (isMoveBackward(event)) {
if (shouldOverrideBrowserDefault(selection, isHoldingShift, !isRTL)) {
event.preventDefault();
moveBackward(selection, isHoldingShift, isRTL);
}
} else if (isMoveForward(event)) {
if (shouldOverrideBrowserDefault(selection, isHoldingShift, isRTL)) {
event.preventDefault();
moveForward(selection, isHoldingShift, isRTL);
}
} else if (isParagraph(event) || isLineBreak(event)) {
event.preventDefault();
insertLineBreak(selection);
} else if (isDeleteBackward(event)) {
event.preventDefault();
deleteBackward(selection);
} else if (isDeleteForward(event)) {
event.preventDefault();
deleteForward(selection);
} else if (isMoveWordBackward(event)) {
if (
IS_SAFARI ||
shouldOverrideBrowserDefault(selection, isHoldingShift, !isRTL)
) {
event.preventDefault();
moveWordBackward(selection, isHoldingShift, isRTL);
}
} else if (isMoveWordForward(event)) {
if (
IS_SAFARI ||
shouldOverrideBrowserDefault(selection, isHoldingShift, isRTL)
) {
event.preventDefault();
moveWordForward(selection, isHoldingShift, isRTL);
}
} else if (isDeleteWordBackward(event)) {
event.preventDefault();
deleteWordBackward(selection);
} else if (isDeleteWordForward(event)) {
event.preventDefault();
deleteWordForward(selection);
} else if (isDeleteLineBackward(event)) {
event.preventDefault();
deleteLineBackward(selection);
} else if (isDeleteLineForward(event)) {
event.preventDefault();
deleteLineForward(selection);
} else if (isSelectAll(event)) {
event.preventDefault();
selectAll(selection);
}
});
}
export function onKeyDownForRichText(
event: KeyboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
updateAndroidSoftKeyFlagIfAny(event);
if (editor.isComposing() || wasRecentlyComposing) {
return;
}
editor.update((view) => {
const selection = view.getSelection();
if (selection === null) {
return;
}
const isHoldingShift = event.shiftKey;
const isRTL = isTopLevelBlockRTL(selection);
if (isMoveBackward(event)) {
if (shouldOverrideBrowserDefault(selection, isHoldingShift, !isRTL)) {
event.preventDefault();
moveBackward(selection, isHoldingShift, isRTL);
}
} else if (isMoveForward(event)) {
if (shouldOverrideBrowserDefault(selection, isHoldingShift, isRTL)) {
event.preventDefault();
moveForward(selection, isHoldingShift, isRTL);
}
} else if (isLineBreak(event)) {
event.preventDefault();
insertLineBreak(selection);
} else if (isParagraph(event)) {
event.preventDefault();
insertParagraph(selection);
} else if (isDeleteBackward(event)) {
event.preventDefault();
deleteBackward(selection);
} else if (isDeleteForward(event)) {
event.preventDefault();
deleteForward(selection);
} else if (isMoveWordBackward(event)) {
if (
IS_SAFARI ||
shouldOverrideBrowserDefault(selection, isHoldingShift, !isRTL)
) {
event.preventDefault();
moveWordBackward(selection, isHoldingShift, isRTL);
}
} else if (isMoveWordForward(event)) {
if (
IS_SAFARI ||
shouldOverrideBrowserDefault(selection, isHoldingShift, isRTL)
) {
event.preventDefault();
moveWordForward(selection, isHoldingShift, isRTL);
}
} else if (isDeleteWordBackward(event)) {
event.preventDefault();
deleteWordBackward(selection);
} else if (isDeleteWordForward(event)) {
event.preventDefault();
deleteWordForward(selection);
} else if (isDeleteLineBackward(event)) {
event.preventDefault();
deleteLineBackward(selection);
} else if (isDeleteLineForward(event)) {
event.preventDefault();
deleteLineForward(selection);
} else if (isBold(event)) {
event.preventDefault();
formatText(selection, 'bold');
} else if (isUnderline(event)) {
event.preventDefault();
formatText(selection, 'underline');
} else if (isItalic(event)) {
event.preventDefault();
formatText(selection, 'italic');
} else if (isTab(event)) {
// Handle code blocks
const anchorNode = selection.getAnchorNode();
const parentBlock = anchorNode.getParentBlockOrThrow();
if (parentBlock.canInsertTab()) {
if (event.shiftKey) {
const textContent = anchorNode.getTextContent();
const character = textContent[selection.anchorOffset - 1];
if (character === '\t') {
deleteBackward(selection);
}
} else {
insertText(selection, '\t');
}
event.preventDefault();
}
} else if (isSelectAll(event)) {
event.preventDefault();
selectAll(selection);
}
});
}
export function onPasteForPlainText(
event: ClipboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
event.preventDefault();
editor.update((view) => {
const selection = view.getSelection();
const clipboardData = event.clipboardData;
if (clipboardData != null && selection !== null) {
insertDataTransferForPlainText(clipboardData, selection, view);
}
});
}
export function onPasteForRichText(
event: ClipboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
event.preventDefault();
editor.update((view) => {
const selection = view.getSelection();
const clipboardData = event.clipboardData;
if (clipboardData != null && selection !== null) {
insertDataTransferForRichText(clipboardData, selection, view);
}
});
}
export function onDropPolyfill(
event: ClipboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
// This should only occur without beforeInput. Block it as it's too much
// hassle to make work at this point.
event.preventDefault();
}
export function onDragStartPolyfill(
event: ClipboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
// Block dragging.
event.preventDefault();
}
export function onCut(
event: ClipboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
onCopy(event, editor, state);
editor.update((view) => {
const selection = view.getSelection();
if (selection !== null) {
removeText(selection);
}
});
}
export function onCopy(
event: ClipboardEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
event.preventDefault();
editor.update((view) => {
const clipboardData = event.clipboardData;
const selection = view.getSelection();
if (selection !== null) {
if (clipboardData != null) {
const domSelection = window.getSelection();
// If we haven't selected a range, then don't copy anything
if (domSelection.isCollapsed) {
return;
}
const range = domSelection.getRangeAt(0);
if (range) {
const container = document.createElement('div');
const frag = range.cloneContents();
container.appendChild(frag);
clipboardData.setData('text/html', container.innerHTML);
}
clipboardData.setData('text/plain', selection.getTextContent());
clipboardData.setData(
'application/x-outline-nodes',
JSON.stringify(getNodesInRange(selection)),
);
}
}
});
}
export function onCompositionStart(
event: CompositionEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
editor.update((view) => {
const selection = view.getSelection();
if (selection !== null) {
if (!CAN_USE_BEFORE_INPUT) {
// We only have native beforeinput composition events for
// Safari, so we have to apply the composition selection for
// other browsers.
state.compositionSelection = selection;
}
if (!selection.isCaret()) {
const focusKey = selection.focusKey;
const anchorNode = selection.getAnchorNode();
const focusNode = selection.getAnchorNode();
// If we have a range that starts on an immutable/segmented node
// then move it to the next node so that we insert text at the
// right place.
if (selection.anchorKey !== focusKey) {
if (
(anchorNode.isImmutable() || anchorNode.isSegmented()) &&
anchorNode.getNextSibling() === selection.getFocusNode()
) {
selection.setRange(focusKey, 0, focusKey, selection.focusOffset);
}
}
if (canRemoveText(anchorNode, focusNode)) {
removeText(selection);
}
}
if (IS_FIREFOX) {
// Not sure why we have to do this, but it seems to fix a bunch
// of FF related composition bugs to do with selection.
selection.isDirty = true;
}
editor.setCompositionKey(selection.anchorKey);
}
});
}
export function onCompositionEnd(
event: CompositionEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
editor.setCompositionKey(null);
wasRecentlyComposing = true;
setTimeout(() => {
wasRecentlyComposing = false;
}, RESOLVE_DELAY);
}
export function onSelectionChange(
event: Event,
editor: OutlineEditor,
state: EventHandlerState,
): void {
const domSelection = window.getSelection();
const editorElement = editor.getEditorElement();
// This is a hot-path, so let's avoid doing an update when
// the anchorNode is not actually inside the editor.
if (editorElement && !editorElement.contains(domSelection.anchorNode)) {
return;
}
editor.update((view) => {
// This update also functions as a way of reconciling a bad selection
// to a good selection.
});
}
export function onPointerDown(
event: PointerEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
state.isHandlingPointer = true;
// Throttle setting of the flag for 50ms, as we don't want this to trigger
// for simple clicks.
setTimeout(() => {
if (state.isHandlingPointer) {
editor.setPointerDown(true);
}
}, 50);
}
export function onPointerUp(
event: PointerEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
state.isHandlingPointer = false;
editor.setPointerDown(false);
}
export function checkForBadInsertion(
anchorElement: HTMLElement,
anchorNode: TextNode,
editor: OutlineEditor,
): boolean {
const nextSibling = anchorNode.getNextSibling();
return (
anchorElement.parentNode === null ||
(nextSibling !== null &&
editor.getElementByKey(nextSibling.getKey()) !==
anchorElement.nextSibling)
);
}
export function handleBlockTextInputOnNode(
anchorNode: TextNode,
view: View,
editor: OutlineEditor,
): boolean {
// If we are mutating an immutable or segmented node, then reset
// the content back to what it was before, as this is not allowed.
if (isImmutableOrInertOrSegmented(anchorNode)) {
// If this node has a decorator, then we'll make it as needing an
// update by React.
anchorNode.markDirtyDecorator();
view.markNodeAsDirty(anchorNode);
editor._compositionKey = null;
return true;
}
return false;
}
export function onNativeInput(
event: InputEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
const inputType = event.inputType;
const isInsertText = inputType === 'insertText';
const isInsertCompositionText = inputType === 'insertCompositionText';
const isDeleteCompositionText = inputType === 'deleteCompositionText';
if (!isInsertText && !isInsertCompositionText && !isDeleteCompositionText) {
return;
}
editor.update((view) => {
const selection = view.getSelection();
if (selection === null) {
return;
}
const anchorKey = selection.anchorKey;
// To ensure we handle Android software keyboard
// text entry properly (which is usually composed text),
// we need to support a few extra heuristics. Notably,
// we need to disable composition for "insertText" or
// "insertCompositionText" when the last key press was
// likely to be an android soft key press. Android will
// then enable composition again automatically.
if (
anchorKey === editor._compositionKey &&
(isInsertText ||
(isInsertCompositionText && lastKeyWasMaybeAndroidSoftKey))
) {
editor._compositionKey = null;
}
const data = event.data;
if (data != null) {
const focusKey = selection.focusKey;
const anchorElement = editor.getElementByKey(anchorKey);
const anchorNode = selection.getAnchorNode();
const textNode = getDOMTextNodeFromElement(anchorElement);
// Let's try and detect a bad update here. This usually comes from text transformation
// tools that attempt to insertText across a range of nodes which obviously we can't
// detect unless we rely on the DOM being the source of truth. We can try and recover
// by dispatching an Undo event, and then capturing the previous selection and trying to
// apply the text on that.
if (
anchorElement !== null &&
checkForBadInsertion(anchorElement, anchorNode, editor)
) {
window.requestAnimationFrame(() => {
document.execCommand('Undo', false, null);
editor.update((undoneView) => {
const undoneSelection = undoneView.getSelection();
if (undoneSelection !== null) {
insertText(undoneSelection, data);
}
});
});
return;
}
if (handleBlockTextInputOnNode(anchorNode, view, editor)) {
return;
}
// If we are inserting text into the same anchor as is our focus
// node, then we can apply a faster optimization that also handles
// text replacement tools that use execCommand (which doesn't trigger
// beforeinput in some browsers).
if (
anchorElement !== null &&
textNode !== null &&
isInsertText &&
anchorKey === focusKey
) {
// Let's read what is in the DOM already, and use that as the value
// for our anchor node. We get the text content from the anchor element's
// text node.
const rawTextContent = textNode.nodeValue;
const textContent = rawTextContent.replace(BYTE_ORDER_MARK, '');
let anchorOffset = window.getSelection().anchorOffset;
// If the first character is a BOM, then we need to offset this because
// this character isn't really apart of our offset.
if (rawTextContent[0] === BYTE_ORDER_MARK) {
anchorOffset--;
}
// We set the range before content, as hashtags might skew the offset
selection.setRange(anchorKey, anchorOffset, anchorKey, anchorOffset);
anchorNode.setTextContent(textContent);
} else {
insertText(selection, data);
}
}
});
}
function applyTargetRange(selection: Selection, event: InputEvent): void {
if (event.getTargetRanges) {
const targetRange = event.getTargetRanges()[0];
if (targetRange) {
selection.applyDOMRange(targetRange);
}
}
}
function canRemoveText(anchorNode: TextNode, focusNode: TextNode): boolean {
return (
anchorNode !== focusNode ||
!isImmutableOrInertOrSegmented(anchorNode) ||
!isImmutableOrInertOrSegmented(focusNode)
);
}
// Block double space auto-period insertion if we're
// at the start of a an empty text node. This happens
// because the BOM gets treated like "text".
function isBadDoubleSpacePeriodReplacment(
event: InputEvent,
selection: Selection,
): boolean {
const inputType = event.inputType;
if (
(inputType === 'insertText' || inputType === 'insertReplacementText') &&
selection.anchorOffset === 0 &&
selection.focusOffset === 1 &&
selection.anchorKey === selection.focusKey
) {
const dataTransfer = event.dataTransfer;
const data =
dataTransfer != null ? dataTransfer.getData('text/plain') : event.data;
return data === '. ';
}
return false;
}
export function onNativeBeforeInputForPlainText(
event: InputEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
const inputType = event.inputType;
editor.update((view) => {
const selection = view.getSelection();
if (selection === null) {
return;
}
if (inputType === 'deleteContentBackward') {
// Used for Android
editor.setCompositionKey(null);
event.preventDefault();
deleteBackward(selection);
return;
}
const data = event.data;
const inputText = inputType === 'insertText';
if (selection.isCaret()) {
applyTargetRange(selection, event);
}
if (isBadDoubleSpacePeriodReplacment(event, selection)) {
event.preventDefault();
insertText(selection, ' ');
return;
}
const anchorNode = selection.getAnchorNode();
const focusNode = selection.getAnchorNode();
if (
inputText ||
inputType === 'insertCompositionText' ||
inputType === 'deleteCompositionText'
) {
if (selection.isCaret() && isImmutableOrInertOrSegmented(anchorNode)) {
event.preventDefault();
return;
}
// Gets around a Safari text replacement bug.
if (IS_SAFARI && data === null && event.dataTransfer) {
const text = event.dataTransfer.getData('text/plain');
event.preventDefault();
insertRichText(selection, text);
return;
}
if (data === '\n') {
event.preventDefault();
insertLineBreak(selection);
} else if (data === '\n\n') {
event.preventDefault();
insertLineBreak(selection);
insertLineBreak(selection);
} else if (!selection.isCaret()) {
const anchorKey = selection.anchorKey;
const focusKey = selection.focusKey;
if (canRemoveText(anchorNode, focusNode)) {
removeText(selection);
}
if (inputText && anchorKey !== focusKey && data) {
event.preventDefault();
editor.setCompositionKey(null);
insertText(selection, data);
}
}
return;
}
// Prevent the browser from carrying out
// the input event, so we can control the
// output.
event.preventDefault();
switch (inputType) {
case 'insertFromComposition': {
if (data) {
insertText(selection, data);
}
break;
}
case 'insertParagraph': {
// Used for Android
editor.setCompositionKey(null);
insertLineBreak(selection);
break;
}
case 'insertFromYank':
case 'insertFromDrop':
case 'insertReplacementText':
case 'insertFromPaste': {
const dataTransfer = event.dataTransfer;
if (dataTransfer != null) {
insertDataTransferForPlainText(dataTransfer, selection, view);
} else {
if (data) {
insertText(selection, data);
}
}
break;
}
case 'deleteByComposition': {
if (canRemoveText(anchorNode, focusNode)) {
removeText(selection);
}
break;
}
case 'deleteByDrag':
case 'deleteByCut': {
removeText(selection);
break;
}
case 'deleteContent': {
deleteForward(selection);
break;
}
case 'deleteWordBackward': {
deleteWordBackward(selection);
break;
}
case 'deleteWordForward': {
deleteWordForward(selection);
break;
}
case 'deleteHardLineBackward':
case 'deleteSoftLineBackward': {
deleteLineBackward(selection);
break;
}
case 'deleteHardLineForward':
case 'deleteSoftLineForward': {
deleteLineForward(selection);
break;
}
default:
// NO-OP
}
});
}
export function onNativeBeforeInputForRichText(
event: InputEvent,
editor: OutlineEditor,
state: EventHandlerState,
): void {
const inputType = event.inputType;
editor.update((view) => {
const selection = view.getSelection();
if (selection === null) {
return;
}
if (inputType === 'deleteContentBackward') {
// Used for Android
editor.setCompositionKey(null);
event.preventDefault();
deleteBackward(selection);
return;
}
const data = event.data;
const inputText = inputType === 'insertText';
if (selection.isCaret()) {
applyTargetRange(selection, event);
}
if (isBadDoubleSpacePeriodReplacment(event, selection)) {
event.preventDefault();
insertText(selection, ' ');
return;
}
const anchorNode = selection.getAnchorNode();
const focusNode = selection.getAnchorNode();
if (
inputText ||
inputType === 'insertCompositionText' ||
inputType === 'deleteCompositionText'
) {
if (selection.isCaret() && isImmutableOrInertOrSegmented(anchorNode)) {
event.preventDefault();
return;
}
// Gets around a Safari text replacement bug.
if (IS_SAFARI && data === null && event.dataTransfer) {
const text = event.dataTransfer.getData('text/plain');
event.preventDefault();
insertRichText(selection, text);
return;
}
if (data === '\n') {
event.preventDefault();
insertLineBreak(selection);
} else if (data === '\n\n') {
event.preventDefault();
insertParagraph(selection);
} else if (!selection.isCaret()) {
const anchorKey = selection.anchorKey;
const focusKey = selection.focusKey;
if (canRemoveText(anchorNode, focusNode)) {
removeText(selection);
}
if (inputText && anchorKey !== focusKey && data) {
event.preventDefault();
editor.setCompositionKey(null);
insertText(selection, data);
}
}
return;
}
// Prevent the browser from carrying out
// the input event, so we can control the
// output.
event.preventDefault();
switch (inputType) {
case 'insertFromComposition': {
if (data) {
insertText(selection, data);
}
break;
}
case 'insertParagraph': {
// Used for Android
editor.setCompositionKey(null);
insertParagraph(selection);
break;
}
case 'formatStrikeThrough': {
formatText(selection, 'strikethrough');
break;
}
case 'insertFromYank':
case 'insertFromDrop':
case 'insertReplacementText':
case 'insertFromPaste': {
const dataTransfer = event.dataTransfer;
if (dataTransfer != null) {
insertDataTransferForRichText(dataTransfer, selection, view);
} else {
if (data) {
insertText(selection, data);
}
}
break;
}
case 'deleteByComposition': {
if (canRemoveText(anchorNode, focusNode)) {
removeText(selection);
}
break;
}
case 'deleteByDrag':
case 'deleteByCut': {
removeText(selection);
break;
}
case 'deleteContent': {
deleteForward(selection);
break;
}
case 'deleteWordBackward': {
deleteWordBackward(selection);
break;
}
case 'deleteWordForward': {
deleteWordForward(selection);
break;
}
case 'deleteHardLineBackward':
case 'deleteSoftLineBackward': {
deleteLineBackward(selection);
break;
}
case 'deleteHardLineForward':
case 'deleteSoftLineForward': {
deleteLineForward(selection);
break;
}
default:
// NO-OP
}
});
}
export function onPolyfilledBeforeInput(
event: SyntheticInputEvent<EventTarget>,
editor: OutlineEditor,
state: EventHandlerState,
): void {
event.preventDefault();
editor.update((view) => {
const selection = view.getSelection();
const data = event.data;
if (data != null && selection !== null) {
const compositionSelection = state.compositionSelection;
state.compositionSelection = null;
if (compositionSelection !== null) {
selection.setRange(
compositionSelection.anchorKey,
compositionSelection.anchorOffset,
compositionSelection.focusKey,
compositionSelection.focusOffset,
);
}
if (handleBlockTextInputOnNode(selection.getAnchorNode(), view, editor)) {
return;
}
insertText(selection, data);
}
});
}