@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
JavaScript
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]);
}