function editOnBeforeInput()

in src/component/handlers/edit/editOnBeforeInput.js [82:287]


function editOnBeforeInput(
  editor: DraftEditor,
  e: SyntheticInputEvent<HTMLElement>,
): void {
  // We need this here in case this beforeInput fires before our
  // immediate below had a chance to fire in IE (say, the user is
  // typing fast).
  if (isIE) {
    if (editor._pendingStateFromBeforeInput !== undefined) {
      editor.update(editor._pendingStateFromBeforeInput);
      editor._pendingStateFromBeforeInput = undefined;
    }
  }

  const editorState = editor._latestEditorState;

  const chars = e.data;

  // In some cases (ex: IE ideographic space insertion) no character data
  // is provided. There's nothing to do when this happens.
  if (!chars) {
    return;
  }

  // Allow the top-level component to handle the insertion manually. This is
  // useful when triggering interesting behaviors for a character insertion,
  // Simple examples: replacing a raw text ':)' with a smile emoji or image
  // decorator, or setting a block to be a list item after typing '- ' at the
  // start of the block.
  if (
    editor.props.handleBeforeInput &&
    isEventHandled(
      editor.props.handleBeforeInput(chars, editorState, e.timeStamp),
    )
  ) {
    e.preventDefault();
    return;
  }

  // If selection is collapsed, conditionally allow native behavior. This
  // reduces re-renders and preserves spellcheck highlighting. If the selection
  // is not collapsed, we will re-render.
  const selection = editorState.getSelection();
  const selectionStart = selection.getStartOffset();
  const anchorKey = selection.getAnchorKey();

  if (!selection.isCollapsed()) {
    e.preventDefault();
    editor.update(
      replaceText(
        editorState,
        chars,
        editorState.getCurrentInlineStyle(),
        getEntityKeyForSelection(
          editorState.getCurrentContent(),
          editorState.getSelection(),
        ),
        true,
      ),
    );
    return;
  }

  let newEditorState = replaceText(
    editorState,
    chars,
    editorState.getCurrentInlineStyle(),
    getEntityKeyForSelection(
      editorState.getCurrentContent(),
      editorState.getSelection(),
    ),
    false,
  );

  // Bunch of different cases follow where we need to prevent native insertion.
  let mustPreventNative = false;
  if (!mustPreventNative) {
    // Browsers tend to insert text in weird places in the DOM when typing at
    // the start of a leaf, so we'll handle it ourselves.
    mustPreventNative = isSelectionAtLeafStart(
      editor._latestCommittedEditorState,
    );
  }
  if (!mustPreventNative) {
    // Let's say we have a decorator that highlights hashtags. In many cases
    // we need to prevent native behavior and rerender ourselves --
    // particularly, any case *except* where the inserted characters end up
    // anywhere except exactly where you put them.
    //
    // Using [] to denote a decorated leaf, some examples:
    //
    // 1. 'hi #' and append 'f'
    // desired rendering: 'hi [#f]'
    // native rendering would be: 'hi #f' (incorrect)
    //
    // 2. 'x [#foo]' and insert '#' before 'f'
    // desired rendering: 'x #[#foo]'
    // native rendering would be: 'x [##foo]' (incorrect)
    //
    // 3. '[#foobar]' and insert ' ' between 'foo' and 'bar'
    // desired rendering: '[#foo] bar'
    // native rendering would be: '[#foo bar]' (incorrect)
    //
    // 4. '[#foo]' and delete '#' [won't use this beforeinput codepath though]
    // desired rendering: 'foo'
    // native rendering would be: '[foo]' (incorrect)
    //
    // 5. '[#foo]' and append 'b'
    // desired rendering: '[#foob]'
    // native rendering would be: '[#foob]'
    // (native insertion here would be ok for decorators like simple spans,
    // but not more complex decorators. To be safe, we need to prevent it.)
    //
    // It is safe to allow native insertion if and only if the full list of
    // decorator ranges matches what we expect native insertion to give, and
    // the range lengths have not changed. We don't need to compare the content
    // because the only possible mutation to consider here is inserting plain
    // text and decorators can't affect text content.
    const oldBlockTree = editorState.getBlockTree(anchorKey);
    const newBlockTree = newEditorState.getBlockTree(anchorKey);
    mustPreventNative =
      oldBlockTree.size !== newBlockTree.size ||
      oldBlockTree.zip(newBlockTree).some(([oldLeafSet, newLeafSet]) => {
        // selectionStart is guaranteed to be selectionEnd here
        const oldStart = oldLeafSet.get('start');
        const adjustedStart =
          oldStart + (oldStart >= selectionStart ? chars.length : 0);
        const oldEnd = oldLeafSet.get('end');
        const adjustedEnd =
          oldEnd + (oldEnd >= selectionStart ? chars.length : 0);
        const newStart = newLeafSet.get('start');
        const newEnd = newLeafSet.get('end');
        const newDecoratorKey = newLeafSet.get('decoratorKey');
        return (
          // Different decorators
          oldLeafSet.get('decoratorKey') !== newDecoratorKey ||
          // Different number of inline styles
          oldLeafSet.get('leaves').size !== newLeafSet.get('leaves').size ||
          // Different effective decorator position
          adjustedStart !== newStart ||
          adjustedEnd !== newEnd ||
          // Decorator already existed and its length changed
          (newDecoratorKey != null && newEnd - newStart !== oldEnd - oldStart)
        );
      });
  }
  if (!mustPreventNative) {
    mustPreventNative = mustPreventDefaultForCharacter(chars);
  }
  if (!mustPreventNative) {
    mustPreventNative =
      nullthrows(newEditorState.getDirectionMap()).get(anchorKey) !==
      nullthrows(editorState.getDirectionMap()).get(anchorKey);
  }

  if (mustPreventNative) {
    e.preventDefault();
    newEditorState = EditorState.set(newEditorState, {
      forceSelection: true,
    });
    editor.update(newEditorState);
    return;
  }

  newEditorState = EditorState.set(newEditorState, {
    nativelyRenderedContent: newEditorState.getCurrentContent(),
  });

  // We have newEditorState, but we just don't want to call "editor.update"
  // just yet. So let's store this state updated with our change to be consumed
  // later, after the native event occurs and the browser inserts the char.
  // After that, when we rerender, the text we see in the DOM will already have
  // been inserted properly.
  //
  editor._pendingStateFromBeforeInput = newEditorState;
  //
  // Part of the reason to do this is because browsers seem to change their
  // behaviour if you preventDefault(). For example, on macOS the browser seems
  // to believe it's no longer in a contenteditable and will change the
  // Touch Bar on a MacBook to stop showing text suggestions.
  //
  // Later (presumably after we render), it realizes "hold up, I am in a content
  // editable, silly me" and shows the suggestions again. But in the meantime
  // what we get is flickering between suggestions and no suggestions. We
  // should probably report this to Apple.
  //
  // Anyway, above we update our editor state if we prevent the native event, since
  // there will be no input event after we preventDefault. Otherwise, we will
  // do so in the "input" event, which fires once the text is inserted.
  //
  // There is one exception however: IE (what a surprise!). IE doesn't fire
  // input events (and React doesn't polyfill them), so we never get to see
  // how the text changed and we never get to call editor.update (which triggers
  // onChange).
  //
  // To get around this, we schedule an immediate to call our usual input
  // handler. It's important that this be an immediate so that no other random
  // tasks from the web page get on the way (mimicking what would happen if the
  // browser fired both the beforeInput and input events). Calling our usual
  // input handler does the trick.
  if (isIE) {
    setImmediate(() => {
      editOnInput(editor, null);
    });
  }
}