UNPKG

@lobehub/editor

Version:

A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.

488 lines (478 loc) 22.5 kB
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } import { $createCodeNode, $isCodeNode } from '@lexical/code'; import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode } from '@lexical/list'; import { $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text'; import { $setBlocksType } from '@lexical/selection'; import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils'; import { debounce } from 'es-toolkit'; import { $createNodeSelection, $getSelection, $isParagraphNode, $isRangeSelection, $setSelection, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, UNDO_COMMAND } from 'lexical'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { noop } from "../../../editor-kernel"; import { INSERT_CODEINLINE_COMMAND } from "../../../plugins/code"; import { $isSelectionInCodeInline } from "../../../plugins/code/node/code"; import { UPDATE_CODEBLOCK_LANG } from "../../../plugins/codeblock"; import { $createCodeMirrorNode, $isCodeMirrorNode } from "../../../plugins/codemirror-block/node/CodeMirrorNode"; import { $isRootTextContentEmpty } from "../../../plugins/common/utils"; import { INSERT_LINK_HIGHLIGHT_COMMAND } from "../../../plugins/link-highlight/command"; import { $isLinkHighlightNode } from "../../../plugins/link-highlight/node/link-highlight"; import { $isLinkNode, TOGGLE_LINK_COMMAND, formatUrl } from "../../../plugins/link/node/LinkNode"; import { extractUrlFromText, sanitizeUrl, validateUrl } from "../../../plugins/link/utils"; import { INSERT_CHECK_LIST_COMMAND } from "../../../plugins/list"; import { $createMathBlockNode, $createMathInlineNode } from "../../../plugins/math/node"; import { $findTopLevelElement, formatParagraph, getSelectedNode } from "./utils"; /** * Editor state and toolbar methods return type */ /** * Provide toolbar state and toolbar methods * @param editor - Editor instance * @returns Editor state and methods for toolbar functionality */ export function useEditorState(editor) { var _editor$isEditable; var _useState = useState(false), _useState2 = _slicedToArray(_useState, 2), canUndo = _useState2[0], setCanUndo = _useState2[1]; var _useState3 = useState(false), _useState4 = _slicedToArray(_useState3, 2), canRedo = _useState4[0], setCanRedo = _useState4[1]; var _useState5 = useState((_editor$isEditable = editor === null || editor === void 0 ? void 0 : editor.isEditable()) !== null && _editor$isEditable !== void 0 ? _editor$isEditable : true), _useState6 = _slicedToArray(_useState5, 2), editable = _useState6[0], setEditable = _useState6[1]; var _useState7 = useState(false), _useState8 = _slicedToArray(_useState7, 2), isBold = _useState8[0], setIsBold = _useState8[1]; var _useState9 = useState(false), _useState10 = _slicedToArray(_useState9, 2), isItalic = _useState10[0], setIsItalic = _useState10[1]; var _useState11 = useState(false), _useState12 = _slicedToArray(_useState11, 2), isUnderline = _useState12[0], setIsUnderline = _useState12[1]; var _useState13 = useState(false), _useState14 = _slicedToArray(_useState13, 2), isStrikethrough = _useState14[0], setIsStrikethrough = _useState14[1]; var _useState15 = useState(false), _useState16 = _slicedToArray(_useState15, 2), isSubscript = _useState16[0], setIsSubscript = _useState16[1]; var _useState17 = useState(false), _useState18 = _slicedToArray(_useState17, 2), isSuperscript = _useState18[0], setIsSuperscript = _useState18[1]; var _useState19 = useState(false), _useState20 = _slicedToArray(_useState19, 2), isCode = _useState20[0], setIsCode = _useState20[1]; var _useState21 = useState(false), _useState22 = _slicedToArray(_useState21, 2), isLink = _useState22[0], setIsLink = _useState22[1]; var _useState23 = useState(false), _useState24 = _slicedToArray(_useState23, 2), isCodeblock = _useState24[0], setIsInCodeblok = _useState24[1]; var _useState25 = useState(false), _useState26 = _slicedToArray(_useState25, 2), isBlockquote = _useState26[0], setIsInBlockquote = _useState26[1]; var _useState27 = useState(null), _useState28 = _slicedToArray(_useState27, 2), codeblockLang = _useState28[0], setCodeblockLang = _useState28[1]; var _useState29 = useState(true), _useState30 = _slicedToArray(_useState29, 2), isEmpty = _useState30[0], setIsEmpty = _useState30[1]; var _useState31 = useState(false), _useState32 = _slicedToArray(_useState31, 2), isSelected = _useState32[0], setIsSelected = _useState32[1]; var _useState33 = useState(null), _useState34 = _slicedToArray(_useState33, 2), blockType = _useState34[0], setBlockType = _useState34[1]; var $handleHeadingNode = useCallback(function (selectedElement) { var type = $isHeadingNode(selectedElement) ? selectedElement.getTag() : selectedElement.getType(); setBlockType(type); }, [setBlockType]); var $updateToolbar = useCallback(function () { var _editor$isEditable2; var selection = $getSelection(); var lexicalEditor = editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor(); setIsSelected(false); setEditable((_editor$isEditable2 = editor === null || editor === void 0 ? void 0 : editor.isEditable()) !== null && _editor$isEditable2 !== void 0 ? _editor$isEditable2 : true); if (lexicalEditor) { setIsEmpty($isRootTextContentEmpty(lexicalEditor.isComposing(), false)); } if ($isRangeSelection(selection)) { var _editor$getLexicalEdi; setIsSelected(!!selection._cachedNodes); setIsBold(selection.hasFormat('bold')); setIsItalic(selection.hasFormat('italic')); setIsUnderline(selection.hasFormat('underline')); setIsStrikethrough(selection.hasFormat('strikethrough')); setIsSubscript(selection.hasFormat('subscript')); setIsSuperscript(selection.hasFormat('superscript')); setIsCode($isSelectionInCodeInline(lexicalEditor)); var anchorNode = selection.anchor.getNode(); var focusNode = selection.focus.getNode(); var element = $findTopLevelElement(anchorNode); var focusElement = $findTopLevelElement(focusNode); var elementKey = element.getKey(); var elementDOM = editor === null || editor === void 0 || (_editor$getLexicalEdi = editor.getLexicalEditor()) === null || _editor$getLexicalEdi === void 0 ? void 0 : _editor$getLexicalEdi.getElementByKey(elementKey); var node = getSelectedNode(selection); var parent = node.getParent(); // Check for both Link and LinkHighlight nodes setIsLink($isLinkNode(parent) || $isLinkNode(node) || $isLinkHighlightNode(parent) || $isLinkHighlightNode(node)); // Support both CodeNode (from codeblock plugin) and CodeMirrorNode (from codemirror-block plugin) var isLexicalCodeBlock = $isCodeNode(element) && $isCodeNode(focusElement) && elementKey === focusElement.getKey(); var isCodeMirrorBlock = $isCodeMirrorNode(element) && $isCodeMirrorNode(focusElement) && elementKey === focusElement.getKey(); var isCodeBlock = isLexicalCodeBlock || isCodeMirrorBlock; setIsInCodeblok(isCodeBlock); if (isLexicalCodeBlock) { setCodeblockLang(element.getLanguage()); } else if (isCodeMirrorBlock) { setCodeblockLang(element.lang); } else { setCodeblockLang(''); } var _isBlockquote = $isQuoteNode(element) && $isQuoteNode(focusElement) && elementKey === focusElement.getKey(); setIsInBlockquote(_isBlockquote); if (elementDOM !== null) { if ($isListNode(element)) { var parentList = $getNearestNodeOfType(anchorNode, ListNode); var type = parentList ? parentList.getListType() : element.getListType(); setBlockType(type); } else { $handleHeadingNode(element); } } } else if (!selection) { setIsSelected(false); setIsBold(false); setIsItalic(false); setIsUnderline(false); setIsStrikethrough(false); setIsSubscript(false); setIsSuperscript(false); setIsCode(false); setIsLink(false); setIsInCodeblok(false); setIsInBlockquote(false); setCodeblockLang(null); setBlockType(null); } }, [editor]); var undo = useCallback(function () { editor === null || editor === void 0 || editor.dispatchCommand(UNDO_COMMAND, undefined); }, [editor]); var redo = useCallback(function () { editor === null || editor === void 0 || editor.dispatchCommand(REDO_COMMAND, undefined); }, [editor]); var formatText = useCallback(function (type) { editor === null || editor === void 0 || editor.dispatchCommand(FORMAT_TEXT_COMMAND, type); }, [editor]); var bold = useCallback(function () { formatText('bold'); }, [formatText]); var underline = useCallback(function () { formatText('underline'); }, [formatText]); var strikethrough = useCallback(function () { formatText('strikethrough'); }, [formatText]); var italic = useCallback(function () { formatText('italic'); }, [formatText]); var subscript = useCallback(function () { formatText('subscript'); }, [formatText]); var superscript = useCallback(function () { formatText('superscript'); }, [formatText]); var code = useCallback(function () { editor === null || editor === void 0 || editor.dispatchCommand(INSERT_CODEINLINE_COMMAND, undefined); }, [formatText]); var bulletList = useCallback(function () { if (blockType !== 'bullet') { editor === null || editor === void 0 || editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); } else { formatParagraph(editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor()); } }, [blockType, editor]); var numberList = useCallback(function () { if (blockType !== 'number') { editor === null || editor === void 0 || editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); } else { formatParagraph(editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor()); } }, [blockType, editor]); var checkList = useCallback(function () { if (blockType !== 'check') { editor === null || editor === void 0 || editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined); } else { formatParagraph(editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor()); } }, [blockType, editor]); var codeblock = useCallback(function () { if (blockType !== 'code') { var _editor$getLexicalEdi2; editor === null || editor === void 0 || (_editor$getLexicalEdi2 = editor.getLexicalEditor()) === null || _editor$getLexicalEdi2 === void 0 || _editor$getLexicalEdi2.update(function () { var _lexicalEditor$_nodes; var selection = $getSelection(); if (!selection) { return; } // Try to use CodeMirrorNode if available, otherwise fall back to CodeNode var lexicalEditor = editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor(); var hasCodeMirrorNode = lexicalEditor ? lexicalEditor._nodes.has('code') && ((_lexicalEditor$_nodes = lexicalEditor._nodes.get('code')) === null || _lexicalEditor$_nodes === void 0 ? void 0 : _lexicalEditor$_nodes.klass.name) === 'CodeMirrorNode' : false; if (hasCodeMirrorNode) { // Use CodeMirrorNode if (!$isRangeSelection(selection) || selection.isCollapsed()) { var textContent = selection.getTextContent(); var codeMirrorNode = $createCodeMirrorNode('plain', textContent); var nodeSelection = $createNodeSelection(); nodeSelection.add(codeMirrorNode.getKey()); selection.insertNodes([codeMirrorNode]); $setSelection(nodeSelection); } else { var _textContent = selection.getTextContent(); var _codeMirrorNode = $createCodeMirrorNode('plain', _textContent); selection.insertNodes([_codeMirrorNode]); var _nodeSelection = $createNodeSelection(); _nodeSelection.add(_codeMirrorNode.getKey()); $setSelection(_nodeSelection); } } else { // Use original CodeNode if (!$isRangeSelection(selection) || selection.isCollapsed()) { $setBlocksType(selection, function () { return $createCodeNode(); }); } else { var _textContent2 = selection.getTextContent(); var codeNode = $createCodeNode(); selection.insertNodes([codeNode]); selection = $getSelection(); if ($isRangeSelection(selection)) { selection.insertRawText(_textContent2); } } } }); } else { formatParagraph(editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor()); } }, [blockType, editor]); var blockquote = useCallback(function () { if (blockType !== 'quote') { var _editor$getLexicalEdi3; editor === null || editor === void 0 || (_editor$getLexicalEdi3 = editor.getLexicalEditor()) === null || _editor$getLexicalEdi3 === void 0 || _editor$getLexicalEdi3.update(function () { var selection = $getSelection(); if ($isRangeSelection(selection)) { $setBlocksType(selection, function () { return $createQuoteNode(); }); } }); } else { formatParagraph(editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor()); } }, [blockType, editor]); var updateCodeblockLang = useCallback(function (lang) { if (!isCodeblock) { return; } editor === null || editor === void 0 || editor.dispatchCommand(UPDATE_CODEBLOCK_LANG, { lang: lang }); }, [editor, isCodeblock]); var insertLink = useCallback(function () { var lexical = editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor(); if (!lexical) return; // Detect which link type we're currently in var inLinkNode = false; var inLinkHighlightNode = false; lexical.getEditorState().read(function () { var selection = $getSelection(); if ($isRangeSelection(selection)) { var node = getSelectedNode(selection); var parent = node.getParent(); if ($isLinkNode(parent) || $isLinkNode(node)) { inLinkNode = true; } if ($isLinkHighlightNode(parent) || $isLinkHighlightNode(node)) { inLinkHighlightNode = true; } } }); // If inside LinkHighlightNode, toggle it if (inLinkHighlightNode) { lexical.dispatchCommand(INSERT_LINK_HIGHLIGHT_COMMAND, undefined); setIsLink(false); return; } // If inside LinkNode, toggle it with standard Link plugin if (inLinkNode) { setIsLink(false); lexical.dispatchCommand(TOGGLE_LINK_COMMAND, null); return; } // Not in any link - try to insert new link // Try LinkHighlight first, then fall back to standard Link if not handled if (!isLink) { // First try INSERT_LINK_HIGHLIGHT_COMMAND var linkHighlightHandled = lexical.dispatchCommand(INSERT_LINK_HIGHLIGHT_COMMAND, undefined); if (linkHighlightHandled) { setIsLink(true); return; } // Fall back to standard Link plugin var nextUrl = sanitizeUrl('https://'); var expandTo = null; lexical.getEditorState().read(function () { var selection = $getSelection(); if ($isRangeSelection(selection)) { var text = selection.getTextContent(); if (!selection.isCollapsed()) { var maybeUrl = formatUrl(text.trim()); if (validateUrl(maybeUrl)) { nextUrl = maybeUrl; } } else { var lineText = selection.anchor.getNode().getTextContent(); var found = extractUrlFromText(lineText); if (found && validateUrl(formatUrl(found.url))) { expandTo = { index: found.index, length: found.length }; } } } }); setIsLink(true); lexical.update(function () { if (expandTo) { var selection = $getSelection(); if ($isRangeSelection(selection)) { var anchorNode = selection.anchor.getNode(); // Move selection to the URL range within the current text node selection.anchor.set(anchorNode.getKey(), expandTo.index, 'text'); selection.focus.set(anchorNode.getKey(), expandTo.index + expandTo.length, 'text'); } } lexical.dispatchCommand(TOGGLE_LINK_COMMAND, validateUrl(nextUrl) ? nextUrl : sanitizeUrl('https://')); }); } }, [editor, isLink]); var insertMath = useCallback(function () { var _editor$getLexicalEdi4; editor === null || editor === void 0 || (_editor$getLexicalEdi4 = editor.getLexicalEditor()) === null || _editor$getLexicalEdi4 === void 0 || _editor$getLexicalEdi4.update(function () { var selection = $getSelection(); if ($isRangeSelection(selection)) { // 检查当前上下文来决定插入行内还是块级数学公式 var anchorNode = selection.anchor.getNode(); var element = $findTopLevelElement(anchorNode); // 判断是否应该插入块级数学公式的条件(默认插入 inline): // 1. 在空段落开头(没有任何内容的段落) // 2. 选择了整行内容 var shouldInsertBlock = $isParagraphNode(element) && selection.isCollapsed() && selection.anchor.offset === 0 && element.getTextContentSize() === 0 || !selection.isCollapsed() && selection.anchor.offset === 0 && selection.focus.offset === element.getTextContentSize(); var mathNode = shouldInsertBlock ? $createMathBlockNode('') : $createMathInlineNode(''); selection.insertNodes([mathNode]); // 选择新创建的数学节点 var nodeSelection = $createNodeSelection(); nodeSelection.add(mathNode.getKey()); $setSelection(nodeSelection); } }); }, [editor]); useEffect(function () { if (!editor) return; var lexicalEditor = editor.getLexicalEditor(); var cleanup = noop; var debounceUpdate = debounce(function () { lexicalEditor === null || lexicalEditor === void 0 || lexicalEditor.read(function () { $updateToolbar(); }); }, 500); var handleLexicalEditor = function handleLexicalEditor(lexicalEditor) { cleanup = mergeRegister(lexicalEditor.registerUpdateListener(debounce(function (_ref) { var editorState = _ref.editorState; editorState.read(function () { $updateToolbar(); }); }, 500)), lexicalEditor.registerCommand(SELECTION_CHANGE_COMMAND, function () { if (lexicalEditor.isComposing()) { return false; } debounceUpdate(); return false; }, COMMAND_PRIORITY_LOW), lexicalEditor.registerCommand(CAN_UNDO_COMMAND, function (payload) { setCanUndo(payload); return false; }, COMMAND_PRIORITY_LOW), lexicalEditor.registerCommand(CAN_REDO_COMMAND, function (payload) { setCanRedo(payload); return false; }, COMMAND_PRIORITY_LOW)); return cleanup; }; if (!lexicalEditor) { editor.on('initialized', handleLexicalEditor); return function () { cleanup(); editor.off('initialized', handleLexicalEditor); }; } return handleLexicalEditor(lexicalEditor); }, [editor, $updateToolbar]); return useMemo(function () { return { blockType: blockType, blockquote: blockquote, bold: bold, bulletList: bulletList, canRedo: canRedo, canUndo: canUndo, checkList: checkList, code: code, codeblock: codeblock, codeblockLang: codeblockLang, editable: editable, insertLink: insertLink, insertMath: insertMath, isBlockquote: isBlockquote, isBold: isBold, isCode: isCode, isCodeblock: isCodeblock, isEmpty: isEmpty, isItalic: isItalic, isSelected: isSelected, isStrikethrough: isStrikethrough, isSubscript: isSubscript, isSuperscript: isSuperscript, isUnderline: isUnderline, italic: italic, numberList: numberList, redo: redo, strikethrough: strikethrough, subscript: subscript, superscript: superscript, underline: underline, undo: undo, updateCodeblockLang: updateCodeblockLang }; }, [blockType, canRedo, canUndo, codeblockLang, isBold, isCode, isEmpty, isBlockquote, isCodeblock, isItalic, isSelected, isStrikethrough, isUnderline, isSubscript, isSuperscript, italic]); }