@lobehub/editor
Version:
A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.
310 lines (291 loc) • 12.4 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 { mergeRegister } from '@lexical/utils';
import { $getNodeByKey, $getSelection, $isNodeSelection, $isRangeSelection } from 'lexical';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useLexicalComposerContext, useLexicalEditor } from "../../../../editor-kernel/react";
import PortalAnchor from "../../../../editor-kernel/react/PortalAnchor";
import { compareNodeOrder } from "../../../../editor-kernel/utils";
import { SELECT_MATH_SIDE_COMMAND, UPDATE_MATH_COMMAND } from "../../command";
import { $isMathNode, MathBlockNode } from "../../node";
import MathEditorContainer from "./MathEditorContainer";
import MathEditorContent from "./MathEditorContent";
import { jsx as _jsx } from "react/jsx-runtime";
import { Fragment as _Fragment } from "react/jsx-runtime";
var MathEdit = /*#__PURE__*/memo(function (_ref) {
var renderComp = _ref.renderComp;
var textareaRef = useRef(null);
var isUpdatingRef = useRef(false);
var _useState = useState(null),
_useState2 = _slicedToArray(_useState, 2),
mathNode = _useState2[0],
setMathNode = _useState2[1];
var _useState3 = useState(''),
_useState4 = _slicedToArray(_useState3, 2),
value = _useState4[0],
setValue = _useState4[1];
// 最近一次成功渲染(校验通过)的 LaTeX
var lastValidRef = useRef('');
var isInputValidRef = useRef(true);
var _useState5 = useState(null),
_useState6 = _slicedToArray(_useState5, 2),
mathDOM = _useState6[0],
setMathDOM = _useState6[1];
var _useState7 = useState(false),
_useState8 = _slicedToArray(_useState7, 2),
prev = _useState8[0],
setPrev = _useState8[1];
var _useState9 = useState(false),
_useState10 = _slicedToArray(_useState9, 2),
isBlockMode = _useState10[0],
setIsBlockMode = _useState10[1];
var _useState11 = useState(false),
_useState12 = _slicedToArray(_useState11, 2),
isOpen = _useState12[0],
setIsOpen = _useState12[1];
var _useLexicalComposerCo = useLexicalComposerContext(),
_useLexicalComposerCo2 = _slicedToArray(_useLexicalComposerCo, 1),
editor = _useLexicalComposerCo2[0];
// 实时更新节点内容(仅当输入可渲染时才同步到 document)
useEffect(function () {
if (!mathNode) return;
// 使用防抖来避免过于频繁的更新
var timeoutId = setTimeout(function () {
// 直接更新节点内容
var lexicalEditor = editor.getLexicalEditor();
if (lexicalEditor && !isUpdatingRef.current) {
if (!isInputValidRef.current) {
var currentNode = lexicalEditor.getEditorState().read(function () {
return lexicalEditor.getElementByKey(mathNode.getKey());
});
if (currentNode) {
lexicalEditor.update(function () {
var writableNode = mathNode.getWritable();
writableNode.__code = value;
});
}
return;
}
// 检查当前值是否与节点中的值不同,避免不必要的更新
var currentCode = mathNode.code;
if (value && currentCode && currentCode === value) {
return; // 值相同,无需更新
}
isUpdatingRef.current = true;
lexicalEditor.update(function () {
var currentNode = lexicalEditor.getEditorState().read(function () {
return lexicalEditor.getElementByKey(mathNode.getKey());
});
if (currentNode) {
var writableNode = mathNode.getWritable();
writableNode.__code = value;
}
});
// 延迟重置更新标志,确保更新监听器不会立即触发
setTimeout(function () {
isUpdatingRef.current = false;
}, 50);
}
}, 50); // 增加防抖延迟
return function () {
return clearTimeout(timeoutId);
};
}, [value, mathNode, editor]);
// 提交逻辑
var handleSubmit = useCallback(function () {
if (!mathNode) return;
var lexicalEditor = editor.getLexicalEditor();
if (lexicalEditor) {
// 提交时若当前不可渲染,则使用最近一次成功渲染的内容
var codeToCommit = isInputValidRef.current ? value : lastValidRef.current;
lexicalEditor.dispatchCommand(UPDATE_MATH_COMMAND, {
code: codeToCommit,
key: mathNode.getKey()
});
}
}, [editor, mathNode, value]);
// 取消编辑,不保存更改
var handleCancel = useCallback(function () {
if (!mathNode) return;
var lexicalEditor = editor.getLexicalEditor();
if (lexicalEditor) {
// 使用命令来正确设置光标位置到数学节点之后
lexicalEditor.dispatchCommand(SELECT_MATH_SIDE_COMMAND, {
key: mathNode.getKey(),
prev: false
});
// 将焦点返回到编辑器
lexicalEditor.focus();
}
}, [mathNode, editor]);
// 删除节点
var handleDelete = useCallback(function () {
if (!mathNode) return;
var lexicalEditor = editor.getLexicalEditor();
if (lexicalEditor) {
lexicalEditor.update(function () {
mathNode.remove();
});
}
}, [mathNode, editor]);
// 左箭头导航
var handleArrowLeft = useCallback(function () {
if (!mathNode) return;
editor.dispatchCommand(SELECT_MATH_SIDE_COMMAND, {
key: mathNode.getKey(),
prev: true
});
}, [mathNode, editor]);
// 右箭头导航
var handleArrowRight = useCallback(function () {
if (!mathNode) return;
editor.dispatchCommand(SELECT_MATH_SIDE_COMMAND, {
key: mathNode.getKey(),
prev: false
});
}, [mathNode, editor]);
// 处理焦点
var handleFocus = useCallback(function () {
var _textareaRef$current;
(_textareaRef$current = textareaRef.current) === null || _textareaRef$current === void 0 || _textareaRef$current.focus();
if (prev) {
var _textareaRef$current2;
(_textareaRef$current2 = textareaRef.current) === null || _textareaRef$current2 === void 0 || (_textareaRef$current2 = _textareaRef$current2.resizableTextArea) === null || _textareaRef$current2 === void 0 || (_textareaRef$current2 = _textareaRef$current2.textArea) === null || _textareaRef$current2 === void 0 || _textareaRef$current2.setSelectionRange(0, 0);
}
}, [prev]);
// 设置 textarea ref
var setTextareaRef = useCallback(function (ref) {
textareaRef.current = ref;
}, []);
useLexicalEditor(function (editor) {
return mergeRegister(editor.registerUpdateListener(function (_ref2) {
var prevEditorState = _ref2.prevEditorState;
// 如果正在更新中,跳过此次监听器调用
if (isUpdatingRef.current) {
return;
}
// Handle editor state updates
var canEdit = editor.getEditorState().read(function () {
var selection = $getSelection();
if (!$isNodeSelection(selection)) {
return false;
}
var node = selection.getNodes()[0];
if (!$isMathNode(node)) {
// Handle math node
return false;
}
// 检查是否是同一个节点,避免不必要的状态更新
var isSameNode = mathNode && mathNode.getKey() === node.getKey();
setMathNode(node);
// 只有在不是同一个节点或值真正改变时才更新 value
if (!isSameNode && node.code !== value) {
setValue(node.code);
lastValidRef.current = node.code; // 切换节点时以节点里的内容作为最近一次有效值
isInputValidRef.current = true; // 重置有效状态
}
setMathDOM(editor.getElementByKey(node.getKey()));
setIsBlockMode(node instanceof MathBlockNode);
return node;
});
if (canEdit) {
var anchorNodeKey = prevEditorState.read(function () {
var sel = prevEditorState._selection;
if (!$isRangeSelection(sel)) {
return false;
}
var node = sel.anchor.key;
return node;
});
editor.getEditorState().read(function () {
var anchorNode = anchorNodeKey ? $getNodeByKey(anchorNodeKey) : null;
var isPrev = anchorNode && compareNodeOrder(anchorNode, canEdit) < 0;
if (isPrev) {
setPrev(true);
} else {
setPrev(false);
}
});
setIsOpen(true);
}
if (!canEdit) {
setMathNode(null);
setMathDOM(null);
setValue('');
lastValidRef.current = '';
isInputValidRef.current = true;
setIsBlockMode(false);
setIsOpen(false);
}
}));
}, [mathNode, value]);
// 构建 MathEditorContent 组件
var mathEditorContent = /*#__PURE__*/_jsx(MathEditorContent, {
focusRef: setTextareaRef,
mathNode: mathNode,
onArrowLeft: handleArrowLeft,
onArrowRight: handleArrowRight,
onCancel: handleCancel,
onDelete: handleDelete,
onSubmit: handleSubmit,
onValidationChange: function onValidationChange(isValid) {
isInputValidRef.current = isValid;
if (isValid) {
lastValidRef.current = value;
}
},
onValueChange: setValue
// prev={prev}
,
value: value
});
// 点击编辑器外部时关闭面板
useEffect(function () {
if (!isOpen) return;
var handlePointerDown = function handlePointerDown(event) {
var target = event.target;
if (!target) return;
// 支持多个容器(例如使用 renderComp 渲染到外部节点时,会有额外容器)
var containers = Array.from(document.querySelectorAll('[data-math-editor-container]'));
if (containers.some(function (el) {
return el && el.contains(target);
})) return;
handleCancel();
};
document.addEventListener('mousedown', handlePointerDown);
document.addEventListener('touchstart', handlePointerDown);
return function () {
document.removeEventListener('mousedown', handlePointerDown);
document.removeEventListener('touchstart', handlePointerDown);
};
}, [isOpen, handleCancel]);
// 如果有自定义渲染组件,使用它来包装 MathEditorContent
if (renderComp) {
// 为自定义渲染的内容增加容器标记,便于外部点击检测
var wrapped = /*#__PURE__*/_jsx("div", {
"data-math-editor-container": true,
children: mathEditorContent
});
return /*#__PURE__*/_jsx(_Fragment, {
children: renderComp({
children: wrapped,
open: isOpen
})
});
}
// 否则使用默认的 MathEditorContainer
return /*#__PURE__*/_jsx(PortalAnchor, {
children: /*#__PURE__*/_jsx(MathEditorContainer, {
isBlockMode: isBlockMode,
mathDOM: mathDOM,
onFocus: handleFocus,
prev: prev,
children: mathEditorContent
})
});
});
export default MathEdit;