Path: blob/master/src/packages/frontend/editors/slate/slate-react/components/editable.tsx
5895 views
import React, {1useCallback,2useEffect,3useMemo,4useRef,5useState,6} from "react";7import {8Editor,9Element,10Node,11NodeEntry,12Path,13Range,14Text,15Transforms,16} from "slate";1718import Children from "./children";19import { WindowingParams } from "./children";20import Hotkeys from "../utils/hotkeys";21import {22IS_FIREFOX,23IS_SAFARI,24HAS_BEFORE_INPUT_SUPPORT,25} from "../utils/environment";26import { ReactEditor } from "..";27import { ReadOnlyContext } from "../hooks/use-read-only";28import { useSlate } from "../hooks/use-slate";29import { useIsomorphicLayoutEffect } from "../hooks/use-isomorphic-layout-effect";30import { DecorateContext } from "../hooks/use-decorate";31import {32DOMElement,33isDOMElement,34isDOMNode,35DOMStaticRange,36isPlainTextOnlyPaste,37} from "../utils/dom";38import {39EDITOR_TO_ELEMENT,40ELEMENT_TO_NODE,41IS_READ_ONLY,42NODE_TO_ELEMENT,43IS_FOCUSED,44PLACEHOLDER_SYMBOL,45} from "../utils/weak-maps";46import { debounce } from "lodash";47import getDirection from "direction";48import { useDOMSelectionChange, useUpdateDOMSelection } from "./selection-sync";49import { hasEditableTarget, hasTarget } from "./dom-utils";5051/**52* `RenderElementProps` are passed to the `renderElement` handler.53*/5455export interface RenderElementProps {56children: any;57element: Element;58attributes: {59"data-slate-node": "element";60"data-slate-inline"?: true;61"data-slate-void"?: true;62dir?: "rtl";63ref: any;64};65}66export const RenderElementProps = null; // webpack + TS es2020 modules need this6768/**69* `RenderLeafProps` are passed to the `renderLeaf` handler.70*/7172export interface RenderLeafProps {73children: any;74leaf: Text;75text: Text;76attributes: {77"data-slate-leaf": true;78};79}80export const RenderLeafProps = null; // webpack + TS es2020 modules need this8182/**83* `EditableProps` are passed to the `<Editable>` component.84*/8586export type EditableProps = {87decorate?: (entry: NodeEntry) => Range[];88onDOMBeforeInput?: (event: Event) => void;89placeholder?: string;90readOnly?: boolean;91role?: string;92style?: React.CSSProperties;93renderElement?: React.FC<RenderElementProps>;94renderLeaf?: React.FC<RenderLeafProps>;95as?: React.ElementType;96windowing?: WindowingParams;97divref?;98} & React.TextareaHTMLAttributes<HTMLDivElement>;99100/**101* Editable.102*/103104export const Editable: React.FC<EditableProps> = (props: EditableProps) => {105const {106windowing,107autoFocus,108decorate = defaultDecorate,109onDOMBeforeInput: propsOnDOMBeforeInput,110placeholder,111readOnly = false,112renderElement,113renderLeaf,114style = {},115as: Component = "div",116...attributes117} = props;118const editor = useSlate();119const fallbackRef = useRef<HTMLDivElement>(null);120const ref = props.divref ?? fallbackRef;121122// Return true if the given event should be handled123// by the event handler code defined below.124// Regarding slateIgnore below, we use this with codemirror125// editor for fenced code blocks and math.126// I couldn't find any way to get codemirror to allow the copy to happen,127// but at the same time to not let the event propogate.128const shouldHandle = useCallback(129(130{131event, // the event itself132name, // name of the event, e.g., "onClick"133notReadOnly, // require doc to not be readOnly (ignored if not specified)134editableTarget, // require event target to be editable (defaults to true if not specified!)135}: {136event;137name: string;138notReadOnly?: boolean;139editableTarget?: boolean;140}, // @ts-ignore141) =>142!event.nativeEvent?.slateIgnore &&143(notReadOnly == null || notReadOnly == !readOnly) &&144((editableTarget ?? true) == true145? hasEditableTarget(editor, event.target)146: hasTarget(editor, event.target)) &&147!isEventHandled(event, attributes[name]),148[editor, attributes, readOnly],149);150151// Update internal state on each render.152IS_READ_ONLY.set(editor, readOnly);153154// Keep track of some state for the event handler logic.155const state: {156isComposing: boolean;157latestElement: DOMElement | null;158shiftKey: boolean;159ignoreSelection: boolean;160} = useMemo(161() => ({162isComposing: false,163latestElement: null as DOMElement | null,164shiftKey: false,165ignoreSelection: false,166}),167[],168);169170// start ignoring the selection sync171editor.setIgnoreSelection = (value: boolean) => {172state.ignoreSelection = value;173};174// stop ignoring selection sync175editor.getIgnoreSelection = () => {176return state.ignoreSelection;177};178179// state whose change causes an update180const [hiddenChildren, setHiddenChildren] = useState<Set<number>>(181new Set([]),182);183184editor.updateHiddenChildren = useCallback(() => {185if (!ReactEditor.isUsingWindowing(editor)) return;186const hiddenChildren0: number[] = [];187let isCollapsed: boolean = false;188let level: number = 0;189let index: number = 0;190let hasAll: boolean = true;191for (const child of editor.children) {192if (!Element.isElement(child)) {193throw Error("bug");194}195if (child.type != "heading" || (isCollapsed && child.level > level)) {196if (isCollapsed) {197hiddenChildren0.push(index);198if (hasAll && !hiddenChildren.has(index)) {199hasAll = false;200}201}202} else {203// it's a heading of a high enough level, and it sets the new state.204// It is always visible.205isCollapsed = !!editor.collapsedSections.get(child);206level = child.level;207}208index += 1;209}210if (hasAll && hiddenChildren0.length == hiddenChildren.size) {211// no actual change (since subset and same cardinality), so don't212// cause re-render.213return;214}215setHiddenChildren(new Set(hiddenChildren0));216}, [editor.children, hiddenChildren]);217218const updateHiddenChildrenDebounce = useMemo(() => {219return debounce(() => editor.updateHiddenChildren(), 1000);220}, []);221222// When the actual document changes we soon update the223// hidden children set, since it is a list of indexes224// into editor.children, so may change. That said, we225// don't want this to impact performance when typing, so226// we debounce it, and it is unlikely that things change227// when the content (but not number) of children changes.228useEffect(updateHiddenChildrenDebounce, [editor.children]);229// We *always* immediately update when the number of children changes, since230// that is highly likely to make the hiddenChildren data structure wrong.231useEffect(() => editor.updateHiddenChildren(), [editor.children.length]);232233// Update element-related weak maps with the DOM element ref.234useIsomorphicLayoutEffect(() => {235if (ref.current) {236EDITOR_TO_ELEMENT.set(editor, ref.current);237NODE_TO_ELEMENT.set(editor, ref.current);238ELEMENT_TO_NODE.set(ref.current, editor);239} else {240NODE_TO_ELEMENT.delete(editor);241}242});243244// The autoFocus TextareaHTMLAttribute doesn't do anything on a div, so it245// needs to be manually focused.246useEffect(() => {247if (ref.current && autoFocus) {248ref.current.focus();249}250}, [autoFocus]);251252useIsomorphicLayoutEffect(() => {253// Whenever the selection changes and is collapsed, make254// sure the cursor is visible. Also, have a facility to255// ignore a single iteration of this, which we use when256// the selection change is being caused by realtime257// collaboration.258259// @ts-ignore260const skip = editor.syncCausedUpdate;261if (262editor.selection != null &&263Range.isCollapsed(editor.selection) &&264!skip265) {266editor.scrollCaretIntoView();267}268}, [editor.selection]);269270// Listen on the native `beforeinput` event to get real "Level 2" events. This271// is required because React's `beforeinput` is fake and never really attaches272// to the real event sadly. (2019/11/01)273// https://github.com/facebook/react/issues/11211274const onDOMBeforeInput = useCallback(275(276event: Event & {277data: string | null;278dataTransfer: DataTransfer | null;279getTargetRanges(): DOMStaticRange[];280inputType: string;281isComposing: boolean;282ctrlKey?: boolean;283altKey?: boolean;284metaKey?: boolean;285},286) => {287if (288!readOnly &&289hasEditableTarget(editor, event.target) &&290!isDOMEventHandled(event, propsOnDOMBeforeInput)291) {292const { selection } = editor;293const { inputType: type } = event;294const data = event.dataTransfer || event.data || undefined;295296// These two types occur while a user is composing text and can't be297// canceled. Let them through and wait for the composition to end.298if (299type === "insertCompositionText" ||300type === "deleteCompositionText"301) {302return;303}304305event.preventDefault();306307if (308type == "insertText" &&309!event.isComposing &&310event.data == " " &&311!(event.ctrlKey || event.altKey || event.metaKey)312) {313editor.insertText(" ", {});314return;315}316317// COMPAT: For the deleting forward/backward input types we don't want318// to change the selection because it is the range that will be deleted,319// and those commands determine that for themselves.320if (!type.startsWith("delete") || type.startsWith("deleteBy")) {321const [targetRange] = event.getTargetRanges();322323if (targetRange) {324let range;325try {326range = ReactEditor.toSlateRange(editor, targetRange);327} catch (err) {328console.warn(329"WARNING: onDOMBeforeInput -- unable to find SlateRange",330targetRange,331err,332);333return;334}335336if (!selection || !Range.equals(selection, range)) {337Transforms.select(editor, range);338}339}340}341342// COMPAT: If the selection is expanded, even if the command seems like343// a delete forward/backward command it should delete the selection.344if (345selection &&346Range.isExpanded(selection) &&347type.startsWith("delete")348) {349Editor.deleteFragment(editor);350return;351}352353switch (type) {354case "deleteByComposition":355case "deleteByCut":356case "deleteByDrag": {357Editor.deleteFragment(editor);358break;359}360361case "deleteContent":362case "deleteContentForward": {363Editor.deleteForward(editor);364break;365}366367case "deleteContentBackward": {368Editor.deleteBackward(editor);369break;370}371372case "deleteEntireSoftLine": {373Editor.deleteBackward(editor, { unit: "line" });374Editor.deleteForward(editor, { unit: "line" });375break;376}377378case "deleteHardLineBackward": {379Editor.deleteBackward(editor, { unit: "block" });380break;381}382383case "deleteSoftLineBackward": {384Editor.deleteBackward(editor, { unit: "line" });385break;386}387388case "deleteHardLineForward": {389Editor.deleteForward(editor, { unit: "block" });390break;391}392393case "deleteSoftLineForward": {394Editor.deleteForward(editor, { unit: "line" });395break;396}397398case "deleteWordBackward": {399Editor.deleteBackward(editor, { unit: "word" });400break;401}402403case "deleteWordForward": {404Editor.deleteForward(editor, { unit: "word" });405break;406}407408case "insertLineBreak":409case "insertParagraph": {410Editor.insertBreak(editor);411break;412}413414case "insertFromComposition": {415// COMPAT: in safari, `compositionend` event is dispatched after416// the beforeinput event with the inputType "insertFromComposition" has been dispatched.417// https://www.w3.org/TR/input-events-2/418// so the following code is the right logic419// because DOM selection in sync will be exec before `compositionend` event420// isComposing is true will prevent DOM selection being update correctly.421state.isComposing = false;422}423case "insertFromDrop":424case "insertFromPaste":425case "insertFromYank":426case "insertReplacementText":427case "insertText": {428try {429if (data instanceof DataTransfer) {430ReactEditor.insertData(editor, data);431} else if (typeof data === "string") {432Editor.insertText(editor, data);433}434} catch (err) {435// I've seen this crash several times in a way I can't reproduce, maybe436// when focusing (not sure). Better make it a warning with useful info.437console.warn(438`SLATE -- issue with DOM insertText/insertData operation ${err}, ${data}`,439);440}441442break;443}444}445}446},447[readOnly, propsOnDOMBeforeInput],448);449450// Attach a native DOM event handler for `beforeinput` events, because React's451// built-in `onBeforeInput` is actually a leaky polyfill that doesn't expose452// real `beforeinput` events sadly... (2019/11/04)453// https://github.com/facebook/react/issues/11211454useIsomorphicLayoutEffect(() => {455if (ref.current && HAS_BEFORE_INPUT_SUPPORT) {456// @ts-ignore The `beforeinput` event isn't recognized.457ref.current.addEventListener("beforeinput", onDOMBeforeInput);458}459return () => {460if (ref.current && HAS_BEFORE_INPUT_SUPPORT) {461// @ts-ignore The `beforeinput` event isn't recognized.462ref.current.removeEventListener("beforeinput", onDOMBeforeInput);463}464};465}, [onDOMBeforeInput]);466467useUpdateDOMSelection({ editor, state });468const DOMSelectionChange = useDOMSelectionChange({ editor, state, readOnly });469470const decorations = decorate([editor, []]);471472if (473placeholder &&474editor.children.length === 1 &&475Array.from(Node.texts(editor)).length === 1 &&476Node.string(editor) === ""477) {478const start = Editor.start(editor, []);479decorations.push({480[PLACEHOLDER_SYMBOL]: true,481placeholder,482anchor: start,483focus: start,484} as any);485}486487return (488<ReadOnlyContext.Provider value={readOnly}>489<Component490role={readOnly ? undefined : "textbox"}491{...attributes}492// COMPAT: Certain browsers don't support the `beforeinput` event, so we'd493// have to use hacks to make these replacement-based features work.494spellCheck={495!HAS_BEFORE_INPUT_SUPPORT ? undefined : attributes.spellCheck496}497autoCorrect={498!HAS_BEFORE_INPUT_SUPPORT ? undefined : attributes.autoCorrect499}500autoCapitalize={501!HAS_BEFORE_INPUT_SUPPORT ? undefined : attributes.autoCapitalize502}503data-slate-editor504data-slate-node="value"505contentEditable={readOnly ? undefined : true}506suppressContentEditableWarning507ref={ref}508style={{509// Prevent the default outline styles.510outline: "none",511// Preserve adjacent whitespace and new lines.512whiteSpace: "pre-wrap",513// Allow words to break if they are too long.514wordWrap: "break-word",515// Allow for passed-in styles to override anything.516...style,517}}518onScroll={519// When height is auto it's critical to keep the div from520// scrolling at all. Otherwise, especially when moving the521// cursor above and back down from a fenced code block, things522// will get scrolled off the screen and not be visible.523// The following code ensures that scrollTop is always 0524// in case of height:'auto'.525style.height === "auto"526? () => {527if (ref.current != null) {528ref.current.scrollTop = 0;529}530}531: undefined532}533onBeforeInput={useCallback(534(event: React.FormEvent<HTMLDivElement>) => {535// COMPAT: Certain browsers don't support the `beforeinput` event, so we536// fall back to React's leaky polyfill instead just for it. It537// only works for the `insertText` input type.538if (539!HAS_BEFORE_INPUT_SUPPORT &&540shouldHandle({ event, name: "onBeforeInput", notReadOnly: true })541) {542event.preventDefault();543const text = (event as any).data as string;544Editor.insertText(editor, text);545}546},547[readOnly],548)}549onBlur={useCallback(550(event: React.FocusEvent<HTMLDivElement>) => {551if (!shouldHandle({ event, name: "onBlur", notReadOnly: true })) {552return;553}554555// COMPAT: If the current `activeElement` is still the previous556// one, this is due to the window being blurred when the tab557// itself becomes unfocused, so we want to abort early to allow to558// editor to stay focused when the tab becomes focused again.559if (state.latestElement === window.document.activeElement) {560return;561}562563const { relatedTarget } = event;564const el = ReactEditor.toDOMNode(editor, editor);565566// COMPAT: The event should be ignored if the focus is returning567// to the editor from an embedded editable element (eg. an <input>568// element inside a void node).569if (relatedTarget === el) {570return;571}572573// COMPAT: The event should be ignored if the focus is moving from574// the editor to inside a void node's spacer element.575if (576isDOMElement(relatedTarget) &&577relatedTarget.hasAttribute("data-slate-spacer")578) {579return;580}581582// COMPAT: The event should be ignored if the focus is moving to a583// non- editable section of an element that isn't a void node (eg.584// a list item of the check list example).585if (586relatedTarget != null &&587isDOMNode(relatedTarget) &&588ReactEditor.hasDOMNode(editor, relatedTarget)589) {590const node = ReactEditor.toSlateNode(editor, relatedTarget);591592if (Element.isElement(node) && !editor.isVoid(node)) {593return;594}595}596597IS_FOCUSED.delete(editor);598},599[readOnly, attributes.onBlur],600)}601onClick={useCallback(602(event: React.MouseEvent<HTMLDivElement>) => {603if (604shouldHandle({605event,606name: "onClick",607notReadOnly: true,608editableTarget: false,609}) &&610isDOMNode(event.target)611) {612let node;613try {614node = ReactEditor.toSlateNode(editor, event.target);615} catch (err) {616// node not actually in editor.617return;618}619let path;620try {621path = ReactEditor.findPath(editor, node);622} catch (err) {623console.warn(624"WARNING: onClick -- unable to find path to node",625node,626err,627);628return;629}630const start = Editor.start(editor, path);631const end = Editor.end(editor, path);632633const startVoid = Editor.void(editor, { at: start });634const endVoid = Editor.void(editor, { at: end });635636// We set selection either if we're not637// focused *or* clicking on a void. The638// not focused part isn't upstream, but we639// need it to have codemirror blocks.640if (641editor.selection == null ||642!ReactEditor.isFocused(editor) ||643(startVoid && endVoid && Path.equals(startVoid[1], endVoid[1]))644) {645const range = Editor.range(editor, start);646Transforms.select(editor, range);647}648}649},650[readOnly, attributes.onClick],651)}652onCompositionEnd={useCallback(653(event: React.CompositionEvent<HTMLDivElement>) => {654if (655shouldHandle({656event,657name: "onCompositionEnd",658notReadOnly: true,659})660) {661state.isComposing = false;662// console.log(`onCompositionEnd :'${event.data}'`);663664// COMPAT: In Chrome, `beforeinput` events for compositions665// aren't correct and never fire the "insertFromComposition"666// type that we need. So instead, insert whenever a composition667// ends since it will already have been committed to the DOM.668if (!IS_SAFARI && !IS_FIREFOX && event.data) {669Editor.insertText(editor, event.data);670}671}672},673[attributes.onCompositionEnd],674)}675onCompositionStart={useCallback(676(event: React.CompositionEvent<HTMLDivElement>) => {677if (678shouldHandle({679event,680name: "onCompositionStart",681notReadOnly: true,682})683) {684state.isComposing = true;685// console.log("onCompositionStart");686}687},688[attributes.onCompositionStart],689)}690onCopy={useCallback(691(event: React.ClipboardEvent<HTMLDivElement>) => {692if (shouldHandle({ event, name: "onCopy" })) {693event.preventDefault();694ReactEditor.setFragmentData(editor, event.clipboardData);695}696},697[attributes.onCopy],698)}699onCut={useCallback(700(event: React.ClipboardEvent<HTMLDivElement>) => {701if (shouldHandle({ event, name: "onCut", notReadOnly: true })) {702event.preventDefault();703ReactEditor.setFragmentData(editor, event.clipboardData);704const { selection } = editor;705706if (selection) {707if (Range.isExpanded(selection)) {708Editor.deleteFragment(editor);709} else {710const node = Node.parent(editor, selection.anchor.path);711if (Element.isElement(node) && Editor.isVoid(editor, node)) {712Transforms.delete(editor);713}714}715}7161;717}718},719[readOnly, attributes.onCut],720)}721onDragOver={useCallback(722(event: React.DragEvent<HTMLDivElement>) => {723if (724shouldHandle({725event,726name: "onDragOver",727editableTarget: false,728})729) {730if (!hasTarget(editor, event.target)) return; // for typescript only731// Only when the target is void, call `preventDefault` to signal732// that drops are allowed. Editable content is droppable by733// default, and calling `preventDefault` hides the cursor.734const node = ReactEditor.toSlateNode(editor, event.target);735736if (Element.isElement(node) && Editor.isVoid(editor, node)) {737event.preventDefault();738}739}740},741[attributes.onDragOver],742)}743onDragStart={useCallback(744(event: React.DragEvent<HTMLDivElement>) => {745if (746shouldHandle({747event,748name: "onDragStart",749editableTarget: false,750})751) {752if (!hasTarget(editor, event.target)) return; // for typescript only753const node = ReactEditor.toSlateNode(editor, event.target);754let path;755try {756path = ReactEditor.findPath(editor, node);757} catch (err) {758console.warn(759"WARNING: onDragStart -- unable to find path to node",760node,761err,762);763return;764}765const voidMatch = Editor.void(editor, { at: path });766767// If starting a drag on a void node, make sure it is selected768// so that it shows up in the selection's fragment.769if (voidMatch) {770const range = Editor.range(editor, path);771Transforms.select(editor, range);772}773774ReactEditor.setFragmentData(editor, event.dataTransfer);775}776},777[attributes.onDragStart],778)}779onDrop={useCallback(780(event: React.DragEvent<HTMLDivElement>) => {781if (782shouldHandle({783event,784name: "onDrop",785editableTarget: false,786notReadOnly: true,787})788) {789// COMPAT: Certain browsers don't fire `beforeinput` events at all, and790// Chromium browsers don't properly fire them for files being791// dropped into a `contenteditable`. (2019/11/26)792// https://bugs.chromium.org/p/chromium/issues/detail?id=1028668793if (794!HAS_BEFORE_INPUT_SUPPORT ||795(!IS_SAFARI && event.dataTransfer.files.length > 0)796) {797event.preventDefault();798let range;799try {800range = ReactEditor.findEventRange(editor, event);801} catch (err) {802console.warn("WARNING: onDrop -- unable to find range", err);803return;804}805const data = event.dataTransfer;806Transforms.select(editor, range);807ReactEditor.insertData(editor, data);808}809}810},811[readOnly, attributes.onDrop],812)}813onFocus={useCallback(814(event: React.FocusEvent<HTMLDivElement>) => {815if (shouldHandle({ event, name: "onFocus", notReadOnly: true })) {816// Call DOMSelectionChange so we can capture what was just817// selected in the DOM to cause this focus.818DOMSelectionChange();819state.latestElement = window.document.activeElement;820IS_FOCUSED.set(editor, true);821}822},823[readOnly, attributes.onFocus],824)}825onKeyUp={useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {826state.shiftKey = event.shiftKey;827}, [])}828onKeyDown={useCallback(829(event: React.KeyboardEvent<HTMLDivElement>) => {830state.shiftKey = event.shiftKey;831if (832state.isComposing ||833!shouldHandle({ event, name: "onKeyDown", notReadOnly: true })834) {835return;836}837838const { nativeEvent } = event;839const { selection } = editor;840841// COMPAT: Since we prevent the default behavior on842// `beforeinput` events, the browser doesn't think there's ever843// any history stack to undo or redo, so we have to manage these844// hotkeys ourselves. (2019/11/06)845if (Hotkeys.isRedo(nativeEvent)) {846event.preventDefault();847848/*849if (HistoryEditor.isHistoryEditor(editor)) {850editor.redo();851}852*/853854return;855}856857if (Hotkeys.isUndo(nativeEvent)) {858event.preventDefault();859860/*861if (HistoryEditor.isHistoryEditor(editor)) {862editor.undo();863}*/864865return;866}867868// COMPAT: Certain browsers don't handle the selection updates869// properly. In Chrome, the selection isn't properly extended.870// And in Firefox, the selection isn't properly collapsed.871// (2017/10/17)872if (Hotkeys.isMoveLineBackward(nativeEvent)) {873event.preventDefault();874Transforms.move(editor, { unit: "line", reverse: true });875return;876}877878if (Hotkeys.isMoveLineForward(nativeEvent)) {879event.preventDefault();880Transforms.move(editor, { unit: "line" });881return;882}883884if (Hotkeys.isExtendLineBackward(nativeEvent)) {885event.preventDefault();886Transforms.move(editor, {887unit: "line",888edge: "focus",889reverse: true,890});891return;892}893894if (Hotkeys.isExtendLineForward(nativeEvent)) {895event.preventDefault();896Transforms.move(editor, { unit: "line", edge: "focus" });897return;898}899900const element = editor.children[selection?.focus.path[0] ?? 0];901// It's definitely possible in edge cases that element is undefined. I've hit this in production,902// resulting in "Node.string(undefined)", which crashes.903const isRTL =904element != null && getDirection(Node.string(element)) === "rtl";905906// COMPAT: If a void node is selected, or a zero-width text node907// adjacent to an inline is selected, we need to handle these908// hotkeys manually because browsers won't be able to skip over909// the void node with the zero-width space not being an empty910// string.911if (Hotkeys.isMoveBackward(nativeEvent)) {912event.preventDefault();913914if (selection && Range.isCollapsed(selection)) {915Transforms.move(editor, { reverse: !isRTL });916} else {917Transforms.collapse(editor, { edge: "start" });918}919920return;921}922923if (Hotkeys.isMoveForward(nativeEvent)) {924event.preventDefault();925926if (selection && Range.isCollapsed(selection)) {927Transforms.move(editor, { reverse: isRTL });928} else {929Transforms.collapse(editor, { edge: "end" });930}931932return;933}934935if (Hotkeys.isMoveWordBackward(nativeEvent)) {936event.preventDefault();937Transforms.move(editor, { unit: "word", reverse: !isRTL });938return;939}940941if (Hotkeys.isMoveWordForward(nativeEvent)) {942event.preventDefault();943Transforms.move(editor, { unit: "word", reverse: isRTL });944return;945}946947// COMPAT: Certain browsers don't support the `beforeinput` event, so we948// fall back to guessing at the input intention for hotkeys.949// COMPAT: In iOS, some of these hotkeys are handled in the950if (!HAS_BEFORE_INPUT_SUPPORT) {951// We don't have a core behavior for these, but they change the952// DOM if we don't prevent them, so we have to.953if (954Hotkeys.isBold(nativeEvent) ||955Hotkeys.isItalic(nativeEvent) ||956Hotkeys.isTransposeCharacter(nativeEvent)957) {958event.preventDefault();959return;960}961962if (Hotkeys.isSplitBlock(nativeEvent)) {963event.preventDefault();964Editor.insertBreak(editor);965return;966}967968if (Hotkeys.isDeleteBackward(nativeEvent)) {969event.preventDefault();970971if (selection && Range.isExpanded(selection)) {972Editor.deleteFragment(editor);973} else {974Editor.deleteBackward(editor);975}976977return;978}979980if (Hotkeys.isDeleteForward(nativeEvent)) {981event.preventDefault();982983if (selection && Range.isExpanded(selection)) {984Editor.deleteFragment(editor);985} else {986Editor.deleteForward(editor);987}988989return;990}991992if (Hotkeys.isDeleteLineBackward(nativeEvent)) {993event.preventDefault();994995if (selection && Range.isExpanded(selection)) {996Editor.deleteFragment(editor);997} else {998Editor.deleteBackward(editor, { unit: "line" });999}10001001return;1002}10031004if (Hotkeys.isDeleteLineForward(nativeEvent)) {1005event.preventDefault();10061007if (selection && Range.isExpanded(selection)) {1008Editor.deleteFragment(editor);1009} else {1010Editor.deleteForward(editor, { unit: "line" });1011}10121013return;1014}10151016if (Hotkeys.isDeleteWordBackward(nativeEvent)) {1017event.preventDefault();10181019if (selection && Range.isExpanded(selection)) {1020Editor.deleteFragment(editor);1021} else {1022Editor.deleteBackward(editor, { unit: "word" });1023}10241025return;1026}10271028if (Hotkeys.isDeleteWordForward(nativeEvent)) {1029event.preventDefault();10301031if (selection && Range.isExpanded(selection)) {1032Editor.deleteFragment(editor);1033} else {1034Editor.deleteForward(editor, { unit: "word" });1035}10361037return;1038}1039}10401041if (1042!event.altKey &&1043!event.ctrlKey &&1044!event.metaKey &&1045event.key.length == 1 &&1046!ReactEditor.selectionIsInDOM(editor)1047) {1048// user likely typed a character so insert it1049editor.insertText(event.key);1050event.preventDefault();1051return;1052}1053},1054[readOnly, attributes.onKeyDown],1055)}1056onPaste={useCallback(1057(event: React.ClipboardEvent<HTMLDivElement>) => {1058// COMPAT: Certain browsers don't support the `beforeinput` event, so we1059// fall back to React's `onPaste` here instead.1060// COMPAT: Firefox, Chrome and Safari are not emitting `beforeinput` events1061// when "paste without formatting" option is used.1062// This unfortunately needs to be handled with paste events instead.1063if (1064shouldHandle({ event, name: "onPaste", notReadOnly: true }) &&1065(!HAS_BEFORE_INPUT_SUPPORT ||1066isPlainTextOnlyPaste(event.nativeEvent))1067) {1068event.preventDefault();1069ReactEditor.insertData(editor, event.clipboardData);1070}1071},1072[readOnly, attributes.onPaste],1073)}1074>1075<DecorateContext.Provider value={decorate}>1076<Children1077isComposing={state.isComposing}1078decorations={decorations}1079node={editor}1080renderElement={renderElement}1081renderLeaf={renderLeaf}1082selection={editor.selection}1083hiddenChildren={hiddenChildren}1084windowing={windowing}1085onScroll={() => {1086if (editor.scrollCaretAfterNextScroll) {1087editor.scrollCaretAfterNextScroll = false;1088}1089editor.updateDOMSelection?.();1090props.onScroll?.({} as any);1091}}1092/>1093</DecorateContext.Provider>1094</Component>1095</ReadOnlyContext.Provider>1096);1097};10981099/**1100* A default memoized decorate function.1101*/11021103const defaultDecorate: (entry: NodeEntry) => Range[] = () => [];11041105/**1106* Check if an event is overrided by a handler.1107*/11081109const isEventHandled = <1110EventType extends React.SyntheticEvent<unknown, unknown>,1111>(1112event: EventType,1113handler?: (event: EventType) => void,1114) => {1115if (!handler) {1116return false;1117}11181119handler(event);1120return event.isDefaultPrevented() || event.isPropagationStopped();1121};11221123/**1124* Check if a DOM event is overrided by a handler.1125*/11261127const isDOMEventHandled = (event: Event, handler?: (event: Event) => void) => {1128if (!handler) {1129return false;1130}11311132handler(event);1133return event.defaultPrevented;1134};113511361137