From 4a995d69a56b6b8b0f59c60636f84107a9c0ec04 Mon Sep 17 00:00:00 2001 From: shixiaohua Date: Mon, 26 Feb 2024 09:24:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:editorConfig=E4=BD=BF=E7=94=A8=E5=B1=80?= =?UTF-8?q?=E9=83=A8=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ItemTree/index.jsx | 4 +- src/pages/Note/Hlexical/hook/userModal.jsx | 47 + src/pages/Note/Hlexical/index.jsx | 62 +- .../nodes/ImageNode/ImageComponent/index.less | 0 .../Note/Hlexical/nodes/ImageNode/index.jsx | 206 ++ .../Note/Hlexical/nodes/ImageNode/index.less | 43 + .../nodes/TableNode/TableComponent/index.jsx | 1717 +++++++++++++++++ .../Note/Hlexical/nodes/TableNode/index.jsx | 383 ++++ src/pages/Note/Hlexical/nodes/UsefulNodes.jsx | 22 + .../Hlexical/plugins/ImagesPlugin/index.jsx | 356 ++++ .../Note/Hlexical/plugins/Input/Dialog.jsx | 16 + .../Note/Hlexical/plugins/Input/DropDown.jsx | 222 +++ .../Note/Hlexical/plugins/Input/FileInput.jsx | 23 + .../Note/Hlexical/plugins/Input/TextInput.jsx | 26 + .../Note/Hlexical/plugins/Input/index.less | 40 + .../InsertLayoutDialog/index.jsx | 45 + .../InsertLayoutDialog/index.less | 0 .../Note/Hlexical/plugins/SaveFilePlugin.js | 12 +- .../Hlexical/plugins/TablePlugin/index.jsx | 183 ++ .../Note/Hlexical/plugins/ToolbarPlugin.js | 1366 +++++++------ .../themes/{ExampleTheme.js => FirstTheme.js} | 4 +- 21 files changed, 4099 insertions(+), 678 deletions(-) create mode 100644 src/pages/Note/Hlexical/hook/userModal.jsx create mode 100644 src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.less create mode 100644 src/pages/Note/Hlexical/nodes/ImageNode/index.jsx create mode 100644 src/pages/Note/Hlexical/nodes/ImageNode/index.less create mode 100644 src/pages/Note/Hlexical/nodes/TableNode/TableComponent/index.jsx create mode 100644 src/pages/Note/Hlexical/nodes/TableNode/index.jsx create mode 100644 src/pages/Note/Hlexical/nodes/UsefulNodes.jsx create mode 100644 src/pages/Note/Hlexical/plugins/ImagesPlugin/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/Dialog.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/DropDown.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/FileInput.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/TextInput.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/index.less create mode 100644 src/pages/Note/Hlexical/plugins/InsertLayoutPlug/InsertLayoutDialog/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/InsertLayoutPlug/InsertLayoutDialog/index.less create mode 100644 src/pages/Note/Hlexical/plugins/TablePlugin/index.jsx rename src/pages/Note/Hlexical/themes/{ExampleTheme.js => FirstTheme.js} (97%) diff --git a/src/components/ItemTree/index.jsx b/src/components/ItemTree/index.jsx index 7e783ac..8541fb3 100644 --- a/src/components/ItemTree/index.jsx +++ b/src/components/ItemTree/index.jsx @@ -301,7 +301,7 @@ const ItemTree = (prop) => { if (dirFlag){ menuItem.push(getMenuItem('1',)) menuItem.push(getMenuItem('2',"添加文件",[ - getMenuItem("2-1",), + getMenuItem("2-1",), getMenuItem("2-2",)] ),null, 'group' ) @@ -318,7 +318,7 @@ const ItemTree = (prop) => { console.log('onClick',e) } } - // onMouseLeave={e => {setState("");}} + onMouseLeave={e => {setState("");}} items={menuItem}> , document.body diff --git a/src/pages/Note/Hlexical/hook/userModal.jsx b/src/pages/Note/Hlexical/hook/userModal.jsx new file mode 100644 index 0000000..f818355 --- /dev/null +++ b/src/pages/Note/Hlexical/hook/userModal.jsx @@ -0,0 +1,47 @@ +import {useCallback, useMemo, useState} from 'react'; +import * as React from 'react'; +import {Modal} from "antd"; + +export default function useModal(){ + const [modalContent, setModalContent] = useState(null); + const [isOpen,setIsOpen]=useState(true) + const onClose = useCallback(() => { + setModalContent(null); + setIsOpen(false) + }, []); + + const modal = useMemo(() => { + if (modalContent === null) { + return null; + } + const {title, content, closeOnClickOutside} = modalContent; + return (
+ + + {content} +
+ ); + }, [modalContent, onClose,isOpen]); + + const showModal = useCallback(( + title, + getContent, + closeOnClickOutside = false, + ) => { + setModalContent({ + closeOnClickOutside, + content: getContent(onClose), + title, + }); + }, + [onClose], + ); + + return [modal, showModal]; +} diff --git a/src/pages/Note/Hlexical/index.jsx b/src/pages/Note/Hlexical/index.jsx index 4b24602..9eda4a1 100644 --- a/src/pages/Note/Hlexical/index.jsx +++ b/src/pages/Note/Hlexical/index.jsx @@ -1,24 +1,16 @@ -import ExampleTheme from "./themes/ExampleTheme"; +import FirstTheme from "./themes/FirstTheme"; import {LexicalComposer} from "@lexical/react/LexicalComposer"; import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin"; import {ContentEditable} from "@lexical/react/LexicalContentEditable"; import {HistoryPlugin} from "@lexical/react/LexicalHistoryPlugin"; import {AutoFocusPlugin} from "@lexical/react/LexicalAutoFocusPlugin"; import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; -import TreeViewPlugin from "./plugins/TreeViewPlugin"; import ToolbarPlugin from "./plugins/ToolbarPlugin"; -import {HeadingNode, QuoteNode} from "@lexical/rich-text"; -import {TableCellNode, TableNode, TableRowNode} from "@lexical/table"; -import {ListItemNode, ListNode} from "@lexical/list"; -import {CodeHighlightNode, CodeNode, $createCodeNode, $isCodeNode} from "@lexical/code"; -import {AutoLinkNode, LinkNode} from "@lexical/link"; import {MarkdownShortcutPlugin} from "@lexical/react/LexicalMarkdownShortcutPlugin"; import { TRANSFORMERS, $convertFromMarkdownString, } from "@lexical/markdown"; import "./index.less" -import {useEffect, useState} from 'react'; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {importFile} from "../../../utils/File" import {isEmpty} from "../../../utils/ObjectUtils"; @@ -30,40 +22,23 @@ import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin"; import ImportFilePlugin from "./plugins/ImportFilePlugin"; import {TablePlugin} from "@lexical/react/LexicalTablePlugin"; import SaveFilePlugin from "./plugins/SaveFilePlugin"; - - +import {TabIndentationPlugin} from "@lexical/react/LexicalTabIndentationPlugin"; +import UsefulNodes from "./nodes/UsefulNodes"; +import ImagesPlugin from "./plugins/ImagesPlugin"; function Placeholder() { return
Enter some rich text...
; } -let editorConfig = { - // The editor theme - theme: ExampleTheme, - // Handling of errors during update - onError(error) { - throw error; - }, - // Any custom nodes go here - nodes: [ - HeadingNode, - ListNode, - ListItemNode, - QuoteNode, - CodeNode, - CodeHighlightNode, - TableNode, - TableCellNode, - TableRowNode, - AutoLinkNode, - LinkNode - ] -}; -// 从字符串化 JSON 设置编辑器状态 -// const editorState = editor.parseEditorState(editorStateJSONString); -// editor.setEditorState(editorState); export default function Hlexical(props) { + let editorConfig = { + theme: FirstTheme, + onError(error) { + throw error; + }, + nodes: UsefulNodes + }; console.log("Hlexical(props):this.props.filePath:", props.filePath) - if (props.filePath.endsWith(".md")){ + if (!isEmpty(props.filePath)&&props.filePath.endsWith(".md")){ importFile(props.filePath).then(value => { if (isEmpty(value)) { return @@ -94,8 +69,21 @@ export default function Hlexical(props) { + {/*markdown 快捷键*/} + {/*图片加载*/} + + {/*目录加载*/} + {/* 表格加载 */} + + {/**/} + {/**/} + {/* {(tableOfContentsArray) => {*/} + {/* return ;*/} + {/* }}*/} + {/**/} + {/*文件操作导入文件*/} diff --git a/src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.less b/src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.less new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Note/Hlexical/nodes/ImageNode/index.jsx b/src/pages/Note/Hlexical/nodes/ImageNode/index.jsx new file mode 100644 index 0000000..fce57a4 --- /dev/null +++ b/src/pages/Note/Hlexical/nodes/ImageNode/index.jsx @@ -0,0 +1,206 @@ +import {$applyNodeReplacement, createEditor, DecoratorNode} from 'lexical'; +import * as React from 'react'; +import {Suspense} from 'react'; + +// const ImageComponent = React.lazy( +// () => import('../ImageComponent'), +// ); + +function convertImageElement(domNode) { + if (domNode instanceof HTMLImageElement) { + const {alt: altText, src, width, height} = domNode; + const node = $createImageNode({altText, height, src, width}); + return {node}; + } + return null; +} + + +export class ImageNode extends DecoratorNode { + __src; + __altText; + __width; + __height; + __maxWidth; + __showCaption; + __caption; + __captionsEnabled; + + static getType() { + return 'image'; + } + + static clone(node) { + return new ImageNode( + node.__src, + node.__altText, + node.__maxWidth, + node.__width, + node.__height, + node.__showCaption, + node.__caption, + node.__captionsEnabled, + node.__key, + ); + } + + static importJSON(serializedNode) { + const {altText, height, width, maxWidth, caption, src, showCaption} = + serializedNode; + const node = $createImageNode({ + altText, + height, + maxWidth, + showCaption, + src, + width, + }); + const nestedEditor = node.__caption; + const editorState = nestedEditor.parseEditorState(caption.editorState); + if (!editorState.isEmpty()) { + nestedEditor.setEditorState(editorState); + } + return node; + } + + exportDOM() { + const element = document.createElement('img'); + element.setAttribute('src', this.__src); + element.setAttribute('alt', this.__altText); + element.setAttribute('width', this.__width.toString()); + element.setAttribute('height', this.__height.toString()); + return {element}; + } + + static importDOM() { + return { + img: (node) => ({ + conversion: convertImageElement, + priority: 0, + }), + }; + } + + constructor( + src, + altText, + maxWidth, + width, + height, + showCaption, + caption, + captionsEnabled, + key, + ) { + super(key); + this.__src = src; + this.__altText = altText; + this.__maxWidth = maxWidth; + this.__width = width || 'inherit'; + this.__height = height || 'inherit'; + this.__showCaption = showCaption || false; + this.__caption = caption || createEditor(); + this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined; + } + + exportJSON() { + return { + altText: this.getAltText(), + caption: this.__caption.toJSON(), + height: this.__height === 'inherit' ? 0 : this.__height, + maxWidth: this.__maxWidth, + showCaption: this.__showCaption, + src: this.getSrc(), + type: 'image', + version: 1, + width: this.__width === 'inherit' ? 0 : this.__width, + }; + } + + setWidthAndHeight( + width, + height, + ) { + const writable = this.getWritable(); + writable.__width = width; + writable.__height = height; + } + + setShowCaption(showCaption) { + const writable = this.getWritable(); + writable.__showCaption = showCaption; + } + + createDOM(config) { + const span = document.createElement('span'); + const theme = config.theme; + const className = theme.image; + if (className !== undefined) { + span.className = className; + } + return span; + } + + updateDOM() { + return false; + } + + getSrc() { + return this.__src; + } + + getAltText() { + return this.__altText; + } + + // decorate() { + // return ( + // + // + // + // ); + // } +} + +export function $createImageNode({ + altText, + height, + maxWidth = 500, + captionsEnabled, + src, + width, + showCaption, + caption, + key, + }) { + return $applyNodeReplacement( + new ImageNode( + src, + altText, + maxWidth, + width, + height, + showCaption, + caption, + captionsEnabled, + key, + ), + ); +} + +export function $isImageNode( + node, +) { + return node instanceof ImageNode; +} diff --git a/src/pages/Note/Hlexical/nodes/ImageNode/index.less b/src/pages/Note/Hlexical/nodes/ImageNode/index.less new file mode 100644 index 0000000..a9c1901 --- /dev/null +++ b/src/pages/Note/Hlexical/nodes/ImageNode/index.less @@ -0,0 +1,43 @@ +/** + * 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. + * + * + */ + +.ImageNode__contentEditable { + min-height: 20px; + border: 0px; + resize: none; + cursor: text; + caret-color: rgb(5, 5, 5); + display: block; + position: relative; + outline: 0px; + padding: 10px; + user-select: text; + font-size: 12px; + width: calc(100% - 20px); + white-space: pre-wrap; + word-break: break-word; +} + +.ImageNode__placeholder { + font-size: 12px; + color: #888; + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 10px; + left: 10px; + user-select: none; + white-space: nowrap; + display: inline-block; + pointer-events: none; +} + +.image-control-wrapper--resizing { + touch-action: none; +} diff --git a/src/pages/Note/Hlexical/nodes/TableNode/TableComponent/index.jsx b/src/pages/Note/Hlexical/nodes/TableNode/TableComponent/index.jsx new file mode 100644 index 0000000..6e7375d --- /dev/null +++ b/src/pages/Note/Hlexical/nodes/TableNode/TableComponent/index.jsx @@ -0,0 +1,1717 @@ +import { + $generateJSONFromSelectedNodes, + $generateNodesFromSerializedNodes, + $insertGeneratedNodes, +} from '@lexical/clipboard'; +import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html'; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer'; +import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection'; +import {mergeRegister} from '@lexical/utils'; +import { + $addUpdateTag, + $createParagraphNode, + $createRangeSelection, + $getNodeByKey, + $getRoot, + $getSelection, + $isNodeSelection, + $isRangeSelection, + CLICK_COMMAND, + COMMAND_PRIORITY_LOW, + COPY_COMMAND, + createEditor, + CUT_COMMAND, + EditorThemeClasses, + FORMAT_TEXT_COMMAND, + 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, + KEY_TAB_COMMAND, + LexicalEditor, + NodeKey, + PASTE_COMMAND, +} from 'lexical'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import * as React from 'react'; +import {createPortal} from 'react-dom'; +// import {IS_APPLE} from 'shared/environment'; + +import {CellContext} from '../../../plugins/TablePlugin'; +import { + $isTableNode, + cellHTMLCache, + cellTextContentCache, + createRow, + createUID, + exportTableCellsToHTML, + extractRowsFromHTML, + TableNode, +} from '../index'; +const NO_CELLS= []; +let IS_APPLE=false +function $createSelectAll(){ + const sel = $createRangeSelection(); + sel.focus.set('root', $getRoot().getChildrenSize(), 'element'); + return sel; +} + +function createEmptyParagraphHTML(theme) { + return `


`; +} + +function focusCell(tableElem, id) { + const cellElem = tableElem.querySelector(`[data-id=${id}]`); + if (cellElem == null) { + return; + } + cellElem.focus(); +} + +function isStartingResize(target) { + return target.nodeType === 1 && target.hasAttribute('data-table-resize'); +} + +function generateHTMLFromJSON( + editorStateJSON, + cellEditor, +) { + const editorState = cellEditor.parseEditorState(editorStateJSON); + let html = cellHTMLCache.get(editorStateJSON); + if (html === undefined) { + html = editorState.read(() => $generateHtmlFromNodes(cellEditor, null)); + const textContent = editorState.read(() => $getRoot().getTextContent()); + cellHTMLCache.set(editorStateJSON, html); + cellTextContentCache.set(editorStateJSON, textContent); + } + return html; +} + +function getCurrentDocument(editor) { + const rootElement = editor.getRootElement(); + return rootElement !== null ? rootElement.ownerDocument : document; +} + +function isCopy( + keyCode, + shiftKey, + metaKey, + ctrlKey, +) { + if (shiftKey) { + return false; + } + if (keyCode === 67) { + return IS_APPLE ? metaKey : ctrlKey; + } + + return false; +} + +function isCut( + keyCode, + shiftKey, + metaKey, + ctrlKey, +) { + if (shiftKey) { + return false; + } + if (keyCode === 88) { + return IS_APPLE ? metaKey : ctrlKey; + } + + return false; +} + +function isPaste( + keyCode, + shiftKey, + metaKey, + ctrlKey, +) { + if (shiftKey) { + return false; + } + if (keyCode === 86) { + return IS_APPLE ? metaKey : ctrlKey; + } + + return false; +} + +function getCellID(domElement) { + let node = domElement; + while (node !== null) { + const possibleID = node.getAttribute('data-id'); + if (possibleID != null) { + return possibleID; + } + node = node.parentElement; + } + return null; +} + +function getTableCellWidth(domElement) { + let node = domElement; + while (node !== null) { + if (node.nodeName === 'TH' || node.nodeName === 'TD') { + return node.getBoundingClientRect().width; + } + node = node.parentElement; + } + return 0; +} + +function $updateCells( + rows, + ids, + cellCoordMap, + cellEditor, + updateTableNode, + fn, +) { + for (const id of ids) { + const cell = getCell(rows, id, cellCoordMap); + if (cell !== null && cellEditor !== null) { + const editorState = cellEditor.parseEditorState(cell.json); + cellEditor._headless = true; + cellEditor.setEditorState(editorState); + cellEditor.update(fn, {discrete: true}); + cellEditor._headless = false; + const newJSON = JSON.stringify(cellEditor.getEditorState()); + updateTableNode((tableNode) => { + const [x, y] = cellCoordMap.get(id); + $addUpdateTag('history-push'); + tableNode.updateCellJSON(x, y, newJSON); + }); + } + } +} + +function isTargetOnPossibleUIControl(target) { + let node= target; + while (node !== null) { + const nodeName = node.nodeName; + if ( + nodeName === 'BUTTON' || + nodeName === 'INPUT' || + nodeName === 'TEXTAREA' + ) { + return true; + } + node = node.parentElement; + } + return false; +} + +function getSelectedRect( + startID, + endID, + cellCoordMap, +){ + const startCoords = cellCoordMap.get(startID); + const endCoords = cellCoordMap.get(endID); + if (startCoords === undefined || endCoords === undefined) { + return null; + } + const startX = Math.min(startCoords[0], endCoords[0]); + const endX = Math.max(startCoords[0], endCoords[0]); + const startY = Math.min(startCoords[1], endCoords[1]); + const endY = Math.max(startCoords[1], endCoords[1]); + + return { + endX, + endY, + startX, + startY, + }; +} + +function getSelectedIDs( + rows, + startID, + endID, + cellCoordMap, +){ + const rect = getSelectedRect(startID, endID, cellCoordMap); + if (rect === null) { + return []; + } + const {startX, endY, endX, startY} = rect; + const ids = []; + + for (let x = startX; x <= endX; x++) { + for (let y = startY; y <= endY; y++) { + ids.push(rows[y].cells[x].id); + } + } + return ids; +} + +function extractCellsFromRows( + rows, + rect, +) { + const {startX, endY, endX, startY} = rect; + const newRows = []; + + for (let y = startY; y <= endY; y++) { + const row = rows[y]; + const newRow = createRow(); + for (let x = startX; x <= endX; x++) { + const cellClone = {...row.cells[x]}; + cellClone.id = createUID(); + newRow.cells.push(cellClone); + } + newRows.push(newRow); + } + return newRows; +} + +function TableCellEditor({cellEditor}) { + const {cellEditorConfig, cellEditorPlugins} = useContext(CellContext); + + if (cellEditorPlugins === null || cellEditorConfig === null) { + return null; + } + + return ( + + {cellEditorPlugins} + + ); +} + +function getCell( + rows, + cellID, + cellCoordMap, +){ + const coords = cellCoordMap.get(cellID); + if (coords === undefined) { + return null; + } + const [x, y] = coords; + const row = rows[y]; + return row.cells[x]; +} + +function TableActionMenu({ + cell, + rows, + cellCoordMap, + menuElem, + updateCellsByID, + onClose, + updateTableNode, + setSortingOptions, + sortingOptions, + }) { + const dropDownRef = useRef(null); + + useEffect(() => { + const dropdownElem = dropDownRef.current; + if (dropdownElem !== null) { + const rect = menuElem.getBoundingClientRect(); + dropdownElem.style.top = `${rect.y}px`; + dropdownElem.style.left = `${rect.x}px`; + } + }, [menuElem]); + + useEffect(() => { + const handleClickOutside = (event) => { + const dropdownElem = dropDownRef.current; + if ( + dropdownElem !== null && + !dropdownElem.contains(event.target) + ) { + event.stopPropagation(); + } + }; + + window.addEventListener('click', handleClickOutside); + return () => window.removeEventListener('click', handleClickOutside); + }, [onClose]); + const coords = cellCoordMap.get(cell.id); + + if (coords === undefined) { + return null; + } + const [x, y] = coords; + + return ( +
{ + e.stopPropagation(); + }} + onPointerDown={(e) => { + e.stopPropagation(); + }} + onPointerUp={(e) => { + e.stopPropagation(); + }} + onClick={(e) => { + e.stopPropagation(); + }}> + + +
+ {cell.type === 'header' && y === 0 && ( + <> + {sortingOptions !== null && sortingOptions.x === x && ( + + )} + {(sortingOptions === null || + sortingOptions.x !== x || + sortingOptions.type === 'descending') && ( + + )} + {(sortingOptions === null || + sortingOptions.x !== x || + sortingOptions.type === 'ascending') && ( + + )} +
+ + )} + + +
+ + +
+ {rows[0].cells.length !== 1 && ( + + )} + {rows.length !== 1 && ( + + )} + +
+ ); +} + +function TableCell({ + cell, + cellCoordMap, + cellEditor, + isEditing, + isSelected, + isPrimarySelected, + theme, + updateCellsByID, + updateTableNode, + rows, + setSortingOptions, + sortingOptions, + }) { + const [showMenu, setShowMenu] = useState(false); + const menuRootRef = useRef(null); + const isHeader = cell.type !== 'normal'; + const editorStateJSON = cell.json; + const CellComponent = isHeader ? 'th' : 'td'; + const cellWidth = cell.width; + const menuElem = menuRootRef.current; + const coords = cellCoordMap.get(cell.id); + const isSorted = + sortingOptions !== null && + coords !== undefined && + coords[0] === sortingOptions.x && + coords[1] === 0; + + useEffect(() => { + if (isEditing || !isPrimarySelected) { + setShowMenu(false); + } + }, [isEditing, isPrimarySelected]); + + return ( + + {isPrimarySelected && ( +
+ )} + {isPrimarySelected && isEditing ? ( + + ) : ( + <> +
+
+ + )} + {isPrimarySelected && !isEditing && ( +
+ +
+ )} + {showMenu && + menuElem !== null && + createPortal( + setShowMenu(false)} + updateTableNode={updateTableNode} + cellCoordMap={cellCoordMap} + rows={rows} + setSortingOptions={setSortingOptions} + sortingOptions={sortingOptions} + />, + document.body, + )} + {isSorted &&
} + + ); +} + +export default function TableComponent({ + nodeKey, + rows: rawRows, + theme, + }) { + const [isSelected, setSelected, clearSelection] = + useLexicalNodeSelection(nodeKey); + const resizeMeasureRef = useRef({ + point: 0, + size: 0, + }); + const [sortingOptions, setSortingOptions] = useState( + null, + ); + const addRowsRef = useRef(null); + const lastCellIDRef = useRef(null); + const tableResizerRulerRef = useRef(null); + const {cellEditorConfig} = useContext(CellContext); + const [isEditing, setIsEditing] = useState(false); + const [showAddColumns, setShowAddColumns] = useState(false); + const [showAddRows, setShowAddRows] = useState(false); + const [editor] = useLexicalComposerContext(); + const mouseDownRef = useRef(false); + const [resizingID, setResizingID] = useState(null); + const tableRef = useRef(null); + const cellCoordMap = useMemo(() => { + const map = new Map(); + + for (let y = 0; y < rawRows.length; y++) { + const row = rawRows[y]; + const cells = row.cells; + for (let x = 0; x < cells.length; x++) { + const cell = cells[x]; + map.set(cell.id, [x, y]); + } + } + return map; + }, [rawRows]); + const rows = useMemo(() => { + if (sortingOptions === null) { + return rawRows; + } + const _rows = rawRows.slice(1); + _rows.sort((a, b) => { + const aCells = a.cells; + const bCells = b.cells; + const x = sortingOptions.x; + const aContent = cellTextContentCache.get(aCells[x].json) || ''; + const bContent = cellTextContentCache.get(bCells[x].json) || ''; + if (aContent === '' || bContent === '') { + return 1; + } + if (sortingOptions.type === 'ascending') { + return aContent.localeCompare(bContent); + } + return bContent.localeCompare(aContent); + }); + _rows.unshift(rawRows[0]); + return _rows; + }, [rawRows, sortingOptions]); + const [primarySelectedCellID, setPrimarySelectedCellID] = useState< + null | string + >(null); + const cellEditor = useMemo(() => { + if (cellEditorConfig === null) { + return null; + } + const _cellEditor = createEditor({ + namespace: cellEditorConfig.namespace, + nodes: cellEditorConfig.nodes, + onError: (error) => cellEditorConfig.onError(error, _cellEditor), + theme: cellEditorConfig.theme, + }); + return _cellEditor; + }, [cellEditorConfig]); + const [selectedCellIDs, setSelectedCellIDs] = useState([]); + const selectedCellSet = useMemo( + () => new Set(selectedCellIDs), + [selectedCellIDs], + ); + + useEffect(() => { + const tableElem = tableRef.current; + if ( + isSelected && + document.activeElement === document.body && + tableElem !== null + ) { + tableElem.focus(); + } + }, [isSelected]); + + const updateTableNode = useCallback( + (fn) => { + editor.update(() => { + const tableNode = $getNodeByKey(nodeKey); + if ($isTableNode(tableNode)) { + fn(tableNode); + } + }); + }, + [editor, nodeKey], + ); + + const addColumns = () => { + updateTableNode((tableNode) => { + $addUpdateTag('history-push'); + tableNode.addColumns(1); + }); + }; + + const addRows = () => { + updateTableNode((tableNode) => { + $addUpdateTag('history-push'); + tableNode.addRows(1); + }); + }; + + const modifySelectedCells = useCallback( + (x, y, extend) => { + const id = rows[y].cells[x].id; + lastCellIDRef.current = id; + if (extend) { + const selectedIDs = getSelectedIDs( + rows, + primarySelectedCellID, + id, + cellCoordMap, + ); + setSelectedCellIDs(selectedIDs); + } else { + setPrimarySelectedCellID(id); + setSelectedCellIDs(NO_CELLS); + focusCell(tableRef.current, id); + } + }, + [cellCoordMap, primarySelectedCellID, rows], + ); + + const saveEditorToJSON = useCallback(() => { + if (cellEditor !== null && primarySelectedCellID !== null) { + const json = JSON.stringify(cellEditor.getEditorState()); + updateTableNode((tableNode) => { + const coords = cellCoordMap.get(primarySelectedCellID); + if (coords === undefined) { + return; + } + $addUpdateTag('history-push'); + const [x, y] = coords; + tableNode.updateCellJSON(x, y, json); + }); + } + }, [cellCoordMap, cellEditor, primarySelectedCellID, updateTableNode]); + + const selectTable = useCallback(() => { + setTimeout(() => { + const parentRootElement = editor.getRootElement(); + if (parentRootElement !== null) { + parentRootElement.focus({preventScroll: true}); + window.getSelection()?.removeAllRanges(); + } + }, 20); + }, [editor]); + + useEffect(() => { + const tableElem = tableRef.current; + if (tableElem === null) { + return; + } + const doc = getCurrentDocument(editor); + + const isAtEdgeOfTable = (event) => { + const x = event.clientX - tableRect.x; + const y = event.clientY - tableRect.y; + return x < 5 || y < 5; + }; + + const handlePointerDown = (event) => { + const possibleID = getCellID(event.target); + if ( + possibleID !== null && + editor.isEditable() && + tableElem.contains(event.target) + ) { + if (isAtEdgeOfTable(event)) { + setSelected(true); + setPrimarySelectedCellID(null); + selectTable(); + return; + } + setSelected(false); + if (isStartingResize(event.target)) { + setResizingID(possibleID); + tableElem.style.userSelect = 'none'; + resizeMeasureRef.current = { + point: event.clientX, + size: getTableCellWidth(event.target), + }; + return; + } + mouseDownRef.current = true; + if (primarySelectedCellID !== possibleID) { + if (isEditing) { + saveEditorToJSON(); + } + setPrimarySelectedCellID(possibleID); + setIsEditing(false); + lastCellIDRef.current = possibleID; + } else { + lastCellIDRef.current = null; + } + setSelectedCellIDs(NO_CELLS); + } else if ( + primarySelectedCellID !== null && + !isTargetOnPossibleUIControl(event.target) + ) { + setSelected(false); + mouseDownRef.current = false; + if (isEditing) { + saveEditorToJSON(); + } + setPrimarySelectedCellID(null); + setSelectedCellIDs(NO_CELLS); + setIsEditing(false); + lastCellIDRef.current = null; + } + }; + + const tableRect = tableElem.getBoundingClientRect(); + + const handlePointerMove = (event) => { + if (resizingID !== null) { + const tableResizerRulerElem = tableResizerRulerRef.current; + if (tableResizerRulerElem !== null) { + const {size, point} = resizeMeasureRef.current; + const diff = event.clientX - point; + const newWidth = size + diff; + let x = event.clientX - tableRect.x; + if (x < 10) { + x = 10; + } else if (x > tableRect.width - 10) { + x = tableRect.width - 10; + } else if (newWidth < 20) { + x = point - size + 20 - tableRect.x; + } + tableResizerRulerElem.style.left = `${x}px`; + } + return; + } + if (!isEditing) { + const {clientX, clientY} = event; + const {width, x, y, height} = tableRect; + const isOnRightEdge = + clientX > x + width * 0.9 && + clientX < x + width + 40 && + !mouseDownRef.current; + setShowAddColumns(isOnRightEdge); + const isOnBottomEdge = + event.target === addRowsRef.current || + (clientY > y + height * 0.85 && + clientY < y + height + 5 && + !mouseDownRef.current); + setShowAddRows(isOnBottomEdge); + } + if ( + isEditing || + !mouseDownRef.current || + primarySelectedCellID === null + ) { + return; + } + const possibleID = getCellID(event.target); + if (possibleID !== null && possibleID !== lastCellIDRef.current) { + if (selectedCellIDs.length === 0) { + tableElem.style.userSelect = 'none'; + } + const selectedIDs = getSelectedIDs( + rows, + primarySelectedCellID, + possibleID, + cellCoordMap, + ); + if (selectedIDs.length === 1) { + setSelectedCellIDs(NO_CELLS); + } else { + setSelectedCellIDs(selectedIDs); + } + lastCellIDRef.current = possibleID; + } + }; + + const handlePointerUp = (event) => { + if (resizingID !== null) { + const {size, point} = resizeMeasureRef.current; + const diff = event.clientX - point; + let newWidth = size + diff; + if (newWidth < 10) { + newWidth = 10; + } + updateTableNode((tableNode) => { + const [x] = cellCoordMap.get(resizingID); + $addUpdateTag('history-push'); + tableNode.updateColumnWidth(x, newWidth); + }); + setResizingID(null); + } + if ( + tableElem !== null && + selectedCellIDs.length > 1 && + mouseDownRef.current + ) { + tableElem.style.userSelect = 'text'; + window.getSelection()?.removeAllRanges(); + } + mouseDownRef.current = false; + }; + + doc.addEventListener('pointerdown', handlePointerDown); + doc.addEventListener('pointermove', handlePointerMove); + doc.addEventListener('pointerup', handlePointerUp); + + return () => { + doc.removeEventListener('pointerdown', handlePointerDown); + doc.removeEventListener('pointermove', handlePointerMove); + doc.removeEventListener('pointerup', handlePointerUp); + }; + }, [ + cellEditor, + editor, + isEditing, + rows, + saveEditorToJSON, + primarySelectedCellID, + selectedCellSet, + selectedCellIDs, + cellCoordMap, + resizingID, + updateTableNode, + setSelected, + selectTable, + ]); + + useEffect(() => { + if (!isEditing && primarySelectedCellID !== null) { + const doc = getCurrentDocument(editor); + + const loadContentIntoCell = (cell) => { + if (cell !== null && cellEditor !== null) { + const editorStateJSON = cell.json; + const editorState = cellEditor.parseEditorState(editorStateJSON); + cellEditor.setEditorState(editorState); + } + }; + + const handleDblClick = (event) => { + const possibleID = getCellID(event.target); + if (possibleID === primarySelectedCellID && editor.isEditable()) { + const cell = getCell(rows, possibleID, cellCoordMap); + loadContentIntoCell(cell); + setIsEditing(true); + setSelectedCellIDs(NO_CELLS); + } + }; + + const handleKeyDown = (event) => { + // Ignore arrow keys, escape or tab + const keyCode = event.keyCode; + if ( + keyCode === 16 || + keyCode === 27 || + keyCode === 9 || + keyCode === 37 || + keyCode === 38 || + keyCode === 39 || + keyCode === 40 || + keyCode === 8 || + keyCode === 46 || + !editor.isEditable() + ) { + return; + } + if (keyCode === 13) { + event.preventDefault(); + } + if ( + !isEditing && + primarySelectedCellID !== null && + editor.getEditorState().read(() => $getSelection() === null) && + (event.target).contentEditable !== 'true' + ) { + if (isCopy(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) { + editor.dispatchCommand(COPY_COMMAND, event); + return; + } + if (isCut(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) { + editor.dispatchCommand(CUT_COMMAND, event); + return; + } + if (isPaste(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) { + editor.dispatchCommand(PASTE_COMMAND, event); + return; + } + } + if (event.metaKey || event.ctrlKey || event.altKey) { + return; + } + const cell = getCell(rows, primarySelectedCellID, cellCoordMap); + loadContentIntoCell(cell); + setIsEditing(true); + setSelectedCellIDs(NO_CELLS); + }; + + doc.addEventListener('dblclick', handleDblClick); + doc.addEventListener('keydown', handleKeyDown); + + return () => { + doc.removeEventListener('dblclick', handleDblClick); + doc.removeEventListener('keydown', handleKeyDown); + }; + } + }, [ + cellEditor, + editor, + isEditing, + rows, + primarySelectedCellID, + cellCoordMap, + ]); + + const updateCellsByID = useCallback( + (ids, fn) => { + $updateCells(rows, ids, cellCoordMap, cellEditor, updateTableNode, fn); + }, + [cellCoordMap, cellEditor, rows, updateTableNode], + ); + + const clearCellsCommand = useCallback(() => { + if (primarySelectedCellID !== null && !isEditing) { + updateCellsByID([primarySelectedCellID, ...selectedCellIDs], () => { + const root = $getRoot(); + root.clear(); + root.append($createParagraphNode()); + }); + return true; + } else if (isSelected) { + updateTableNode((tableNode) => { + $addUpdateTag('history-push'); + tableNode.selectNext(); + tableNode.remove(); + }); + } + return false; + }, [ + isEditing, + isSelected, + primarySelectedCellID, + selectedCellIDs, + updateCellsByID, + updateTableNode, + ]); + + useEffect(() => { + const tableElem = tableRef.current; + if (tableElem === null) { + return; + } + + const copyDataToClipboard = ( + event, + htmlString, + lexicalString, + plainTextString, + ) => { + const clipboardData = + event instanceof KeyboardEvent ? null : event.clipboardData; + event.preventDefault(); + + if (clipboardData != null) { + clipboardData.setData('text/html', htmlString); + clipboardData.setData('text/plain', plainTextString); + clipboardData.setData('application/x-lexical-editor', lexicalString); + } else { + const clipboard = navigator.clipboard; + if (clipboard != null) { + // Most browsers only support a single item in the clipboard at one time. + // So we optimize by only putting in HTML. + const data = [ + new ClipboardItem({ + 'text/html': new Blob([htmlString], { + type: 'text/html', + }), + }), + ]; + clipboard.write(data); + } + } + }; + + const getTypeFromObject = async ( + clipboardData, + type, + ) => { + try { + return clipboardData instanceof DataTransfer + ? clipboardData.getData(type) + : clipboardData instanceof ClipboardItem + ? await (await clipboardData.getType(type)).text() + : ''; + } catch { + return ''; + } + }; + + const pasteContent = async (event) => { + let clipboardData = + (event instanceof InputEvent ? null : event.clipboardData) || null; + + if (primarySelectedCellID !== null && cellEditor !== null) { + event.preventDefault(); + + if (clipboardData === null) { + try { + const items = await navigator.clipboard.read(); + clipboardData = items[0]; + } catch { + // NO-OP + } + } + const lexicalString = + clipboardData !== null + ? await getTypeFromObject( + clipboardData, + 'application/x-lexical-editor', + ) + : ''; + + if (lexicalString) { + try { + const payload = JSON.parse(lexicalString); + if ( + payload.namespace === editor._config.namespace && + Array.isArray(payload.nodes) + ) { + $updateCells( + rows, + [primarySelectedCellID], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const root = $getRoot(); + root.clear(); + root.append($createParagraphNode()); + root.selectEnd(); + const nodes = $generateNodesFromSerializedNodes( + payload.nodes, + ); + const sel = $getSelection(); + if ($isRangeSelection(sel)) { + $insertGeneratedNodes(cellEditor, nodes, sel); + } + }, + ); + return; + } + // eslint-disable-next-line no-empty + } catch {} + } + const htmlString = + clipboardData !== null + ? await getTypeFromObject(clipboardData, 'text/html') + : ''; + + if (htmlString) { + try { + const parser = new DOMParser(); + const dom = parser.parseFromString(htmlString, 'text/html'); + const possibleTableElement = dom.querySelector('table'); + + if (possibleTableElement != null) { + const pasteRows = extractRowsFromHTML(possibleTableElement); + updateTableNode((tableNode) => { + const [x, y] = cellCoordMap.get(primarySelectedCellID); + $addUpdateTag('history-push'); + tableNode.mergeRows(x, y, pasteRows); + }); + return; + } + $updateCells( + rows, + [primarySelectedCellID], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const root = $getRoot(); + root.clear(); + root.append($createParagraphNode()); + root.selectEnd(); + const nodes = $generateNodesFromDOM(editor, dom); + const sel = $getSelection(); + if ($isRangeSelection(sel)) { + $insertGeneratedNodes(cellEditor, nodes, sel); + } + }, + ); + return; + // eslint-disable-next-line no-empty + } catch {} + } + + // Multi-line plain text in rich text mode pasted as separate paragraphs + // instead of single paragraph with linebreaks. + const text = + clipboardData !== null + ? await getTypeFromObject(clipboardData, 'text/plain') + : ''; + + if (text != null) { + $updateCells( + rows, + [primarySelectedCellID], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const root = $getRoot(); + root.clear(); + root.selectEnd(); + const sel = $getSelection(); + if (sel !== null) { + sel.insertRawText(text); + } + }, + ); + } + } + }; + + const copyPrimaryCell = (event) => { + if (primarySelectedCellID !== null && cellEditor !== null) { + const cell = getCell(rows, primarySelectedCellID, cellCoordMap); + const json = cell.json; + const htmlString = cellHTMLCache.get(json) || null; + if (htmlString === null) { + return; + } + const editorState = cellEditor.parseEditorState(json); + const plainTextString = editorState.read(() => + $getRoot().getTextContent(), + ); + const lexicalString = editorState.read(() => { + return JSON.stringify( + $generateJSONFromSelectedNodes(cellEditor, null), + ); + }); + + copyDataToClipboard(event, htmlString, lexicalString, plainTextString); + } + }; + + const copyCellRange = (event) => { + const lastCellID = lastCellIDRef.current; + if ( + primarySelectedCellID !== null && + cellEditor !== null && + lastCellID !== null + ) { + const rect = getSelectedRect( + primarySelectedCellID, + lastCellID, + cellCoordMap, + ); + if (rect === null) { + return; + } + const dom = exportTableCellsToHTML(rows, rect); + const htmlString = dom.outerHTML; + const plainTextString = dom.outerText; + const tableNodeJSON = editor.getEditorState().read(() => { + const tableNode = $getNodeByKey(nodeKey); + return tableNode.exportJSON(); + }); + tableNodeJSON.rows = extractCellsFromRows(rows, rect); + const lexicalJSON = { + namespace: cellEditor._config.namespace, + nodes: [tableNodeJSON], + }; + const lexicalString = JSON.stringify(lexicalJSON); + copyDataToClipboard(event, htmlString, lexicalString, plainTextString); + } + }; + + const handlePaste = ( + event, + activeEditor, + ) => { + const selection = $getSelection(); + if ( + primarySelectedCellID !== null && + !isEditing && + selection === null && + activeEditor === editor + ) { + pasteContent(event); + mouseDownRef.current = false; + setSelectedCellIDs(NO_CELLS); + return true; + } + return false; + }; + + const handleCopy = (event, activeEditor) => { + const selection = $getSelection(); + if ( + primarySelectedCellID !== null && + !isEditing && + selection === null && + activeEditor === editor + ) { + if (selectedCellIDs.length === 0) { + copyPrimaryCell(event); + } else { + copyCellRange(event); + } + return true; + } + return false; + }; + + return mergeRegister( + editor.registerCommand( + CLICK_COMMAND, + (payload) => { + const selection = $getSelection(); + if ($isNodeSelection(selection)) { + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + PASTE_COMMAND, + handlePaste, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + COPY_COMMAND, + handleCopy, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + CUT_COMMAND, + (event, activeEditor) => { + if (handleCopy(event, activeEditor)) { + clearCellsCommand(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_BACKSPACE_COMMAND, + clearCellsCommand, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_DELETE_COMMAND, + clearCellsCommand, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + FORMAT_TEXT_COMMAND, + (payload) => { + if (primarySelectedCellID !== null && !isEditing) { + $updateCells( + rows, + [primarySelectedCellID, ...selectedCellIDs], + cellCoordMap, + cellEditor, + updateTableNode, + () => { + const sel = $createSelectAll(); + sel.formatText(payload); + }, + ); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ENTER_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if ( + primarySelectedCellID === null && + !isEditing && + $isNodeSelection(selection) && + selection.has(nodeKey) && + selection.getNodes().length === 1 && + targetEditor === editor + ) { + const firstCellID = rows[0].cells[0].id; + setPrimarySelectedCellID(firstCellID); + focusCell(tableElem, firstCellID); + event.preventDefault(); + event.stopPropagation(); + clearSelection(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_TAB_COMMAND, + (event) => { + const selection = $getSelection(); + if ( + !isEditing && + selection === null && + primarySelectedCellID !== null + ) { + const isBackward = event.shiftKey; + const [x, y] = cellCoordMap.get(primarySelectedCellID); + event.preventDefault(); + let nextX = null; + let nextY = null; + if (x === 0 && isBackward) { + if (y !== 0) { + nextY = y - 1; + nextX = rows[nextY].cells.length - 1; + } + } else if (x === rows[y].cells.length - 1 && !isBackward) { + if (y !== rows.length - 1) { + nextY = y + 1; + nextX = 0; + } + } else if (!isBackward) { + nextX = x + 1; + nextY = y; + } else { + nextX = x - 1; + nextY = y; + } + if (nextX !== null && nextY !== null) { + modifySelectedCells(nextX, nextY, false); + return true; + } + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_UP_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID); + if (y !== 0) { + modifySelectedCells(x, y - 1, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if ( + selection.isCollapsed() && + selection.anchor + .getNode() + .getTopLevelElementOrThrow() + .getPreviousSibling() === null + ) { + event.preventDefault(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_DOWN_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID); + if (y !== rows.length - 1) { + modifySelectedCells(x, y + 1, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if ( + selection.isCollapsed() && + selection.anchor + .getNode() + .getTopLevelElementOrThrow() + .getNextSibling() === null + ) { + event.preventDefault(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_LEFT_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID); + if (x !== 0) { + modifySelectedCells(x - 1, y, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if (selection.isCollapsed() && selection.anchor.offset === 0) { + event.preventDefault(); + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ARROW_RIGHT_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null) { + const extend = event.shiftKey; + const cellID = extend + ? lastCellIDRef.current || primarySelectedCellID + : primarySelectedCellID; + if (cellID !== null) { + const [x, y] = cellCoordMap.get(cellID); + if (x !== rows[y].cells.length - 1) { + modifySelectedCells(x + 1, y, extend); + return true; + } + } + } + if (!$isRangeSelection(selection) || targetEditor !== cellEditor) { + return false; + } + if (selection.isCollapsed()) { + const anchor = selection.anchor; + if ( + (anchor.type === 'text' && + anchor.offset === anchor.getNode().getTextContentSize()) || + (anchor.type === 'element' && + anchor.offset === anchor.getNode().getChildrenSize()) + ) { + event.preventDefault(); + return true; + } + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_ESCAPE_COMMAND, + (event, targetEditor) => { + const selection = $getSelection(); + if (!isEditing && selection === null && targetEditor === editor) { + setSelected(true); + setPrimarySelectedCellID(null); + selectTable(); + return true; + } + if (!$isRangeSelection(selection)) { + return false; + } + if (isEditing) { + saveEditorToJSON(); + setIsEditing(false); + if (primarySelectedCellID !== null) { + setTimeout(() => { + focusCell(tableElem, primarySelectedCellID); + }, 20); + } + return true; + } + return false; + }, + COMMAND_PRIORITY_LOW, + ), + ); + }, [ + cellCoordMap, + cellEditor, + clearCellsCommand, + clearSelection, + editor, + isEditing, + modifySelectedCells, + nodeKey, + primarySelectedCellID, + rows, + saveEditorToJSON, + selectTable, + selectedCellIDs, + setSelected, + updateTableNode, + ]); + + if (cellEditor === null) { + return; + } + + return ( +
+ + + {rows.map((row) => ( + + {row.cells.map((cell) => { + const {id} = cell; + return ( + + ); + })} + + ))} + +
+ {showAddColumns && ( + + + + ); +} + +export function InsertImageUploadedDialogBody({ + onClick, + }) { + const [src, setSrc] = useState(''); + const [altText, setAltText] = useState(''); + + const isDisabled = src === ''; + + const loadImage = (files) => { + const reader = new FileReader(); + reader.onload = function () { + if (typeof reader.result === 'string') { + setSrc(reader.result); + } + return ''; + }; + if (files !== null) { + reader.readAsDataURL(files[0]); + } + }; + + return ( + <> + + + + + + + ); +} + +export function InsertImageDialog({ + activeEditor, + onClose, + }) { + const [mode, setMode] = useState(null); + const hasModifier = useRef(false); + + useEffect(() => { + hasModifier.current = false; + const handler = (e) => { + hasModifier.current = e.altKey; + }; + document.addEventListener('keydown', handler); + return () => { + document.removeEventListener('keydown', handler); + }; + }, [activeEditor]); + + const onClick = (payload) => { + activeEditor.dispatchCommand(INSERT_IMAGE_COMMAND, payload); + onClose(); + }; + + return ( + <> + {!mode && ( + + + + + + )} + {mode === 'url' && } + {mode === 'file' && } + + ); +} + +export default function ImagesPlugin({captionsEnabled,}){ + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!editor.hasNodes([ImageNode])) { + throw new Error('ImagesPlugin: ImageNode not registered on editor'); + } + + return mergeRegister( + editor.registerCommand( + INSERT_IMAGE_COMMAND, + (payload) => { + const imageNode = $createImageNode(payload); + $insertNodes([imageNode]); + if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) { + $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd(); + } + + return true; + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DRAGSTART_COMMAND, + (event) => { + return onDragStart(event); + }, + COMMAND_PRIORITY_HIGH, + ), + editor.registerCommand( + DRAGOVER_COMMAND, + (event) => { + return onDragover(event); + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + DROP_COMMAND, + (event) => { + return onDrop(event, editor); + }, + COMMAND_PRIORITY_HIGH, + ), + ); + }, [captionsEnabled, editor]); + + return null; +} + +const TRANSPARENT_IMAGE = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; +const img = document.createElement('img'); +img.src = TRANSPARENT_IMAGE; + +function onDragStart(event) { + const node = getImageNodeInSelection(); + if (!node) { + return false; + } + const dataTransfer = event.dataTransfer; + if (!dataTransfer) { + return false; + } + dataTransfer.setData('text/plain', '_'); + dataTransfer.setDragImage(img, 0, 0); + dataTransfer.setData( + 'application/x-lexical-drag', + JSON.stringify({ + data: { + altText: node.__altText, + caption: node.__caption, + height: node.__height, + key: node.getKey(), + maxWidth: node.__maxWidth, + showCaption: node.__showCaption, + src: node.__src, + width: node.__width, + }, + type: 'image', + }), + ); + + return true; +} + +function onDragover(event) { + const node = getImageNodeInSelection(); + if (!node) { + return false; + } + if (!canDropImage(event)) { + event.preventDefault(); + } + return true; +} + +function onDrop(event, editor) { + const node = getImageNodeInSelection(); + if (!node) { + return false; + } + const data = getDragImageData(event); + if (!data) { + return false; + } + event.preventDefault(); + if (canDropImage(event)) { + const range = getDragSelection(event); + node.remove(); + const rangeSelection = $createRangeSelection(); + if (range !== null && range !== undefined) { + rangeSelection.applyDOMRange(range); + } + $setSelection(rangeSelection); + editor.dispatchCommand(INSERT_IMAGE_COMMAND, data); + } + return true; +} + +function getImageNodeInSelection() { + const selection = $getSelection(); + if (!$isNodeSelection(selection)) { + return null; + } + const nodes = selection.getNodes(); + const node = nodes[0]; + return $isImageNode(node) ? node : null; +} + +function getDragImageData(event) { + const dragData = event.dataTransfer?.getData('application/x-lexical-drag'); + if (!dragData) { + return null; + } + const {type, data} = JSON.parse(dragData); + if (type !== 'image') { + return null; + } + + return data; +} + +function canDropImage(event) { + const target = event.target; + return !!( + target && + target instanceof HTMLElement && + !target.closest('code, span.editor-image') && + target.parentElement && + target.parentElement.closest('div.ContentEditable__root') + ); +} + +function getDragSelection(even) { + let range; + const target = event.target; + const targetWindow = + target == null + ? null + : target.nodeType === 9 + ? (target).defaultView + : (target).ownerDocument.defaultView; + if (document.caretRangeFromPoint) { + range = document.caretRangeFromPoint(event.clientX, event.clientY); + } else { + throw Error(`Cannot get the selection when dragging`); + } + + return range; +} diff --git a/src/pages/Note/Hlexical/plugins/Input/Dialog.jsx b/src/pages/Note/Hlexical/plugins/Input/Dialog.jsx new file mode 100644 index 0000000..329d572 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/Dialog.jsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import './index.less'; +export function DialogButtonsList({children}) { + return
{children}
; +} + +export function DialogActions({ + 'data-test-id': dataTestId, + children, + }) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/pages/Note/Hlexical/plugins/Input/DropDown.jsx b/src/pages/Note/Hlexical/plugins/Input/DropDown.jsx new file mode 100644 index 0000000..beb01a7 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/DropDown.jsx @@ -0,0 +1,222 @@ +import * as React from 'react'; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import {createPortal} from 'react-dom'; + +const DropDownContext = React.createContext(null); + +const dropDownPadding = 4; + +export function DropDownItem({ + children, + className, + onClick, + title, + }) { + const ref = useRef(null); + + const dropDownContext = React.useContext(DropDownContext); + + if (dropDownContext === null) { + throw new Error('DropDownItem must be used within a DropDown'); + } + + const {registerItem} = dropDownContext; + + useEffect(() => { + if (ref && ref.current) { + registerItem(ref); + } + }, [ref, registerItem]); + + return ( + + ); +} + +function DropDownItems({ + children, + dropDownRef, + onClose, + }) { + const [items, setItems] = useState(); + const [highlightedItem, setHighlightedItem] = useState(); + + const registerItem = useCallback( + (itemRef) => { + setItems((prev) => (prev ? [...prev, itemRef] : [itemRef])); + }, + [setItems], + ); + + const handleKeyDown = (event) => { + if (!items) return; + + const key = event.key; + + if (['Escape', 'ArrowUp', 'ArrowDown', 'Tab'].includes(key)) { + event.preventDefault(); + } + + if (key === 'Escape' || key === 'Tab') { + onClose(); + } else if (key === 'ArrowUp') { + setHighlightedItem((prev) => { + if (!prev) return items[0]; + const index = items.indexOf(prev) - 1; + return items[index === -1 ? items.length - 1 : index]; + }); + } else if (key === 'ArrowDown') { + setHighlightedItem((prev) => { + if (!prev) return items[0]; + return items[items.indexOf(prev) + 1]; + }); + } + }; + + const contextValue = useMemo( + () => ({ + registerItem, + }), + [registerItem], + ); + + useEffect(() => { + if (items && !highlightedItem) { + setHighlightedItem(items[0]); + } + + if (highlightedItem && highlightedItem.current) { + highlightedItem.current.focus(); + } + }, [items, highlightedItem]); + + return ( + +
+ {children} +
+
+ ); +} + +export default function DropDown({ + disabled = false, + buttonLabel, + buttonAriaLabel, + buttonClassName, + buttonIconClassName, + children, + stopCloseOnClickSelf, + }){ + const dropDownRef = useRef(null); + const buttonRef = useRef(null); + const [showDropDown, setShowDropDown] = useState(false); + + const handleClose = () => { + setShowDropDown(false); + if (buttonRef && buttonRef.current) { + buttonRef.current.focus(); + } + }; + + useEffect(() => { + const button = buttonRef.current; + const dropDown = dropDownRef.current; + + if (showDropDown && button !== null && dropDown !== null) { + const {top, left} = button.getBoundingClientRect(); + dropDown.style.top = `${top + button.offsetHeight + dropDownPadding}px`; + dropDown.style.left = `${Math.min( + left, + window.innerWidth - dropDown.offsetWidth - 20, + )}px`; + } + }, [dropDownRef, buttonRef, showDropDown]); + + useEffect(() => { + const button = buttonRef.current; + + if (button !== null && showDropDown) { + const handle = (event) => { + const target = event.target; + if (stopCloseOnClickSelf) { + if ( + dropDownRef.current && + dropDownRef.current.contains(target) + ) + return; + } + if (!button.contains(target)) { + setShowDropDown(false); + } + }; + document.addEventListener('click', handle); + + return () => { + document.removeEventListener('click', handle); + }; + } + }, [dropDownRef, buttonRef, showDropDown, stopCloseOnClickSelf]); + + useEffect(() => { + const handleButtonPositionUpdate = () => { + if (showDropDown) { + const button = buttonRef.current; + const dropDown = dropDownRef.current; + if (button !== null && dropDown !== null) { + const {top} = button.getBoundingClientRect(); + const newPosition = top + button.offsetHeight + dropDownPadding; + if (newPosition !== dropDown.getBoundingClientRect().top) { + dropDown.style.top = `${newPosition}px`; + } + } + } + }; + + document.addEventListener('scroll', handleButtonPositionUpdate); + + return () => { + document.removeEventListener('scroll', handleButtonPositionUpdate); + }; + }, [buttonRef, dropDownRef, showDropDown]); + + return ( + <> + + + {showDropDown && + createPortal( + + {children} + , + document.body, + )} + + ); +} diff --git a/src/pages/Note/Hlexical/plugins/Input/FileInput.jsx b/src/pages/Note/Hlexical/plugins/Input/FileInput.jsx new file mode 100644 index 0000000..fcabecd --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/FileInput.jsx @@ -0,0 +1,23 @@ +import './index.less'; + +import * as React from 'react'; + +export default function FileInput({ + accept, + label, + onChange, + 'data-test-id': dataTestId, + }) { + return ( +
+ + onChange(e.target.files)} + data-test-id={dataTestId} + /> +
+ ); +} diff --git a/src/pages/Note/Hlexical/plugins/Input/TextInput.jsx b/src/pages/Note/Hlexical/plugins/Input/TextInput.jsx new file mode 100644 index 0000000..2b303be --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/TextInput.jsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import './index.less'; +export default function TextInput({ + label, + value, + onChange, + placeholder = '', + 'data-test-id': dataTestId, + type = 'text', + }) { + return ( +
+ + { + onChange(e.target.value); + }} + data-test-id={dataTestId} + /> +
+ ); +} diff --git a/src/pages/Note/Hlexical/plugins/Input/index.less b/src/pages/Note/Hlexical/plugins/Input/index.less new file mode 100644 index 0000000..96aec54 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/index.less @@ -0,0 +1,40 @@ +.Input__wrapper { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 10px; +} +.Input__label { + display: flex; + flex: 1; + color: #666; +} +.Input__input { + display: flex; + flex: 2; + border: 1px solid #999; + padding-top: 7px; + padding-bottom: 7px; + padding-left: 10px; + padding-right: 10px; + font-size: 16px; + border-radius: 5px; + min-width: 0; +} +.DialogActions { + display: flex; + flex-direction: row; + justify-content: right; + margin-top: 20px; +} + +.DialogButtonsList { + display: flex; + flex-direction: column; + justify-content: right; + margin-top: 20px; +} + +.DialogButtonsList button { + margin-bottom: 20px; +} \ No newline at end of file diff --git a/src/pages/Note/Hlexical/plugins/InsertLayoutPlug/InsertLayoutDialog/index.jsx b/src/pages/Note/Hlexical/plugins/InsertLayoutPlug/InsertLayoutDialog/index.jsx new file mode 100644 index 0000000..a5dd568 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/InsertLayoutPlug/InsertLayoutDialog/index.jsx @@ -0,0 +1,45 @@ +import {LexicalEditor} from 'lexical'; +import * as React from 'react'; +import {useState} from 'react'; +import DropDown, {DropDownItem} from "../../Input/DropDown"; +import {Button} from "antd"; + + +const LAYOUTS = [ + {label: '2 columns (equal width)', value: '1fr 1fr'}, + {label: '2 columns (25% - 75%)', value: '1fr 3fr'}, + {label: '3 columns (equal width)', value: '1fr 1fr 1fr'}, + {label: '3 columns (25% - 50% - 25%)', value: '1fr 2fr 1fr'}, + {label: '4 columns (equal width)', value: '1fr 1fr 1fr 1fr'}, +]; + +export default function InsertLayoutDialog({ + activeEditor, + onClose, + }){ + const [layout, setLayout] = useState(LAYOUTS[0].value); + const buttonLabel = LAYOUTS.find((item) => item.value === layout)?.label; + + const onClick = () => { + activeEditor.dispatchCommand(INSERT_LAYOUT_COMMAND, layout); + onClose(); + }; + + return ( + <> + + {LAYOUTS.map(({label, value}) => ( + setLayout(value)}> + {label} + + ))} + + + + ); +} diff --git a/src/pages/Note/Hlexical/plugins/InsertLayoutPlug/InsertLayoutDialog/index.less b/src/pages/Note/Hlexical/plugins/InsertLayoutPlug/InsertLayoutDialog/index.less new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js b/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js index 3cad213..cd317cb 100644 --- a/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js +++ b/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js @@ -10,7 +10,7 @@ import {useDispatch, useSelector} from "react-redux"; import md5 from "md5" import {message} from "antd"; const {ipcRenderer} = window.require('electron') -export default function SaveFilePlugin(props) { +const SaveFilePlugin=(props)=> { let activeKey = useSelector(state => state.tableBarItem.activeKey); const dispatch = useDispatch(); const [messageApi,contextHolder] = message.useMessage(); @@ -18,6 +18,9 @@ export default function SaveFilePlugin(props) { const [editorState,setEditorState]=useState(); function onChange(editorState) { + if (isEmpty(props.filePath)){ + return + } if (props.filePath.endsWith(".md")){ let read = editorState.read(() => $convertToMarkdownString(TRANSFORMERS)); setEditorState(read) @@ -83,7 +86,7 @@ export default function SaveFilePlugin(props) { if (save) { overWriteFile(filePath, resultSave) console.log("保存成功"+ filePath) - messageApi.open({type:"success",content:"保存成功:" + filePath}) + messageApi.open({type:"success",content:"保存成功:" + filePath,duration:1}) } }).catch(error => console.error(error) @@ -104,8 +107,9 @@ export default function SaveFilePlugin(props) { },[editor,onChange] ) return ( - + <> {contextHolder} - + ) } +export default SaveFilePlugin diff --git a/src/pages/Note/Hlexical/plugins/TablePlugin/index.jsx b/src/pages/Note/Hlexical/plugins/TablePlugin/index.jsx new file mode 100644 index 0000000..899ff3a --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/TablePlugin/index.jsx @@ -0,0 +1,183 @@ +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {INSERT_TABLE_COMMAND} from '@lexical/table'; +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, + EditorThemeClasses, + Klass, + LexicalCommand, + LexicalEditor, + LexicalNode, +} from 'lexical'; +import {createContext, useContext, useEffect, useMemo, useState} from 'react'; +import * as React from 'react'; +// import invariant from 'shared/invariant'; +import TextInput from "../Input/TextInput"; +import {DialogActions} from "../Input/Dialog"; +import {Button} from "antd"; +import {$createTableNodeWithDimensions, TableNode} from "../../nodes/TableNode"; + +export const INSERT_NEW_TABLE_COMMAND = createCommand('INSERT_NEW_TABLE_COMMAND'); + +export const CellContext = createContext({ + cellEditorConfig: null, + cellEditorPlugins: null, + set: () => { + // Empty + }, +}); + +export function TableContext({children}) { + const [contextValue, setContextValue] = useState({ + cellEditorConfig: null, + cellEditorPlugins: null, + }); + return ( + ({ + cellEditorConfig: contextValue.cellEditorConfig, + cellEditorPlugins: contextValue.cellEditorPlugins, + set: (cellEditorConfig, cellEditorPlugins) => { + setContextValue({cellEditorConfig, cellEditorPlugins}); + }, + }), + [contextValue.cellEditorConfig, contextValue.cellEditorPlugins], + )}> + {children} + + ); +} + +export function InsertTableDialog({activeEditor, + onClose}){ + const [rows, setRows] = useState('5'); + const [columns, setColumns] = useState('5'); + const [isDisabled, setIsDisabled] = useState(true); + + useEffect(() => { + const row = Number(rows); + const column = Number(columns); + if (row && row > 0 && row <= 500 && column && column > 0 && column <= 50) { + setIsDisabled(false); + } else { + setIsDisabled(true); + } + }, [rows, columns]); + + const onClick = () => { + activeEditor.dispatchCommand(INSERT_TABLE_COMMAND, { + columns, + rows, + }); + + onClose(); + }; + + return ( + <> + + + + + + + ); +} + +export function InsertNewTableDialog({activeEditor, onClose, + }){ + const [rows, setRows] = useState(''); + const [columns, setColumns] = useState(''); + const [isDisabled, setIsDisabled] = useState(true); + + useEffect(() => { + const row = Number(rows); + const column = Number(columns); + if (row && row > 0 && row <= 500 && column && column > 0 && column <= 50) { + setIsDisabled(false); + } else { + setIsDisabled(true); + } + }, [rows, columns]); + + const onClick = () => { + activeEditor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, {columns, rows}); + onClose(); + }; + + return ( + <> + + + + + + + ); +} + +export function TablePlugin({ + cellEditorConfig, + children, + }) { + const [editor] = useLexicalComposerContext(); + const cellContext = useContext(CellContext); + + useEffect(() => { + if (!editor.hasNodes([TableNode])) { + console.log(false, 'TablePlugin: TableNode is not registered on editor'); + } + + cellContext.set(cellEditorConfig, children); + + return editor.registerCommand( + INSERT_NEW_TABLE_COMMAND, + ({columns, rows, includeHeaders}) => { + const tableNode = $createTableNodeWithDimensions( + Number(rows), + Number(columns), + includeHeaders, + ); + $insertNodes([tableNode]); + return true; + }, + COMMAND_PRIORITY_EDITOR, + ); + }, [cellContext, cellEditorConfig, children, editor]); + + return null; +} diff --git a/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js b/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js index 51f6d46..f0fac87 100644 --- a/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js +++ b/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js @@ -1,697 +1,797 @@ -import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext"; +import {useCallback, useEffect, useMemo, useRef, useState} from "react"; import { - CAN_REDO_COMMAND, - CAN_UNDO_COMMAND, - REDO_COMMAND, - UNDO_COMMAND, - SELECTION_CHANGE_COMMAND, - FORMAT_TEXT_COMMAND, - FORMAT_ELEMENT_COMMAND, - $getSelection, - $isRangeSelection, - $createParagraphNode, - $getNodeByKey + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + REDO_COMMAND, + UNDO_COMMAND, + SELECTION_CHANGE_COMMAND, + FORMAT_TEXT_COMMAND, + FORMAT_ELEMENT_COMMAND, + $getSelection, + $isRangeSelection, + $createParagraphNode, + $getNodeByKey } from "lexical"; -import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; +import {$isLinkNode, TOGGLE_LINK_COMMAND} from "@lexical/link"; import { - $isParentElementRTL, - $wrapNodes, - $isAtNodeEnd + $isParentElementRTL, + $wrapNodes, + $isAtNodeEnd } from "@lexical/selection"; -import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import {$getNearestNodeOfType, mergeRegister} from "@lexical/utils"; import { - INSERT_ORDERED_LIST_COMMAND, - INSERT_UNORDERED_LIST_COMMAND, - REMOVE_LIST_COMMAND, - $isListNode, - ListNode + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, + REMOVE_LIST_COMMAND, + $isListNode, + ListNode } from "@lexical/list"; -import { createPortal } from "react-dom"; +import {createPortal} from "react-dom"; import { - $createHeadingNode, - $createQuoteNode, - $isHeadingNode + $createHeadingNode, + $createQuoteNode, + $isHeadingNode } from "@lexical/rich-text"; import { - $createCodeNode, - $isCodeNode, - getDefaultCodeLanguage, - getCodeLanguages + $createCodeNode, + $isCodeNode, + getDefaultCodeLanguage, + getCodeLanguages } from "@lexical/code"; +import DropDown, {DropDownItem} from "./Input/DropDown"; + +import {InsertImageDialog} from "./ImagesPlugin"; +import useModal from "../hook/userModal"; +import {InsertTableDialog} from "./TablePlugin"; +import {INSERT_HORIZONTAL_RULE_COMMAND} from "@lexical/react/LexicalHorizontalRuleNode.prod"; const LowPriority = 1; const supportedBlockTypes = new Set([ - "paragraph", - "quote", - "code", - "h1", - "h2", - "ul", - "ol" + "paragraph", + "quote", + "code", + "h1", + "h2", + "h3", + "h4", + "h5", + "ul", + "ol" ]); const blockTypeToBlockName = { - code: "代码块", - h1: "一级标题", - h2: "二级标题", - h3: "三级标题", - h4: "四级标题", - h5: "五级标题", - ol: "有序序列", - paragraph: "普通文本", - quote: "引用", - ul: "无序序列" + code: "代码块", + h1: "一级标题", + h2: "二级标题", + h3: "三级标题", + h4: "四级标题", + h5: "五级标题", + ol: "有序序列", + paragraph: "普通文本", + quote: "引用", + ul: "无序序列" }; function Divider() { - return
; + return
; } function positionEditorElement(editor, rect) { - if (rect === null) { - editor.style.opacity = "0"; - editor.style.top = "-1000px"; - editor.style.left = "-1000px"; - } else { - editor.style.opacity = "1"; - editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; - editor.style.left = `${ - rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 - }px`; - } + if (rect === null) { + editor.style.opacity = "0"; + editor.style.top = "-1000px"; + editor.style.left = "-1000px"; + } else { + editor.style.opacity = "1"; + editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; + editor.style.left = `${ + rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 + }px`; + } } -function FloatingLinkEditor({ editor }) { - const editorRef = useRef(null); - const inputRef = useRef(null); - const mouseDownRef = useRef(false); - const [linkUrl, setLinkUrl] = useState(""); - const [isEditMode, setEditMode] = useState(false); - const [lastSelection, setLastSelection] = useState(null); +function FloatingLinkEditor({editor}) { + const editorRef = useRef(null); + const inputRef = useRef(null); + const mouseDownRef = useRef(false); + const [linkUrl, setLinkUrl] = useState(""); + const [isEditMode, setEditMode] = useState(false); + const [lastSelection, setLastSelection] = useState(null); - const updateLinkEditor = useCallback(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - const node = getSelectedNode(selection); - const parent = node.getParent(); - if ($isLinkNode(parent)) { - setLinkUrl(parent.getURL()); - } else if ($isLinkNode(node)) { - setLinkUrl(node.getURL()); - } else { - setLinkUrl(""); - } - } - const editorElem = editorRef.current; - const nativeSelection = window.getSelection(); - const activeElement = document.activeElement; - - if (editorElem === null) { - return; - } - - const rootElement = editor.getRootElement(); - if ( - selection !== null && - !nativeSelection.isCollapsed && - rootElement !== null && - rootElement.contains(nativeSelection.anchorNode) - ) { - const domRange = nativeSelection.getRangeAt(0); - let rect; - if (nativeSelection.anchorNode === rootElement) { - let inner = rootElement; - while (inner.firstElementChild != null) { - inner = inner.firstElementChild; - } - rect = inner.getBoundingClientRect(); - } else { - rect = domRange.getBoundingClientRect(); - } - - if (!mouseDownRef.current) { - positionEditorElement(editorElem, rect); - } - setLastSelection(selection); - } else if (!activeElement || activeElement.className !== "link-input") { - positionEditorElement(editorElem, null); - setLastSelection(null); - setEditMode(false); - setLinkUrl(""); - } - - return true; - }, [editor]); - - useEffect(() => { - return mergeRegister( - editor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - updateLinkEditor(); - }); - }), - - editor.registerCommand( - SELECTION_CHANGE_COMMAND, - () => { - updateLinkEditor(); - return true; - }, - LowPriority - ) - ); - }, [editor, updateLinkEditor]); - - useEffect(() => { - editor.getEditorState().read(() => { - updateLinkEditor(); - }); - }, [editor, updateLinkEditor]); - - useEffect(() => { - if (isEditMode && inputRef.current) { - inputRef.current.focus(); - } - }, [isEditMode]); - - return ( -
- {isEditMode ? ( - { - setLinkUrl(event.target.value); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - if (lastSelection !== null) { - if (linkUrl !== "") { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); - } - setEditMode(false); - } - } else if (event.key === "Escape") { - event.preventDefault(); - setEditMode(false); + const updateLinkEditor = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent)) { + setLinkUrl(parent.getURL()); + } else if ($isLinkNode(node)) { + setLinkUrl(node.getURL()); + } else { + setLinkUrl(""); } - }} - /> - ) : ( - <> -
- - {linkUrl} - -
event.preventDefault()} - onClick={() => { - setEditMode(true); - }} - /> -
- - )} -
- ); + } + const editorElem = editorRef.current; + const nativeSelection = window.getSelection(); + const activeElement = document.activeElement; + + if (editorElem === null) { + return; + } + + const rootElement = editor.getRootElement(); + if ( + selection !== null && + !nativeSelection.isCollapsed && + rootElement !== null && + rootElement.contains(nativeSelection.anchorNode) + ) { + const domRange = nativeSelection.getRangeAt(0); + let rect; + if (nativeSelection.anchorNode === rootElement) { + let inner = rootElement; + while (inner.firstElementChild != null) { + inner = inner.firstElementChild; + } + rect = inner.getBoundingClientRect(); + } else { + rect = domRange.getBoundingClientRect(); + } + + if (!mouseDownRef.current) { + positionEditorElement(editorElem, rect); + } + setLastSelection(selection); + } else if (!activeElement || activeElement.className !== "link-input") { + positionEditorElement(editorElem, null); + setLastSelection(null); + setEditMode(false); + setLinkUrl(""); + } + + return true; + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({editorState}) => { + editorState.read(() => { + updateLinkEditor(); + }); + }), + + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + updateLinkEditor(); + return true; + }, + LowPriority + ) + ); + }, [editor, updateLinkEditor]); + + useEffect(() => { + editor.getEditorState().read(() => { + updateLinkEditor(); + }); + }, [editor, updateLinkEditor]); + + useEffect(() => { + if (isEditMode && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditMode]); + + return ( +
+ {isEditMode ? ( + { + setLinkUrl(event.target.value); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (lastSelection !== null) { + if (linkUrl !== "") { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); + } + setEditMode(false); + } + } else if (event.key === "Escape") { + event.preventDefault(); + setEditMode(false); + } + }} + /> + ) : ( + <> +
+ + {linkUrl} + +
event.preventDefault()} + onClick={() => { + setEditMode(true); + }} + /> +
+ + )} +
+ ); } -function Select({ onChange, className, options, value }) { - return ( - - ); +function Select({onChange, className, options, value}) { + return ( + + ); } function getSelectedNode(selection) { - const anchor = selection.anchor; - const focus = selection.focus; - const anchorNode = selection.anchor.getNode(); - const focusNode = selection.focus.getNode(); - if (anchorNode === focusNode) { - return anchorNode; - } - const isBackward = selection.isBackward(); - if (isBackward) { - return $isAtNodeEnd(focus) ? anchorNode : focusNode; - } else { - return $isAtNodeEnd(anchor) ? focusNode : anchorNode; - } + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + if (anchorNode === focusNode) { + return anchorNode; + } + const isBackward = selection.isBackward(); + if (isBackward) { + return $isAtNodeEnd(focus) ? anchorNode : focusNode; + } else { + return $isAtNodeEnd(anchor) ? focusNode : anchorNode; + } } function BlockOptionsDropdownList({ - editor, - blockType, - toolbarRef, - setShowBlockOptionsDropDown -}) { - const dropDownRef = useRef(null); + editor, + blockType, + toolbarRef, + setShowBlockOptionsDropDown + }) { + const dropDownRef = useRef(null); - useEffect(() => { - const toolbar = toolbarRef.current; - const dropDown = dropDownRef.current; + useEffect(() => { + const toolbar = toolbarRef.current; + const dropDown = dropDownRef.current; - if (toolbar !== null && dropDown !== null) { - const { top, left } = toolbar.getBoundingClientRect(); - dropDown.style.top = `${top + 40}px`; - dropDown.style.left = `${left}px`; - } - }, [dropDownRef, toolbarRef]); - - useEffect(() => { - const dropDown = dropDownRef.current; - const toolbar = toolbarRef.current; - - if (dropDown !== null && toolbar !== null) { - const handle = (event) => { - const target = event.target; - - if (!dropDown.contains(target) && !toolbar.contains(target)) { - setShowBlockOptionsDropDown(false); + if (toolbar !== null && dropDown !== null) { + const {top, left} = toolbar.getBoundingClientRect(); + dropDown.style.top = `${top + 40}px`; + dropDown.style.left = `${left}px`; } - }; - document.addEventListener("click", handle); + }, [dropDownRef, toolbarRef]); - return () => { - document.removeEventListener("click", handle); - }; - } - }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]); + useEffect(() => { + const dropDown = dropDownRef.current; + const toolbar = toolbarRef.current; - const formatParagraph = () => { - if (blockType !== "paragraph") { - editor.update(() => { - const selection = $getSelection(); + if (dropDown !== null && toolbar !== null) { + const handle = (event) => { + const target = event.target; - if ($isRangeSelection(selection)) { - $wrapNodes(selection, () => $createParagraphNode()); + if (!dropDown.contains(target) && !toolbar.contains(target)) { + setShowBlockOptionsDropDown(false); + } + }; + document.addEventListener("click", handle); + + return () => { + document.removeEventListener("click", handle); + }; } - }); - } - setShowBlockOptionsDropDown(false); - }; + }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]); - const formatLargeHeading = () => { - if (blockType !== "h1") { - editor.update(() => { - const selection = $getSelection(); + const formatParagraph = () => { + if (blockType !== "paragraph") { + editor.update(() => { + const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $wrapNodes(selection, () => $createHeadingNode("h1")); + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createParagraphNode()); + } + }); } - }); - } - setShowBlockOptionsDropDown(false); - }; + setShowBlockOptionsDropDown(false); + }; - const formatSmallHeading = () => { - if (blockType !== "h2") { - editor.update(() => { - const selection = $getSelection(); + const formatLargeHeading = () => { + if (blockType !== "h1") { + editor.update(() => { + const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $wrapNodes(selection, () => $createHeadingNode("h2")); + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h1")); + } + }); } - }); - } - setShowBlockOptionsDropDown(false); - }; + setShowBlockOptionsDropDown(false); + }; - const formatBulletList = () => { - if (blockType !== "ul") { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND); - } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND); - } - setShowBlockOptionsDropDown(false); - }; + const formatSmallHeading = () => { + if (blockType !== "h2") { + editor.update(() => { + const selection = $getSelection(); - const formatNumberedList = () => { - if (blockType !== "ol") { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND); - } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND); - } - setShowBlockOptionsDropDown(false); - }; - - const formatQuote = () => { - if (blockType !== "quote") { - editor.update(() => { - const selection = $getSelection(); - - if ($isRangeSelection(selection)) { - $wrapNodes(selection, () => $createQuoteNode()); + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h2")); + } + }); } - }); - } - setShowBlockOptionsDropDown(false); - }; + setShowBlockOptionsDropDown(false); + }; - const formatCode = () => { - if (blockType !== "code") { - editor.update(() => { - const selection = $getSelection(); - - if ($isRangeSelection(selection)) { - $wrapNodes(selection, () => $createCodeNode()); + const formatBulletList = () => { + if (blockType !== "ul") { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND); } - }); - } - setShowBlockOptionsDropDown(false); - }; + setShowBlockOptionsDropDown(false); + }; - return ( -
- - - - - - - -
- ); + const formatNumberedList = () => { + if (blockType !== "ol") { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND); + } + setShowBlockOptionsDropDown(false); + }; + + const formatQuote = () => { + if (blockType !== "quote") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createQuoteNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatCode = () => { + if (blockType !== "code") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createCodeNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + return ( +
+ + + + + + + +
+ ); } export default function ToolbarPlugin() { - const [editor] = useLexicalComposerContext(); - const toolbarRef = useRef(null); - const [canUndo, setCanUndo] = useState(false); - const [canRedo, setCanRedo] = useState(false); - const [blockType, setBlockType] = useState("paragraph"); - const [selectedElementKey, setSelectedElementKey] = useState(null); - const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState( - false - ); - const [codeLanguage, setCodeLanguage] = useState(""); - const [isRTL, setIsRTL] = useState(false); - const [isLink, setIsLink] = useState(false); - const [isBold, setIsBold] = useState(false); - const [isItalic, setIsItalic] = useState(false); - const [isUnderline, setIsUnderline] = useState(false); - const [isStrikethrough, setIsStrikethrough] = useState(false); - const [isCode, setIsCode] = useState(false); - - const updateToolbar = useCallback(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - const anchorNode = selection.anchor.getNode(); - const element = - anchorNode.getKey() === "root" - ? anchorNode - : anchorNode.getTopLevelElementOrThrow(); - const elementKey = element.getKey(); - const elementDOM = editor.getElementByKey(elementKey); - if (elementDOM !== null) { - setSelectedElementKey(elementKey); - if ($isListNode(element)) { - const parentList = $getNearestNodeOfType(anchorNode, ListNode); - const type = parentList ? parentList.getTag() : element.getTag(); - setBlockType(type); - } else { - const type = $isHeadingNode(element) - ? element.getTag() - : element.getType(); - setBlockType(type); - if ($isCodeNode(element)) { - setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage()); - } - } - } - // Update text format - setIsBold(selection.hasFormat("bold")); - setIsItalic(selection.hasFormat("italic")); - setIsUnderline(selection.hasFormat("underline")); - setIsStrikethrough(selection.hasFormat("strikethrough")); - setIsCode(selection.hasFormat("code")); - setIsRTL($isParentElementRTL(selection)); - - // Update links - const node = getSelectedNode(selection); - const parent = node.getParent(); - if ($isLinkNode(parent) || $isLinkNode(node)) { - setIsLink(true); - } else { - setIsLink(false); - } - } - }, [editor]); - - useEffect(() => { - return mergeRegister( - editor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - updateToolbar(); - }); - }), - editor.registerCommand( - SELECTION_CHANGE_COMMAND, - (_payload, newEditor) => { - updateToolbar(); - return false; - }, - LowPriority - ), - editor.registerCommand( - CAN_UNDO_COMMAND, - (payload) => { - setCanUndo(payload); - return false; - }, - LowPriority - ), - editor.registerCommand( - CAN_REDO_COMMAND, - (payload) => { - setCanRedo(payload); - return false; - }, - LowPriority - ) + const [modal, showModal] = useModal(); + const [editor] = useLexicalComposerContext(); + const [activeEditor, setActiveEditor] = useState(editor); + const toolbarRef = useRef(null); + const [canUndo, setCanUndo] = useState(false); + const [canRedo, setCanRedo] = useState(false); + const [blockType, setBlockType] = useState("paragraph"); + const [selectedElementKey, setSelectedElementKey] = useState(null); + const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState( + false ); - }, [editor, updateToolbar]); + const [codeLanguage, setCodeLanguage] = useState(""); + const [isRTL, setIsRTL] = useState(false); + const [isLink, setIsLink] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + const [isCode, setIsCode] = useState(false); - const codeLanguges = useMemo(() => getCodeLanguages(), []); - const onCodeLanguageSelect = useCallback( - (e) => { - editor.update(() => { - if (selectedElementKey !== null) { - const node = $getNodeByKey(selectedElementKey); - if ($isCodeNode(node)) { - node.setLanguage(e.target.value); - } + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + const element = + anchorNode.getKey() === "root" + ? anchorNode + : anchorNode.getTopLevelElementOrThrow(); + const elementKey = element.getKey(); + const elementDOM = editor.getElementByKey(elementKey); + if (elementDOM !== null) { + setSelectedElementKey(elementKey); + if ($isListNode(element)) { + const parentList = $getNearestNodeOfType(anchorNode, ListNode); + const type = parentList ? parentList.getTag() : element.getTag(); + setBlockType(type); + } else { + const type = $isHeadingNode(element) + ? element.getTag() + : element.getType(); + setBlockType(type); + if ($isCodeNode(element)) { + setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage()); + } + } + } + // Update text format + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + setIsCode(selection.hasFormat("code")); + setIsRTL($isParentElementRTL(selection)); + + // Update links + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent) || $isLinkNode(node)) { + setIsLink(true); + } else { + setIsLink(false); + } } - }); - }, - [editor, selectedElementKey] - ); + }, [editor]); - const insertLink = useCallback(() => { - if (!isLink) { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); - } else { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); - } - }, [editor, isLink]); + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({editorState}) => { + editorState.read(() => { + updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, newEditor) => { + updateToolbar(); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_UNDO_COMMAND, + (payload) => { + setCanUndo(payload); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_REDO_COMMAND, + (payload) => { + setCanRedo(payload); + return false; + }, + LowPriority + ) + ); + }, [editor, updateToolbar]); - return ( -
- - - - {supportedBlockTypes.has(blockType) && ( - <> - - {showBlockOptionsDropDown && - createPortal( - , - document.body + const codeLanguges = useMemo(() => getCodeLanguages(), []); + const onCodeLanguageSelect = useCallback( + (e) => { + editor.update(() => { + if (selectedElementKey !== null) { + const node = $getNodeByKey(selectedElementKey); + if ($isCodeNode(node)) { + node.setLanguage(e.target.value); + } + } + }); + }, + [editor, selectedElementKey] + ); + + const insertLink = useCallback(() => { + if (!isLink) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); + } else { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [editor, isLink]); + + return ( +
+ + + + {supportedBlockTypes.has(blockType) && ( + <> + + {showBlockOptionsDropDown && + createPortal( + , + document.body + )} + + )} - - - )} - {blockType === "code" ? ( - <> - + + + ) : ( + <> + + + + + + + {isLink && + createPortal(, document.body)} + + + + + + {" "} + + + { + activeEditor.dispatchCommand( + INSERT_HORIZONTAL_RULE_COMMAND, + undefined, + ); + }} + className="item"> + + Horizontal Rule + + { + activeEditor.dispatchCommand(INSERT_PAGE_BREAK, undefined); + }} + className="item"> + + Page Break + + { + showModal('Insert Image', (onClose) => ( + + )); + }} + className="item"> + + Image + + { + showModal('Insert Inline Image', (onClose) => ( + + )); + }} + className="item"> + + Inline Image + + + insertGifOnClick({ + altText: 'Cat typing on a laptop', + src: catTypingGif, + }) + } + className="item"> + + GIF + + { + activeEditor.dispatchCommand( + INSERT_EXCALIDRAW_COMMAND, + undefined, + ); + }} + className="item"> + + Excalidraw + + { + showModal('插入表格', (onClose) => ( + + )); + }} + className="item"> + + 表格 + + + + + )} +
+ ); } diff --git a/src/pages/Note/Hlexical/themes/ExampleTheme.js b/src/pages/Note/Hlexical/themes/FirstTheme.js similarity index 97% rename from src/pages/Note/Hlexical/themes/ExampleTheme.js rename to src/pages/Note/Hlexical/themes/FirstTheme.js index 3313bb3..77628cf 100644 --- a/src/pages/Note/Hlexical/themes/ExampleTheme.js +++ b/src/pages/Note/Hlexical/themes/FirstTheme.js @@ -1,4 +1,4 @@ -const exampleTheme = { +const firstTheme = { ltr: "ltr", rtl: "rtl", placeholder: "editor-placeholder", @@ -66,5 +66,5 @@ const exampleTheme = { } }; - export default exampleTheme; + export default firstTheme; \ No newline at end of file