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.

262 lines (255 loc) 10.2 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 { mergeRegister } from '@lexical/utils'; import { Block, Button, Flexbox, Hotkey, Icon, Input, Text } from '@lobehub/ui'; import { cssVar } from 'antd-style'; import { COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_NORMAL, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, createCommand } from 'lexical'; import { BaselineIcon, LinkIcon } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useLexicalEditor } from "../../../../editor-kernel/react"; import { useEditable } from "../../../../editor-kernel/react/useEditable"; import { useTranslation } from "../../../../editor-kernel/react/useTranslation"; import { cleanPosition, updatePosition } from "../../../../utils/updatePosition"; import { UPDATE_LINK_TEXT_COMMAND } from "../../command"; import { styles } from "../style"; import { jsx as _jsx } from "react/jsx-runtime"; import { jsxs as _jsxs } from "react/jsx-runtime"; export var EDIT_LINK_COMMAND = createCommand(); var LinkEdit = function LinkEdit(_ref) { var editor = _ref.editor; var divRef = useRef(null); var linkNodeRef = useRef(null); var linkInputRef = useRef(null); var linkTextInputRef = useRef(null); var _useState = useState(''), _useState2 = _slicedToArray(_useState, 2), linkUrl = _useState2[0], setLinkUrl = _useState2[1]; var _useState3 = useState(''), _useState4 = _slicedToArray(_useState3, 2), linkText = _useState4[0], setLinkText = _useState4[1]; var _useState5 = useState(null), _useState6 = _slicedToArray(_useState5, 2), linkDom = _useState6[0], setLinkDom = _useState6[1]; var _useEditable = useEditable(), editable = _useEditable.editable; var t = useTranslation(); // 取消编辑,不保存更改 var handleCancel = useCallback(function () { if (!editor) return; editor.focus(); cleanPosition(divRef.current); linkNodeRef.current = null; setLinkUrl(''); setLinkText(''); setLinkDom(null); }, [editor]); // 提取提交逻辑到独立函数 var handleSubmit = useCallback(function () { if (!linkNodeRef.current || !linkInputRef.current || !linkTextInputRef.current || !editor) return; var linkNode = linkNodeRef.current; var input = linkInputRef.current; var inputDOM = input.input; var textInput = linkTextInputRef.current; var textInputDOM = textInput.input; // 更新链接URL var currentURL = editor.getEditorState().read(function () { return linkNode.getURL(); }); if (currentURL !== inputDOM.value) { editor.update(function () { linkNode.setURL(inputDOM.value); }); } // 更新链接文本 var currentText = editor.getEditorState().read(function () { return linkNode.getTextContent(); }); if (currentText !== textInputDOM.value) { editor.dispatchCommand(UPDATE_LINK_TEXT_COMMAND, { key: linkNode.getKey(), text: textInputDOM.value }); } // 关闭编辑器并聚焦到编辑器 editor.focus(); // 隐藏编辑面板 handleCancel(); }, [editor, linkNodeRef, linkInputRef, linkTextInputRef, handleCancel]); var handleKeyDown = useCallback(function (event) { if (!linkNodeRef.current || !linkInputRef.current || !linkTextInputRef.current || !editor) { return; } var linkNode = linkNodeRef.current; var input = linkInputRef.current; var inputDOM = input.input; var textInput = linkTextInputRef.current; var textInputDOM = textInput.input; switch (event.key) { case 'Enter': { event.preventDefault(); if (event.currentTarget === textInputDOM) { var currentText = editor.getEditorState().read(function () { return linkNode.getTextContent(); }); if (currentText !== textInputDOM.value) { editor.dispatchCommand(UPDATE_LINK_TEXT_COMMAND, { key: linkNode.getKey(), text: textInputDOM.value }); // 更新文本后跳转到链接输入框 inputDOM.focus(); } else { // 如果文本没有变化,直接跳转到链接输入框 inputDOM.focus(); } } else if (event.currentTarget === inputDOM) { // 在链接输入框按回车时提交所有更改 handleSubmit(); } return; } case 'Tab': { event.preventDefault(); if (event.currentTarget === textInputDOM) { inputDOM.focus(); } else { editor.focus(); } return; } case 'Escape': { event.preventDefault(); handleCancel(); return; } // No default } }, [linkNodeRef, linkInputRef, handleSubmit, handleCancel]); useEffect(function () { if (linkDom) { updatePosition({ floating: divRef.current, reference: linkDom }); } else { cleanPosition(divRef.current); } }, [linkDom]); // 点击编辑器外部时关闭面板 useEffect(function () { var handlePointerDown = function handlePointerDown(event) { if (!divRef.current) return; var target = event.target; if (!target) return; // 点击面板内部忽略 if (divRef.current.contains(target)) return; // 面板打开时(存在 linkDom)才触发关闭 if (linkDom) handleCancel(); }; document.addEventListener('mousedown', handlePointerDown); document.addEventListener('touchstart', handlePointerDown); return function () { document.removeEventListener('mousedown', handlePointerDown); document.removeEventListener('touchstart', handlePointerDown); }; }, [linkDom]); useLexicalEditor(function (editor) { return mergeRegister(editor.registerCommand(EDIT_LINK_COMMAND, function (payload) { if (!payload.linkNode || !payload.linkNodeDOM) { handleCancel(); return false; } linkNodeRef.current = payload.linkNode; setLinkUrl(payload.linkNode.getURL()); setLinkText(payload.linkNode.getTextContent()); setLinkDom(payload.linkNodeDOM); return true; }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(KEY_ESCAPE_COMMAND, function () { handleCancel(); return true; }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(KEY_TAB_COMMAND, function (payload) { if (linkNodeRef.current && linkTextInputRef.current) { payload.stopImmediatePropagation(); payload.preventDefault(); linkTextInputRef.current.focus(); return true; } return false; }, COMMAND_PRIORITY_NORMAL)); }, [editor]); if (!linkNodeRef.current || !editable) return null; return /*#__PURE__*/_jsxs(Block, { className: styles.linkEdit, ref: divRef, shadow: true, variant: 'outlined', children: [/*#__PURE__*/_jsxs(Flexbox, { gap: 8, padding: 12, children: [/*#__PURE__*/_jsx(Text, { weight: 500, children: t('link.editTextTitle') }), /*#__PURE__*/_jsx(Input, { onChange: function onChange(e) { // Handle link text change setLinkText(e.target.value); }, onKeyDown: handleKeyDown, prefix: /*#__PURE__*/_jsx(Icon, { color: cssVar.colorTextDescription, icon: BaselineIcon }), ref: linkTextInputRef, value: linkText }), /*#__PURE__*/_jsx(Text, { weight: 500, children: t('link.editLinkTitle') }), /*#__PURE__*/_jsx(Input, { onChange: function onChange(e) { // Handle link URL change setLinkUrl(e.target.value); }, onKeyDown: handleKeyDown, placeholder: "https://enter-link-url", prefix: /*#__PURE__*/_jsx(Icon, { color: cssVar.colorTextDescription, icon: LinkIcon }), ref: linkInputRef, value: linkUrl })] }), /*#__PURE__*/_jsx(Flexbox, { className: styles.linkEditFooter, horizontal: true, justify: 'flex-end', padding: 4, width: '100%', children: /*#__PURE__*/_jsxs(Button, { onClick: function onClick(e) { e.preventDefault(); handleSubmit(); }, size: 'small', type: 'text', variant: 'filled', children: [t('confirm'), /*#__PURE__*/_jsx(Hotkey, { compact: true, keys: "enter", variant: 'borderless' })] }) })] }); }; LinkEdit.displayName = 'LinkEdit'; export default LinkEdit;