Path: blob/master/src/packages/frontend/editors/slate/slate-react/components/editable.tsx
1698 views
import React from "react";1import { useEffect, useRef, useMemo, useState, useCallback } from "react";2import {3Editor,4Element,5NodeEntry,6Node,7Range,8Text,9Transforms,10Path,11} from "slate";12import Children from "./children";13import { WindowingParams } from "./children";14import Hotkeys from "../utils/hotkeys";15import {16IS_FIREFOX,17IS_SAFARI,18HAS_BEFORE_INPUT_SUPPORT,19} from "../utils/environment";20import { ReactEditor } from "..";21import { ReadOnlyContext } from "../hooks/use-read-only";22import { useSlate } from "../hooks/use-slate";23import { useIsomorphicLayoutEffect } from "../hooks/use-isomorphic-layout-effect";24import { DecorateContext } from "../hooks/use-decorate";25import {26DOMElement,27isDOMElement,28isDOMNode,29DOMStaticRange,30isPlainTextOnlyPaste,31} from "../utils/dom";32import {33EDITOR_TO_ELEMENT,34ELEMENT_TO_NODE,35IS_READ_ONLY,36NODE_TO_ELEMENT,37IS_FOCUSED,38PLACEHOLDER_SYMBOL,39} from "../utils/weak-maps";40import { debounce } from "lodash";41import getDirection from "direction";42import { useDOMSelectionChange, useUpdateDOMSelection } from "./selection-sync";43import { hasEditableTarget, hasTarget } from "./dom-utils";4445/**46* `RenderElementProps` are passed to the `renderElement` handler.47*/4849export interface RenderElementProps {50children: any;51element: Element;52attributes: {53"data-slate-node": "element";54"data-slate-inline"?: true;55"data-slate-void"?: true;56dir?: "rtl";57ref: any;58};59}60export const RenderElementProps = null; // webpack + TS es2020 modules need this6162/**63* `RenderLeafProps` are passed to the `renderLeaf` handler.64*/6566export interface RenderLeafProps {67children: any;68leaf: Text;69text: Text;70attributes: {71"data-slate-leaf": true;72};73}74export const RenderLeafProps = null; // webpack + TS es2020 modules need this7576/**77* `EditableProps` are passed to the `<Editable>` component.78*/7980export type EditableProps = {81decorate?: (entry: NodeEntry) => Range[];82onDOMBeforeInput?: (event: Event) => void;83placeholder?: string;84readOnly?: boolean;85role?: string;86style?: React.CSSProperties;87renderElement?: React.FC<RenderElementProps>;88renderLeaf?: React.FC<RenderLeafProps>;89as?: React.ElementType;90windowing?: WindowingParams;91divref?;92} & React.TextareaHTMLAttributes<HTMLDivElement>;9394/**95* Editable.96*/9798export const Editable: React.FC<EditableProps> = (props: EditableProps) => {99const {100windowing,101autoFocus,102decorate = defaultDecorate,103onDOMBeforeInput: propsOnDOMBeforeInput,104placeholder,105readOnly = false,106renderElement,107renderLeaf,108style = {},109as: Component = "div",110...attributes111} = props;112const editor = useSlate();113const ref = props.divref ?? useRef<HTMLDivElement>(null);114115// Return true if the given event should be handled116// by the event handler code defined below.117// Regarding slateIgnore below, we use this with codemirror118// editor for fenced code blocks and math.119// I couldn't find any way to get codemirror to allow the copy to happen,120// but at the same time to not let the event propogate.121const shouldHandle = useCallback(122(123{124event, // the event itself125name, // name of the event, e.g., "onClick"126notReadOnly, // require doc to not be readOnly (ignored if not specified)127editableTarget, // require event target to be editable (defaults to true if not specified!)128}: {129event;130name: string;131notReadOnly?: boolean;132editableTarget?: boolean;133}, // @ts-ignore134) =>135!event.nativeEvent?.slateIgnore &&136(notReadOnly == null || notReadOnly == !readOnly) &&137((editableTarget ?? true) == true138? hasEditableTarget(editor, event.target)139: hasTarget(editor, event.target)) &&140!isEventHandled(event, attributes[name]),141[editor, attributes, readOnly],142);143144// Update internal state on each render.145IS_READ_ONLY.set(editor, readOnly);146147// Keep track of some state for the event handler logic.148const state: {149isComposing: boolean;150latestElement: DOMElement | null;151shiftKey: boolean;152ignoreSelection: boolean;153} = useMemo(154() => ({155isComposing: false,156latestElement: null as DOMElement | null,157shiftKey: false,158ignoreSelection: false,159}),160[],161);162163// start ignoring the selection sync164editor.setIgnoreSelection = (value: boolean) => {165state.ignoreSelection = value;166};167// stop ignoring selection sync168editor.getIgnoreSelection = () => {169return state.ignoreSelection;170};171172// state whose change causes an update173const [hiddenChildren, setHiddenChildren] = useState<Set<number>>(174new Set([]),175);176177editor.updateHiddenChildren = useCallback(() => {178if (!ReactEditor.isUsingWindowing(editor)) return;179const hiddenChildren0: number[] = [];180let isCollapsed: boolean = false;181let level: number = 0;182let index: number = 0;183let hasAll: boolean = true;184for (const child of editor.children) {185if (!Element.isElement(child)) {186throw Error("bug");187}188if (child.type != "heading" || (isCollapsed && child.level > level)) {189if (isCollapsed) {190hiddenChildren0.push(index);191if (hasAll && !hiddenChildren.has(index)) {192hasAll = false;193}194}195} else {196// it's a heading of a high enough level, and it sets the new state.197// It is always visible.198isCollapsed = !!editor.collapsedSections.get(child);199level = child.level;200}201index += 1;202}203if (hasAll && hiddenChildren0.length == hiddenChildren.size) {204// no actual change (since subset and same cardinality), so don't205// cause re-render.206return;207}208setHiddenChildren(new Set(hiddenChildren0));209}, [editor.children, hiddenChildren]);210211const updateHiddenChildrenDebounce = useMemo(() => {212return debounce(() => editor.updateHiddenChildren(), 1000);213}, []);214215// When the actual document changes we soon update the216// hidden children set, since it is a list of indexes217// into editor.children, so may change. That said, we218// don't want this to impact performance when typing, so219// we debounce it, and it is unlikely that things change220// when the content (but not number) of children changes.221useEffect(updateHiddenChildrenDebounce, [editor.children]);222// We *always* immediately update when the number of children changes, since223// that is highly likely to make the hiddenChildren data structure wrong.224useEffect(() => editor.updateHiddenChildren(), [editor.children.length]);225226// Update element-related weak maps with the DOM element ref.227useIsomorphicLayoutEffect(() => {228if (ref.current) {229EDITOR_TO_ELEMENT.set(editor, ref.current);230NODE_TO_ELEMENT.set(editor, ref.current);231ELEMENT_TO_NODE.set(ref.current, editor);232} else {233NODE_TO_ELEMENT.delete(editor);234}235});236237// The autoFocus TextareaHTMLAttribute doesn't do anything on a div, so it238// needs to be manually focused.239useEffect(() => {240if (ref.current && autoFocus) {241ref.current.focus();242}243}, [autoFocus]);244245useIsomorphicLayoutEffect(() => {246// Whenever the selection changes and is collapsed, make247// sure the cursor is visible. Also, have a facility to248// ignore a single iteration of this, which we use when249// the selection change is being caused by realtime250// collaboration.251252// @ts-ignore253const skip = editor.syncCausedUpdate;254if (255editor.selection != null &&256Range.isCollapsed(editor.selection) &&257!skip258) {259editor.scrollCaretIntoView();260}261}, [editor.selection]);262263// Listen on the native `beforeinput` event to get real "Level 2" events. This264// is required because React's `beforeinput` is fake and never really attaches265// to the real event sadly. (2019/11/01)266// https://github.com/facebook/react/issues/11211267const onDOMBeforeInput = useCallback(268(269event: Event & {270data: string | null;271dataTransfer: DataTransfer | null;272getTargetRanges(): DOMStaticRange[];273inputType: string;274isComposing: boolean;275ctrlKey?: boolean;276altKey?: boolean;277metaKey?: boolean;278},279) => {280if (281!readOnly &&282hasEditableTarget(editor, event.target) &&283!isDOMEventHandled(event, propsOnDOMBeforeInput)284) {285const { selection } = editor;286const { inputType: type } = event;287const data = event.dataTransfer || event.data || undefined;288289// These two types occur while a user is composing text and can't be290// canceled. Let them through and wait for the composition to end.291if (292type === "insertCompositionText" ||293type === "deleteCompositionText"294) {295return;296}297298event.preventDefault();299300if (301type == "insertText" &&302!event.isComposing &&303event.data == " " &&304!(event.ctrlKey || event.altKey || event.metaKey)305) {306editor.insertText(" ", {});307return;308}309310// COMPAT: For the deleting forward/backward input types we don't want311// to change the selection because it is the range that will be deleted,312// and those commands determine that for themselves.313if (!type.startsWith("delete") || type.startsWith("deleteBy")) {314const [targetRange] = event.getTargetRanges();315316if (targetRange) {317let range;318try {319range = ReactEditor.toSlateRange(editor, targetRange);320} catch (err) {321console.warn(322"WARNING: onDOMBeforeInput -- unable to find SlateRange",323targetRange,324err,325);326return;327}328329if (!selection || !Range.equals(selection, range)) {330Transforms.select(editor, range);331}332}333}334335// COMPAT: If the selection is expanded, even if the command seems like336// a delete forward/backward command it should delete the selection.337if (338selection &&339Range.isExpanded(selection) &&340type.startsWith("delete")341) {342Editor.deleteFragment(editor);343return;344}345346switch (type) {347case "deleteByComposition":348case "deleteByCut":349case "deleteByDrag": {350Editor.deleteFragment(editor);351break;352}353354case "deleteContent":355case "deleteContentForward": {356Editor.deleteForward(editor);357break;358}359360case "deleteContentBackward": {361Editor.deleteBackward(editor);362break;363}364365case "deleteEntireSoftLine": {366Editor.deleteBackward(editor, { unit: "line" });367Editor.deleteForward(editor, { unit: "line" });368break;369}370371case "deleteHardLineBackward": {372Editor.deleteBackward(editor, { unit: "block" });373break;374}375376case "deleteSoftLineBackward": {377Editor.deleteBackward(editor, { unit: "line" });378break;379}380381case "deleteHardLineForward": {382Editor.deleteForward(editor, { unit: "block" });383break;384}385386case "deleteSoftLineForward": {387Editor.deleteForward(editor, { unit: "line" });388break;389}390391case "deleteWordBackward": {392Editor.deleteBackward(editor, { unit: "word" });393break;394}395396case "deleteWordForward": {397Editor.deleteForward(editor, { unit: "word" });398break;399}400401case "insertLineBreak":402case "insertParagraph": {403Editor.insertBreak(editor);404break;405}406407case "insertFromComposition": {408// COMPAT: in safari, `compositionend` event is dispatched after409// the beforeinput event with the inputType "insertFromComposition" has been dispatched.410// https://www.w3.org/TR/input-events-2/411// so the following code is the right logic412// because DOM selection in sync will be exec before `compositionend` event413// isComposing is true will prevent DOM selection being update correctly.414state.isComposing = false;415}416case "insertFromDrop":417case "insertFromPaste":418case "insertFromYank":419case "insertReplacementText":420case "insertText": {421try {422if (data instanceof DataTransfer) {423ReactEditor.insertData(editor, data);424} else if (typeof data === "string") {425Editor.insertText(editor, data);426}427} catch (err) {428// I've seen this crash several times in a way I can't reproduce, maybe429// when focusing (not sure). Better make it a warning with useful info.430console.warn(431`SLATE -- issue with DOM insertText/insertData operation ${err}, ${data}`,432);433}434435break;436}437}438}439},440[readOnly, propsOnDOMBeforeInput],441);442443// Attach a native DOM event handler for `beforeinput` events, because React's444// built-in `onBeforeInput` is actually a leaky polyfill that doesn't expose445// real `beforeinput` events sadly... (2019/11/04)446// https://github.com/facebook/react/issues/11211447useIsomorphicLayoutEffect(() => {448if (ref.current && HAS_BEFORE_INPUT_SUPPORT) {449// @ts-ignore The `beforeinput` event isn't recognized.450ref.current.addEventListener("beforeinput", onDOMBeforeInput);451}452return () => {453if (ref.current && HAS_BEFORE_INPUT_SUPPORT) {454// @ts-ignore The `beforeinput` event isn't recognized.455ref.current.removeEventListener("beforeinput", onDOMBeforeInput);456}457};458}, [onDOMBeforeInput]);459460useUpdateDOMSelection({ editor, state });461const DOMSelectionChange = useDOMSelectionChange({ editor, state, readOnly });462463const decorations = decorate([editor, []]);464465if (466placeholder &&467editor.children.length === 1 &&468Array.from(Node.texts(editor)).length === 1 &&469Node.string(editor) === ""470) {471const start = Editor.start(editor, []);472decorations.push({473[PLACEHOLDER_SYMBOL]: true,474placeholder,475anchor: start,476focus: start,477} as any);478}479480return (481<ReadOnlyContext.Provider value={readOnly}>482<Component483role={readOnly ? undefined : "textbox"}484{...attributes}485// COMPAT: Certain browsers don't support the `beforeinput` event, so we'd486// have to use hacks to make these replacement-based features work.487spellCheck={488!HAS_BEFORE_INPUT_SUPPORT ? undefined : attributes.spellCheck489}490autoCorrect={491!HAS_BEFORE_INPUT_SUPPORT ? undefined : attributes.autoCorrect492}493autoCapitalize={494!HAS_BEFORE_INPUT_SUPPORT ? undefined : attributes.autoCapitalize495}496data-slate-editor497data-slate-node="value"498contentEditable={readOnly ? undefined : true}499suppressContentEditableWarning500ref={ref}501style={{502// Prevent the default outline styles.503outline: "none",504// Preserve adjacent whitespace and new lines.505whiteSpace: "pre-wrap",506// Allow words to break if they are too long.507wordWrap: "break-word",508// Allow for passed-in styles to override anything.509...style,510}}511onScroll={512// When height is auto it's critical to keep the div from513// scrolling at all. Otherwise, especially when moving the514// cursor above and back down from a fenced code block, things515// will get scrolled off the screen and not be visible.516// The following code ensures that scrollTop is always 0517// in case of height:'auto'.518style.height === "auto"519? () => {520if (ref.current != null) {521ref.current.scrollTop = 0;522}523}524: undefined525}526onBeforeInput={useCallback(527(event: React.FormEvent<HTMLDivElement>) => {528// COMPAT: Certain browsers don't support the `beforeinput` event, so we529// fall back to React's leaky polyfill instead just for it. It530// only works for the `insertText` input type.531if (532!HAS_BEFORE_INPUT_SUPPORT &&533shouldHandle({ event, name: "onBeforeInput", notReadOnly: true })534) {535event.preventDefault();536const text = (event as any).data as string;537Editor.insertText(editor, text);538}539},540[readOnly],541)}542onBlur={useCallback(543(event: React.FocusEvent<HTMLDivElement>) => {544if (!shouldHandle({ event, name: "onBlur", notReadOnly: true })) {545return;546}547548// COMPAT: If the current `activeElement` is still the previous549// one, this is due to the window being blurred when the tab550// itself becomes unfocused, so we want to abort early to allow to551// editor to stay focused when the tab becomes focused again.552if (state.latestElement === window.document.activeElement) {553return;554}555556const { relatedTarget } = event;557const el = ReactEditor.toDOMNode(editor, editor);558559// COMPAT: The event should be ignored if the focus is returning560// to the editor from an embedded editable element (eg. an <input>561// element inside a void node).562if (relatedTarget === el) {563return;564}565566// COMPAT: The event should be ignored if the focus is moving from567// the editor to inside a void node's spacer element.568if (569isDOMElement(relatedTarget) &&570relatedTarget.hasAttribute("data-slate-spacer")571) {572return;573}574575// COMPAT: The event should be ignored if the focus is moving to a576// non- editable section of an element that isn't a void node (eg.577// a list item of the check list example).578if (579relatedTarget != null &&580isDOMNode(relatedTarget) &&581ReactEditor.hasDOMNode(editor, relatedTarget)582) {583const node = ReactEditor.toSlateNode(editor, relatedTarget);584585if (Element.isElement(node) && !editor.isVoid(node)) {586return;587}588}589590IS_FOCUSED.delete(editor);591},592[readOnly, attributes.onBlur],593)}594onClick={useCallback(595(event: React.MouseEvent<HTMLDivElement>) => {596if (597shouldHandle({598event,599name: "onClick",600notReadOnly: true,601editableTarget: false,602}) &&603isDOMNode(event.target)604) {605let node;606try {607node = ReactEditor.toSlateNode(editor, event.target);608} catch (err) {609// node not actually in editor.610return;611}612let path;613try {614path = ReactEditor.findPath(editor, node);615} catch (err) {616console.warn(617"WARNING: onClick -- unable to find path to node",618node,619err,620);621return;622}623const start = Editor.start(editor, path);624const end = Editor.end(editor, path);625626const startVoid = Editor.void(editor, { at: start });627const endVoid = Editor.void(editor, { at: end });628629// We set selection either if we're not630// focused *or* clicking on a void. The631// not focused part isn't upstream, but we632// need it to have codemirror blocks.633if (634editor.selection == null ||635!ReactEditor.isFocused(editor) ||636(startVoid && endVoid && Path.equals(startVoid[1], endVoid[1]))637) {638const range = Editor.range(editor, start);639Transforms.select(editor, range);640}641}642},643[readOnly, attributes.onClick],644)}645onCompositionEnd={useCallback(646(event: React.CompositionEvent<HTMLDivElement>) => {647if (648shouldHandle({649event,650name: "onCompositionEnd",651notReadOnly: true,652})653) {654state.isComposing = false;655// console.log(`onCompositionEnd :'${event.data}'`);656657// COMPAT: In Chrome, `beforeinput` events for compositions658// aren't correct and never fire the "insertFromComposition"659// type that we need. So instead, insert whenever a composition660// ends since it will already have been committed to the DOM.661if (!IS_SAFARI && !IS_FIREFOX && event.data) {662Editor.insertText(editor, event.data);663}664}665},666[attributes.onCompositionEnd],667)}668onCompositionStart={useCallback(669(event: React.CompositionEvent<HTMLDivElement>) => {670if (671shouldHandle({672event,673name: "onCompositionStart",674notReadOnly: true,675})676) {677state.isComposing = true;678// console.log("onCompositionStart");679}680},681[attributes.onCompositionStart],682)}683onCopy={useCallback(684(event: React.ClipboardEvent<HTMLDivElement>) => {685if (shouldHandle({ event, name: "onCopy" })) {686event.preventDefault();687ReactEditor.setFragmentData(editor, event.clipboardData);688}689},690[attributes.onCopy],691)}692onCut={useCallback(693(event: React.ClipboardEvent<HTMLDivElement>) => {694if (shouldHandle({ event, name: "onCut", notReadOnly: true })) {695event.preventDefault();696ReactEditor.setFragmentData(editor, event.clipboardData);697const { selection } = editor;698699if (selection) {700if (Range.isExpanded(selection)) {701Editor.deleteFragment(editor);702} else {703const node = Node.parent(editor, selection.anchor.path);704if (Element.isElement(node) && Editor.isVoid(editor, node)) {705Transforms.delete(editor);706}707}708}7091;710}711},712[readOnly, attributes.onCut],713)}714onDragOver={useCallback(715(event: React.DragEvent<HTMLDivElement>) => {716if (717shouldHandle({718event,719name: "onDragOver",720editableTarget: false,721})722) {723if (!hasTarget(editor, event.target)) return; // for typescript only724// Only when the target is void, call `preventDefault` to signal725// that drops are allowed. Editable content is droppable by726// default, and calling `preventDefault` hides the cursor.727const node = ReactEditor.toSlateNode(editor, event.target);728729if (Element.isElement(node) && Editor.isVoid(editor, node)) {730event.preventDefault();731}732}733},734[attributes.onDragOver],735)}736onDragStart={useCallback(737(event: React.DragEvent<HTMLDivElement>) => {738if (739shouldHandle({740event,741name: "onDragStart",742editableTarget: false,743})744) {745if (!hasTarget(editor, event.target)) return; // for typescript only746const node = ReactEditor.toSlateNode(editor, event.target);747let path;748try {749path = ReactEditor.findPath(editor, node);750} catch (err) {751console.warn(752"WARNING: onDragStart -- unable to find path to node",753node,754err,755);756return;757}758const voidMatch = Editor.void(editor, { at: path });759760// If starting a drag on a void node, make sure it is selected761// so that it shows up in the selection's fragment.762if (voidMatch) {763const range = Editor.range(editor, path);764Transforms.select(editor, range);765}766767ReactEditor.setFragmentData(editor, event.dataTransfer);768}769},770[attributes.onDragStart],771)}772onDrop={useCallback(773(event: React.DragEvent<HTMLDivElement>) => {774if (775shouldHandle({776event,777name: "onDrop",778editableTarget: false,779notReadOnly: true,780})781) {782// COMPAT: Certain browsers don't fire `beforeinput` events at all, and783// Chromium browsers don't properly fire them for files being784// dropped into a `contenteditable`. (2019/11/26)785// https://bugs.chromium.org/p/chromium/issues/detail?id=1028668786if (787!HAS_BEFORE_INPUT_SUPPORT ||788(!IS_SAFARI && event.dataTransfer.files.length > 0)789) {790event.preventDefault();791let range;792try {793range = ReactEditor.findEventRange(editor, event);794} catch (err) {795console.warn("WARNING: onDrop -- unable to find range", err);796return;797}798const data = event.dataTransfer;799Transforms.select(editor, range);800ReactEditor.insertData(editor, data);801}802}803},804[readOnly, attributes.onDrop],805)}806onFocus={useCallback(807(event: React.FocusEvent<HTMLDivElement>) => {808if (shouldHandle({ event, name: "onFocus", notReadOnly: true })) {809// Call DOMSelectionChange so we can capture what was just810// selected in the DOM to cause this focus.811DOMSelectionChange();812state.latestElement = window.document.activeElement;813IS_FOCUSED.set(editor, true);814}815},816[readOnly, attributes.onFocus],817)}818onKeyUp={useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {819state.shiftKey = event.shiftKey;820}, [])}821onKeyDown={useCallback(822(event: React.KeyboardEvent<HTMLDivElement>) => {823state.shiftKey = event.shiftKey;824if (825state.isComposing ||826!shouldHandle({ event, name: "onKeyDown", notReadOnly: true })827) {828return;829}830831const { nativeEvent } = event;832const { selection } = editor;833834// COMPAT: Since we prevent the default behavior on835// `beforeinput` events, the browser doesn't think there's ever836// any history stack to undo or redo, so we have to manage these837// hotkeys ourselves. (2019/11/06)838if (Hotkeys.isRedo(nativeEvent)) {839event.preventDefault();840841/*842if (HistoryEditor.isHistoryEditor(editor)) {843editor.redo();844}845*/846847return;848}849850if (Hotkeys.isUndo(nativeEvent)) {851event.preventDefault();852853/*854if (HistoryEditor.isHistoryEditor(editor)) {855editor.undo();856}*/857858return;859}860861// COMPAT: Certain browsers don't handle the selection updates862// properly. In Chrome, the selection isn't properly extended.863// And in Firefox, the selection isn't properly collapsed.864// (2017/10/17)865if (Hotkeys.isMoveLineBackward(nativeEvent)) {866event.preventDefault();867Transforms.move(editor, { unit: "line", reverse: true });868return;869}870871if (Hotkeys.isMoveLineForward(nativeEvent)) {872event.preventDefault();873Transforms.move(editor, { unit: "line" });874return;875}876877if (Hotkeys.isExtendLineBackward(nativeEvent)) {878event.preventDefault();879Transforms.move(editor, {880unit: "line",881edge: "focus",882reverse: true,883});884return;885}886887if (Hotkeys.isExtendLineForward(nativeEvent)) {888event.preventDefault();889Transforms.move(editor, { unit: "line", edge: "focus" });890return;891}892893const element = editor.children[selection?.focus.path[0] ?? 0];894// It's definitely possible in edge cases that element is undefined. I've hit this in production,895// resulting in "Node.string(undefined)", which crashes.896const isRTL =897element != null && getDirection(Node.string(element)) === "rtl";898899// COMPAT: If a void node is selected, or a zero-width text node900// adjacent to an inline is selected, we need to handle these901// hotkeys manually because browsers won't be able to skip over902// the void node with the zero-width space not being an empty903// string.904if (Hotkeys.isMoveBackward(nativeEvent)) {905event.preventDefault();906907if (selection && Range.isCollapsed(selection)) {908Transforms.move(editor, { reverse: !isRTL });909} else {910Transforms.collapse(editor, { edge: "start" });911}912913return;914}915916if (Hotkeys.isMoveForward(nativeEvent)) {917event.preventDefault();918919if (selection && Range.isCollapsed(selection)) {920Transforms.move(editor, { reverse: isRTL });921} else {922Transforms.collapse(editor, { edge: "end" });923}924925return;926}927928if (Hotkeys.isMoveWordBackward(nativeEvent)) {929event.preventDefault();930Transforms.move(editor, { unit: "word", reverse: !isRTL });931return;932}933934if (Hotkeys.isMoveWordForward(nativeEvent)) {935event.preventDefault();936Transforms.move(editor, { unit: "word", reverse: isRTL });937return;938}939940// COMPAT: Certain browsers don't support the `beforeinput` event, so we941// fall back to guessing at the input intention for hotkeys.942// COMPAT: In iOS, some of these hotkeys are handled in the943if (!HAS_BEFORE_INPUT_SUPPORT) {944// We don't have a core behavior for these, but they change the945// DOM if we don't prevent them, so we have to.946if (947Hotkeys.isBold(nativeEvent) ||948Hotkeys.isItalic(nativeEvent) ||949Hotkeys.isTransposeCharacter(nativeEvent)950) {951event.preventDefault();952return;953}954955if (Hotkeys.isSplitBlock(nativeEvent)) {956event.preventDefault();957Editor.insertBreak(editor);958return;959}960961if (Hotkeys.isDeleteBackward(nativeEvent)) {962event.preventDefault();963964if (selection && Range.isExpanded(selection)) {965Editor.deleteFragment(editor);966} else {967Editor.deleteBackward(editor);968}969970return;971}972973if (Hotkeys.isDeleteForward(nativeEvent)) {974event.preventDefault();975976if (selection && Range.isExpanded(selection)) {977Editor.deleteFragment(editor);978} else {979Editor.deleteForward(editor);980}981982return;983}984985if (Hotkeys.isDeleteLineBackward(nativeEvent)) {986event.preventDefault();987988if (selection && Range.isExpanded(selection)) {989Editor.deleteFragment(editor);990} else {991Editor.deleteBackward(editor, { unit: "line" });992}993994return;995}996997if (Hotkeys.isDeleteLineForward(nativeEvent)) {998event.preventDefault();9991000if (selection && Range.isExpanded(selection)) {1001Editor.deleteFragment(editor);1002} else {1003Editor.deleteForward(editor, { unit: "line" });1004}10051006return;1007}10081009if (Hotkeys.isDeleteWordBackward(nativeEvent)) {1010event.preventDefault();10111012if (selection && Range.isExpanded(selection)) {1013Editor.deleteFragment(editor);1014} else {1015Editor.deleteBackward(editor, { unit: "word" });1016}10171018return;1019}10201021if (Hotkeys.isDeleteWordForward(nativeEvent)) {1022event.preventDefault();10231024if (selection && Range.isExpanded(selection)) {1025Editor.deleteFragment(editor);1026} else {1027Editor.deleteForward(editor, { unit: "word" });1028}10291030return;1031}1032}10331034if (1035!event.altKey &&1036!event.ctrlKey &&1037!event.metaKey &&1038event.key.length == 1 &&1039!ReactEditor.selectionIsInDOM(editor)1040) {1041// user likely typed a character so insert it1042editor.insertText(event.key);1043event.preventDefault();1044return;1045}1046},1047[readOnly, attributes.onKeyDown],1048)}1049onPaste={useCallback(1050(event: React.ClipboardEvent<HTMLDivElement>) => {1051// COMPAT: Certain browsers don't support the `beforeinput` event, so we1052// fall back to React's `onPaste` here instead.1053// COMPAT: Firefox, Chrome and Safari are not emitting `beforeinput` events1054// when "paste without formatting" option is used.1055// This unfortunately needs to be handled with paste events instead.1056if (1057shouldHandle({ event, name: "onPaste", notReadOnly: true }) &&1058(!HAS_BEFORE_INPUT_SUPPORT ||1059isPlainTextOnlyPaste(event.nativeEvent))1060) {1061event.preventDefault();1062ReactEditor.insertData(editor, event.clipboardData);1063}1064},1065[readOnly, attributes.onPaste],1066)}1067>1068<DecorateContext.Provider value={decorate}>1069<Children1070isComposing={state.isComposing}1071decorations={decorations}1072node={editor}1073renderElement={renderElement}1074renderLeaf={renderLeaf}1075selection={editor.selection}1076hiddenChildren={hiddenChildren}1077windowing={windowing}1078onScroll={() => {1079if (editor.scrollCaretAfterNextScroll) {1080editor.scrollCaretAfterNextScroll = false;1081}1082editor.updateDOMSelection?.();1083props.onScroll?.({} as any);1084}}1085/>1086</DecorateContext.Provider>1087</Component>1088</ReadOnlyContext.Provider>1089);1090};10911092/**1093* A default memoized decorate function.1094*/10951096const defaultDecorate: (entry: NodeEntry) => Range[] = () => [];10971098/**1099* Check if an event is overrided by a handler.1100*/11011102const isEventHandled = <1103EventType extends React.SyntheticEvent<unknown, unknown>,1104>(1105event: EventType,1106handler?: (event: EventType) => void,1107) => {1108if (!handler) {1109return false;1110}11111112handler(event);1113return event.isDefaultPrevented() || event.isPropagationStopped();1114};11151116/**1117* Check if a DOM event is overrided by a handler.1118*/11191120const isDOMEventHandled = (event: Event, handler?: (event: Event) => void) => {1121if (!handler) {1122return false;1123}11241125handler(event);1126return event.defaultPrevented;1127};112811291130