BookStackApp_BookStack/resources/js/wysiwyg/lexical/rich-text/index.ts
Dan Brown 2add15bd72
Lexical: Added direction support to extra blocks
Also removed duplicated dir functionality that remained in core.
2024-09-22 12:07:24 +01:00

1056 lines
28 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
CommandPayloadType,
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
ElementFormatType,
LexicalCommand,
LexicalEditor,
LexicalNode,
NodeKey,
ParagraphNode,
PasteCommandType,
RangeSelection,
SerializedElementNode,
Spread,
TextFormatType,
} from 'lexical';
import {
$insertDataTransferForRichText,
copyToClipboard,
} from '@lexical/clipboard';
import {
$moveCharacter,
$shouldOverrideDefaultCharacterSelection,
} from '@lexical/selection';
import {
$findMatchingParent,
$getNearestBlockElementAncestorOrThrow,
addClassNamesToElement,
isHTMLElement,
mergeRegister,
objectKlassEquals,
} from '@lexical/utils';
import {
$applyNodeReplacement,
$createParagraphNode,
$createRangeSelection,
$createTabNode,
$getAdjacentNode,
$getNearestNodeFromDOMNode,
$getRoot,
$getSelection,
$insertNodes,
$isDecoratorNode,
$isElementNode,
$isNodeSelection,
$isRangeSelection,
$isRootNode,
$isTextNode,
$normalizeSelection__EXPERIMENTAL,
$selectAll,
$setSelection,
CLICK_COMMAND,
COMMAND_PRIORITY_EDITOR,
CONTROLLED_TEXT_INSERTION_COMMAND,
COPY_COMMAND,
createCommand,
CUT_COMMAND,
DELETE_CHARACTER_COMMAND,
DELETE_LINE_COMMAND,
DELETE_WORD_COMMAND,
DRAGOVER_COMMAND,
DRAGSTART_COMMAND,
DROP_COMMAND,
ElementNode,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
INSERT_LINE_BREAK_COMMAND,
INSERT_PARAGRAPH_COMMAND,
INSERT_TAB_COMMAND,
isSelectionCapturedInDecoratorInput,
KEY_ARROW_DOWN_COMMAND,
KEY_ARROW_LEFT_COMMAND,
KEY_ARROW_RIGHT_COMMAND,
KEY_ARROW_UP_COMMAND,
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
KEY_ENTER_COMMAND,
KEY_ESCAPE_COMMAND,
OUTDENT_CONTENT_COMMAND,
PASTE_COMMAND,
REMOVE_TEXT_COMMAND,
SELECT_ALL_COMMAND,
} from 'lexical';
import caretFromPoint from 'lexical/shared/caretFromPoint';
import {
CAN_USE_BEFORE_INPUT,
IS_APPLE_WEBKIT,
IS_IOS,
IS_SAFARI,
} from 'lexical/shared/environment';
export type SerializedHeadingNode = Spread<
{
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
},
SerializedElementNode
>;
export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
'DRAG_DROP_PASTE_FILE',
);
export type SerializedQuoteNode = SerializedElementNode;
/** @noInheritDoc */
export class QuoteNode extends ElementNode {
static getType(): string {
return 'quote';
}
static clone(node: QuoteNode): QuoteNode {
return new QuoteNode(node.__key);
}
constructor(key?: NodeKey) {
super(key);
}
// View
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('blockquote');
addClassNamesToElement(element, config.theme.quote);
return element;
}
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {
blockquote: (node: Node) => ({
conversion: $convertBlockquoteElement,
priority: 0,
}),
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
const formatType = this.getFormatType();
element.style.textAlign = formatType;
}
return {
element,
};
}
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
const node = $createQuoteNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'quote',
};
}
// Mutation
insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
const newBlock = $createParagraphNode();
const direction = this.getDirection();
newBlock.setDirection(direction);
this.insertAfter(newBlock, restoreSelection);
return newBlock;
}
collapseAtStart(): true {
const paragraph = $createParagraphNode();
const children = this.getChildren();
children.forEach((child) => paragraph.append(child));
this.replace(paragraph);
return true;
}
canMergeWhenEmpty(): true {
return true;
}
}
export function $createQuoteNode(): QuoteNode {
return $applyNodeReplacement(new QuoteNode());
}
export function $isQuoteNode(
node: LexicalNode | null | undefined,
): node is QuoteNode {
return node instanceof QuoteNode;
}
export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
/** @noInheritDoc */
export class HeadingNode extends ElementNode {
/** @internal */
__tag: HeadingTagType;
static getType(): string {
return 'heading';
}
static clone(node: HeadingNode): HeadingNode {
return new HeadingNode(node.__tag, node.__key);
}
constructor(tag: HeadingTagType, key?: NodeKey) {
super(key);
this.__tag = tag;
}
getTag(): HeadingTagType {
return this.__tag;
}
// View
createDOM(config: EditorConfig): HTMLElement {
const tag = this.__tag;
const element = document.createElement(tag);
const theme = config.theme;
const classNames = theme.heading;
if (classNames !== undefined) {
const className = classNames[tag];
addClassNamesToElement(element, className);
}
return element;
}
updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {
h1: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h2: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h3: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h4: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h5: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h6: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
p: (node: Node) => {
// domNode is a <p> since we matched it by nodeName
const paragraph = node as HTMLParagraphElement;
const firstChild = paragraph.firstChild;
if (firstChild !== null && isGoogleDocsTitle(firstChild)) {
return {
conversion: () => ({node: null}),
priority: 3,
};
}
return null;
},
span: (node: Node) => {
if (isGoogleDocsTitle(node)) {
return {
conversion: (domNode: Node) => {
return {
node: $createHeadingNode('h1'),
};
},
priority: 3,
};
}
return null;
},
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
const formatType = this.getFormatType();
element.style.textAlign = formatType;
}
return {
element,
};
}
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
const node = $createHeadingNode(serializedNode.tag);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
return node;
}
exportJSON(): SerializedHeadingNode {
return {
...super.exportJSON(),
tag: this.getTag(),
type: 'heading',
version: 1,
};
}
// Mutation
insertNewAfter(
selection?: RangeSelection,
restoreSelection = true,
): ParagraphNode | HeadingNode {
const anchorOffet = selection ? selection.anchor.offset : 0;
const lastDesc = this.getLastDescendant();
const isAtEnd =
!lastDesc ||
(selection &&
selection.anchor.key === lastDesc.getKey() &&
anchorOffet === lastDesc.getTextContentSize());
const newElement =
isAtEnd || !selection
? $createParagraphNode()
: $createHeadingNode(this.getTag());
const direction = this.getDirection();
newElement.setDirection(direction);
this.insertAfter(newElement, restoreSelection);
if (anchorOffet === 0 && !this.isEmpty() && selection) {
const paragraph = $createParagraphNode();
paragraph.select();
this.replace(paragraph, true);
}
return newElement;
}
collapseAtStart(): true {
const newElement = !this.isEmpty()
? $createHeadingNode(this.getTag())
: $createParagraphNode();
const children = this.getChildren();
children.forEach((child) => newElement.append(child));
this.replace(newElement);
return true;
}
extractWithChild(): boolean {
return true;
}
}
function isGoogleDocsTitle(domNode: Node): boolean {
if (domNode.nodeName.toLowerCase() === 'span') {
return (domNode as HTMLSpanElement).style.fontSize === '26pt';
}
return false;
}
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
const nodeName = element.nodeName.toLowerCase();
let node = null;
if (
nodeName === 'h1' ||
nodeName === 'h2' ||
nodeName === 'h3' ||
nodeName === 'h4' ||
nodeName === 'h5' ||
nodeName === 'h6'
) {
node = $createHeadingNode(nodeName);
if (element.style !== null) {
node.setFormat(element.style.textAlign as ElementFormatType);
}
}
return {node};
}
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
const node = $createQuoteNode();
if (element.style !== null) {
node.setFormat(element.style.textAlign as ElementFormatType);
}
return {node};
}
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
return $applyNodeReplacement(new HeadingNode(headingTag));
}
export function $isHeadingNode(
node: LexicalNode | null | undefined,
): node is HeadingNode {
return node instanceof HeadingNode;
}
function onPasteForRichText(
event: CommandPayloadType<typeof PASTE_COMMAND>,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(
() => {
const selection = $getSelection();
const clipboardData =
objectKlassEquals(event, InputEvent) ||
objectKlassEquals(event, KeyboardEvent)
? null
: (event as ClipboardEvent).clipboardData;
if (clipboardData != null && selection !== null) {
$insertDataTransferForRichText(clipboardData, selection, editor);
}
},
{
tag: 'paste',
},
);
}
async function onCutForRichText(
event: CommandPayloadType<typeof CUT_COMMAND>,
editor: LexicalEditor,
): Promise<void> {
await copyToClipboard(
editor,
objectKlassEquals(event, ClipboardEvent) ? (event as ClipboardEvent) : null,
);
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.removeText();
} else if ($isNodeSelection(selection)) {
selection.getNodes().forEach((node) => node.remove());
}
});
}
// Clipboard may contain files that we aren't allowed to read. While the event is arguably useless,
// in certain occasions, we want to know whether it was a file transfer, as opposed to text. We
// control this with the first boolean flag.
export function eventFiles(
event: DragEvent | PasteCommandType,
): [boolean, Array<File>, boolean] {
let dataTransfer: null | DataTransfer = null;
if (objectKlassEquals(event, DragEvent)) {
dataTransfer = (event as DragEvent).dataTransfer;
} else if (objectKlassEquals(event, ClipboardEvent)) {
dataTransfer = (event as ClipboardEvent).clipboardData;
}
if (dataTransfer === null) {
return [false, [], false];
}
const types = dataTransfer.types;
const hasFiles = types.includes('Files');
const hasContent =
types.includes('text/html') || types.includes('text/plain');
return [hasFiles, Array.from(dataTransfer.files), hasContent];
}
function $handleIndentAndOutdent(
indentOrOutdent: (block: ElementNode) => void,
): boolean {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
const alreadyHandled = new Set();
const nodes = selection.getNodes();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const key = node.getKey();
if (alreadyHandled.has(key)) {
continue;
}
const parentBlock = $findMatchingParent(
node,
(parentNode): parentNode is ElementNode =>
$isElementNode(parentNode) && !parentNode.isInline(),
);
if (parentBlock === null) {
continue;
}
const parentKey = parentBlock.getKey();
if (parentBlock.canIndent() && !alreadyHandled.has(parentKey)) {
alreadyHandled.add(parentKey);
indentOrOutdent(parentBlock);
}
}
return alreadyHandled.size > 0;
}
function $isTargetWithinDecorator(target: HTMLElement): boolean {
const node = $getNearestNodeFromDOMNode(target);
return $isDecoratorNode(node);
}
function $isSelectionAtEndOfRoot(selection: RangeSelection) {
const focus = selection.focus;
return focus.key === 'root' && focus.offset === $getRoot().getChildrenSize();
}
export function registerRichText(editor: LexicalEditor): () => void {
const removeListener = mergeRegister(
editor.registerCommand(
CLICK_COMMAND,
(payload) => {
const selection = $getSelection();
if ($isNodeSelection(selection)) {
selection.clear();
return true;
}
return false;
},
0,
),
editor.registerCommand<boolean>(
DELETE_CHARACTER_COMMAND,
(isBackward) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
selection.deleteCharacter(isBackward);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<boolean>(
DELETE_WORD_COMMAND,
(isBackward) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
selection.deleteWord(isBackward);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<boolean>(
DELETE_LINE_COMMAND,
(isBackward) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
selection.deleteLine(isBackward);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
CONTROLLED_TEXT_INSERTION_COMMAND,
(eventOrText) => {
const selection = $getSelection();
if (typeof eventOrText === 'string') {
if (selection !== null) {
selection.insertText(eventOrText);
}
} else {
if (selection === null) {
return false;
}
const dataTransfer = eventOrText.dataTransfer;
if (dataTransfer != null) {
$insertDataTransferForRichText(dataTransfer, selection, editor);
} else if ($isRangeSelection(selection)) {
const data = eventOrText.data;
if (data) {
selection.insertText(data);
}
return true;
}
}
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
REMOVE_TEXT_COMMAND,
() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
selection.removeText();
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<TextFormatType>(
FORMAT_TEXT_COMMAND,
(format) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
selection.formatText(format);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<ElementFormatType>(
FORMAT_ELEMENT_COMMAND,
(format) => {
const selection = $getSelection();
if (!$isRangeSelection(selection) && !$isNodeSelection(selection)) {
return false;
}
const nodes = selection.getNodes();
for (const node of nodes) {
const element = $findMatchingParent(
node,
(parentNode): parentNode is ElementNode =>
$isElementNode(parentNode) && !parentNode.isInline(),
);
if (element !== null) {
element.setFormat(format);
}
}
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<boolean>(
INSERT_LINE_BREAK_COMMAND,
(selectStart) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
selection.insertLineBreak(selectStart);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
INSERT_PARAGRAPH_COMMAND,
() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
selection.insertParagraph();
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
INSERT_TAB_COMMAND,
() => {
$insertNodes([$createTabNode()]);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
INDENT_CONTENT_COMMAND,
() => {
return $handleIndentAndOutdent((block) => {
const indent = block.getIndent();
block.setIndent(indent + 1);
});
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
OUTDENT_CONTENT_COMMAND,
() => {
return $handleIndentAndOutdent((block) => {
const indent = block.getIndent();
if (indent > 0) {
block.setIndent(indent - 1);
}
});
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>(
KEY_ARROW_UP_COMMAND,
(event) => {
const selection = $getSelection();
if (
$isNodeSelection(selection) &&
!$isTargetWithinDecorator(event.target as HTMLElement)
) {
// If selection is on a node, let's try and move selection
// back to being a range selection.
const nodes = selection.getNodes();
if (nodes.length > 0) {
nodes[0].selectPrevious();
return true;
}
} else if ($isRangeSelection(selection)) {
const possibleNode = $getAdjacentNode(selection.focus, true);
if (
!event.shiftKey &&
$isDecoratorNode(possibleNode) &&
!possibleNode.isIsolated() &&
!possibleNode.isInline()
) {
possibleNode.selectPrevious();
event.preventDefault();
return true;
}
}
return false;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>(
KEY_ARROW_DOWN_COMMAND,
(event) => {
const selection = $getSelection();
if ($isNodeSelection(selection)) {
// If selection is on a node, let's try and move selection
// back to being a range selection.
const nodes = selection.getNodes();
if (nodes.length > 0) {
nodes[0].selectNext(0, 0);
return true;
}
} else if ($isRangeSelection(selection)) {
if ($isSelectionAtEndOfRoot(selection)) {
event.preventDefault();
return true;
}
const possibleNode = $getAdjacentNode(selection.focus, false);
if (
!event.shiftKey &&
$isDecoratorNode(possibleNode) &&
!possibleNode.isIsolated() &&
!possibleNode.isInline()
) {
possibleNode.selectNext();
event.preventDefault();
return true;
}
}
return false;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>(
KEY_ARROW_LEFT_COMMAND,
(event) => {
const selection = $getSelection();
if ($isNodeSelection(selection)) {
// If selection is on a node, let's try and move selection
// back to being a range selection.
const nodes = selection.getNodes();
if (nodes.length > 0) {
event.preventDefault();
nodes[0].selectPrevious();
return true;
}
}
if (!$isRangeSelection(selection)) {
return false;
}
if ($shouldOverrideDefaultCharacterSelection(selection, true)) {
const isHoldingShift = event.shiftKey;
event.preventDefault();
$moveCharacter(selection, isHoldingShift, true);
return true;
}
return false;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>(
KEY_ARROW_RIGHT_COMMAND,
(event) => {
const selection = $getSelection();
if (
$isNodeSelection(selection) &&
!$isTargetWithinDecorator(event.target as HTMLElement)
) {
// If selection is on a node, let's try and move selection
// back to being a range selection.
const nodes = selection.getNodes();
if (nodes.length > 0) {
event.preventDefault();
nodes[0].selectNext(0, 0);
return true;
}
}
if (!$isRangeSelection(selection)) {
return false;
}
const isHoldingShift = event.shiftKey;
if ($shouldOverrideDefaultCharacterSelection(selection, false)) {
event.preventDefault();
$moveCharacter(selection, isHoldingShift, false);
return true;
}
return false;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>(
KEY_BACKSPACE_COMMAND,
(event) => {
if ($isTargetWithinDecorator(event.target as HTMLElement)) {
return false;
}
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
event.preventDefault();
const {anchor} = selection;
const anchorNode = anchor.getNode();
if (
selection.isCollapsed() &&
anchor.offset === 0 &&
!$isRootNode(anchorNode)
) {
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
if (element.getIndent() > 0) {
return editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
}
}
return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>(
KEY_DELETE_COMMAND,
(event) => {
if ($isTargetWithinDecorator(event.target as HTMLElement)) {
return false;
}
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
event.preventDefault();
return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, false);
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent | null>(
KEY_ENTER_COMMAND,
(event) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
if (event !== null) {
// If we have beforeinput, then we can avoid blocking
// the default behavior. This ensures that the iOS can
// intercept that we're actually inserting a paragraph,
// and autocomplete, autocapitalize etc work as intended.
// This can also cause a strange performance issue in
// Safari, where there is a noticeable pause due to
// preventing the key down of enter.
if (
(IS_IOS || IS_SAFARI || IS_APPLE_WEBKIT) &&
CAN_USE_BEFORE_INPUT
) {
return false;
}
event.preventDefault();
if (event.shiftKey) {
return editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);
}
}
return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
KEY_ESCAPE_COMMAND,
() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
editor.blur();
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<DragEvent>(
DROP_COMMAND,
(event) => {
const [, files] = eventFiles(event);
if (files.length > 0) {
const x = event.clientX;
const y = event.clientY;
const eventRange = caretFromPoint(x, y);
if (eventRange !== null) {
const {offset: domOffset, node: domNode} = eventRange;
const node = $getNearestNodeFromDOMNode(domNode);
if (node !== null) {
const selection = $createRangeSelection();
if ($isTextNode(node)) {
selection.anchor.set(node.getKey(), domOffset, 'text');
selection.focus.set(node.getKey(), domOffset, 'text');
} else {
const parentKey = node.getParentOrThrow().getKey();
const offset = node.getIndexWithinParent() + 1;
selection.anchor.set(parentKey, offset, 'element');
selection.focus.set(parentKey, offset, 'element');
}
const normalizedSelection =
$normalizeSelection__EXPERIMENTAL(selection);
$setSelection(normalizedSelection);
}
editor.dispatchCommand(DRAG_DROP_PASTE, files);
}
event.preventDefault();
return true;
}
const selection = $getSelection();
if ($isRangeSelection(selection)) {
return true;
}
return false;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<DragEvent>(
DRAGSTART_COMMAND,
(event) => {
const [isFileTransfer] = eventFiles(event);
const selection = $getSelection();
if (isFileTransfer && !$isRangeSelection(selection)) {
return false;
}
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<DragEvent>(
DRAGOVER_COMMAND,
(event) => {
const [isFileTransfer] = eventFiles(event);
const selection = $getSelection();
if (isFileTransfer && !$isRangeSelection(selection)) {
return false;
}
const x = event.clientX;
const y = event.clientY;
const eventRange = caretFromPoint(x, y);
if (eventRange !== null) {
const node = $getNearestNodeFromDOMNode(eventRange.node);
if ($isDecoratorNode(node)) {
// Show browser caret as the user is dragging the media across the screen. Won't work
// for DecoratorNode nor it's relevant.
event.preventDefault();
}
}
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
SELECT_ALL_COMMAND,
() => {
$selectAll();
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
COPY_COMMAND,
(event) => {
copyToClipboard(
editor,
objectKlassEquals(event, ClipboardEvent)
? (event as ClipboardEvent)
: null,
);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
CUT_COMMAND,
(event) => {
onCutForRichText(event, editor);
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
PASTE_COMMAND,
(event) => {
const [, files, hasTextContent] = eventFiles(event);
if (files.length > 0 && !hasTextContent) {
editor.dispatchCommand(DRAG_DROP_PASTE, files);
return true;
}
// if inputs then paste within the input ignore creating a new node on paste event
if (isSelectionCapturedInDecoratorInput(event.target as Node)) {
return false;
}
const selection = $getSelection();
if (selection !== null) {
onPasteForRichText(event, editor);
return true;
}
return false;
},
COMMAND_PRIORITY_EDITOR,
),
);
return removeListener;
}