feat:添加画图
This commit is contained in:
parent
8dd57ec5b4
commit
4f6c3915ee
|
@ -25,6 +25,7 @@
|
||||||
"@electron-forge/maker-rpm": "^6.0.4",
|
"@electron-forge/maker-rpm": "^6.0.4",
|
||||||
"@electron-forge/maker-squirrel": "^6.0.4",
|
"@electron-forge/maker-squirrel": "^6.0.4",
|
||||||
"@electron-forge/maker-zip": "^6.0.4",
|
"@electron-forge/maker-zip": "^6.0.4",
|
||||||
|
"@excalidraw/excalidraw": "^0.17.0",
|
||||||
"@lexical/react": "^0.12.6",
|
"@lexical/react": "^0.12.6",
|
||||||
"@reduxjs/toolkit": "^1.9.3",
|
"@reduxjs/toolkit": "^1.9.3",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
@ -4071,6 +4072,16 @@
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@excalidraw/excalidraw": {
|
||||||
|
"version": "0.17.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@excalidraw/excalidraw/-/excalidraw-0.17.3.tgz",
|
||||||
|
"integrity": "sha512-t+0sR30AboKcINt0WUJmSAC1cJy6npO37j/zONvuWvSh6XDOGoL1E0L+WYKJMBzp4wnOQhRIghQJmdfktQlO8w==",
|
||||||
|
"dev": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^17.0.2 || ^18.2.0",
|
||||||
|
"react-dom": "^17.0.2 || ^18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@gar/promisify": {
|
"node_modules/@gar/promisify": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/pro-components": "^2.3.57",
|
"@ant-design/pro-components": "^2.3.57",
|
||||||
"@craco/craco": "^6.0.0",
|
"@craco/craco": "^6.0.0",
|
||||||
|
"@excalidraw/excalidraw": "^0.17.0",
|
||||||
"@electron-forge/cli": "^6.0.4",
|
"@electron-forge/cli": "^6.0.4",
|
||||||
"@electron-forge/maker-deb": "^6.0.4",
|
"@electron-forge/maker-deb": "^6.0.4",
|
||||||
"@electron-forge/maker-rpm": "^6.0.4",
|
"@electron-forge/maker-rpm": "^6.0.4",
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {HorizontalRulePlugin} from "@lexical/react/LexicalHorizontalRulePlugin"
|
||||||
import InlineImagePlugin from "./plugins/InlineImagePlugin";
|
import InlineImagePlugin from "./plugins/InlineImagePlugin";
|
||||||
import {TablePlugin} from "@lexical/react/LexicalTablePlugin";
|
import {TablePlugin} from "@lexical/react/LexicalTablePlugin";
|
||||||
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
|
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
|
||||||
|
import ExcalidrawPlugin from "./plugins/ExcalidrawPlugin";
|
||||||
function Placeholder() {
|
function Placeholder() {
|
||||||
return <div className="editor-placeholder">Enter some rich text...</div>;
|
return <div className="editor-placeholder">Enter some rich text...</div>;
|
||||||
}
|
}
|
||||||
|
@ -80,8 +81,6 @@ export default function Hlexical(props) {
|
||||||
/>
|
/>
|
||||||
{/* 表格单元格操作 */}
|
{/* 表格单元格操作 */}
|
||||||
<TableCellActionMenuPlugin/>
|
<TableCellActionMenuPlugin/>
|
||||||
|
|
||||||
|
|
||||||
<TabIndentationPlugin />
|
<TabIndentationPlugin />
|
||||||
{/*markdown 快捷键*/}
|
{/*markdown 快捷键*/}
|
||||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS}/>
|
<MarkdownShortcutPlugin transformers={TRANSFORMERS}/>
|
||||||
|
@ -94,13 +93,9 @@ export default function Hlexical(props) {
|
||||||
{/*页分割线*/}
|
{/*页分割线*/}
|
||||||
|
|
||||||
{/*目录加载*/}
|
{/*目录加载*/}
|
||||||
{/* 表格加载 */}
|
|
||||||
{/*<TableOfContentsPlugin />*/}
|
{/* 画图 */}
|
||||||
{/*<LexicalTableOfContents>*/}
|
<ExcalidrawPlugin />
|
||||||
{/* {(tableOfContentsArray) => {*/}
|
|
||||||
{/* return <MyCustomTableOfContetsPlugin tableOfContents={tableOfContentsArray} />;*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/*</LexicalTableOfContents>*/}
|
|
||||||
|
|
||||||
<ImportFilePlugin filePath={props.filePath}/>
|
<ImportFilePlugin filePath={props.filePath}/>
|
||||||
<SaveFilePlugin filePath={props.filePath}/>
|
<SaveFilePlugin filePath={props.filePath}/>
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||||
|
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
|
||||||
|
import {mergeRegister} from '@lexical/utils';
|
||||||
|
import {
|
||||||
|
$getNodeByKey,
|
||||||
|
$getSelection,
|
||||||
|
$isNodeSelection,
|
||||||
|
CLICK_COMMAND,
|
||||||
|
COMMAND_PRIORITY_LOW,
|
||||||
|
KEY_BACKSPACE_COMMAND,
|
||||||
|
KEY_DELETE_COMMAND,
|
||||||
|
} from 'lexical';
|
||||||
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import ImageResizer from '../../plugins/Input/ImageResizer';
|
||||||
|
import {$isExcalidrawNode} from '.';
|
||||||
|
import ExcalidrawImage from './ExcalidrawImage';
|
||||||
|
import ExcalidrawModal from './ExcalidrawModal';
|
||||||
|
|
||||||
|
export default function ExcalidrawComponent({
|
||||||
|
nodeKey,
|
||||||
|
data,
|
||||||
|
}) {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const [isModalOpen, setModalOpen] = useState (
|
||||||
|
data === '[]' && editor.isEditable(),
|
||||||
|
);
|
||||||
|
const imageContainerRef = useRef(null);
|
||||||
|
const buttonRef = useRef(null);
|
||||||
|
const captionButtonRef = useRef(null);
|
||||||
|
const [isSelected, setSelected, clearSelection] =
|
||||||
|
useLexicalNodeSelection(nodeKey);
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
|
||||||
|
const onDelete = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (isSelected && $isNodeSelection($getSelection())) {
|
||||||
|
event.preventDefault();
|
||||||
|
editor.update(() => {
|
||||||
|
const node = $getNodeByKey(nodeKey);
|
||||||
|
if ($isExcalidrawNode(node)) {
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[editor, isSelected, nodeKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set editor to readOnly if excalidraw is open to prevent unwanted changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalOpen) {
|
||||||
|
editor.setEditable(false);
|
||||||
|
} else {
|
||||||
|
editor.setEditable(true);
|
||||||
|
}
|
||||||
|
}, [isModalOpen, editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return mergeRegister(
|
||||||
|
editor.registerCommand(
|
||||||
|
CLICK_COMMAND,
|
||||||
|
(event) => {
|
||||||
|
const buttonElem = buttonRef.current;
|
||||||
|
const eventTarget = event.target;
|
||||||
|
|
||||||
|
if (isResizing) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonElem !== null && buttonElem.contains(eventTarget)) {
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
clearSelection();
|
||||||
|
}
|
||||||
|
setSelected(!isSelected);
|
||||||
|
if (event.detail > 1) {
|
||||||
|
setModalOpen(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_LOW,
|
||||||
|
),
|
||||||
|
editor.registerCommand(
|
||||||
|
KEY_DELETE_COMMAND,
|
||||||
|
onDelete,
|
||||||
|
COMMAND_PRIORITY_LOW,
|
||||||
|
),
|
||||||
|
editor.registerCommand(
|
||||||
|
KEY_BACKSPACE_COMMAND,
|
||||||
|
onDelete,
|
||||||
|
COMMAND_PRIORITY_LOW,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [clearSelection, editor, isSelected, isResizing, onDelete, setSelected]);
|
||||||
|
|
||||||
|
const deleteNode = useCallback(() => {
|
||||||
|
setModalOpen(false);
|
||||||
|
return editor.update(() => {
|
||||||
|
const node = $getNodeByKey(nodeKey);
|
||||||
|
if ($isExcalidrawNode(node)) {
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [editor, nodeKey]);
|
||||||
|
|
||||||
|
const setData = (
|
||||||
|
els,
|
||||||
|
aps,
|
||||||
|
fls,
|
||||||
|
) => {
|
||||||
|
if (!editor.isEditable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return editor.update(() => {
|
||||||
|
const node = $getNodeByKey(nodeKey);
|
||||||
|
if ($isExcalidrawNode(node)) {
|
||||||
|
if (els.length > 0 || Object.keys(fls).length > 0) {
|
||||||
|
node.setData(
|
||||||
|
JSON.stringify({
|
||||||
|
appState: aps,
|
||||||
|
elements: els,
|
||||||
|
files: fls,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResizeStart = () => {
|
||||||
|
setIsResizing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResizeEnd = (
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
) => {
|
||||||
|
// Delay hiding the resize bars for click case
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
editor.update(() => {
|
||||||
|
const node = $getNodeByKey(nodeKey);
|
||||||
|
|
||||||
|
if ($isExcalidrawNode(node)) {
|
||||||
|
node.setWidth(nextWidth);
|
||||||
|
node.setHeight(nextHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = useCallback(() => {
|
||||||
|
setModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
elements = [],
|
||||||
|
files = {},
|
||||||
|
appState = {},
|
||||||
|
} = useMemo(() => JSON.parse(data), [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExcalidrawModal
|
||||||
|
initialElements={elements}
|
||||||
|
initialFiles={files}
|
||||||
|
initialAppState={appState}
|
||||||
|
isShown={isModalOpen}
|
||||||
|
onDelete={deleteNode}
|
||||||
|
onClose={() => setModalOpen(false)}
|
||||||
|
onSave={(els, aps, fls) => {
|
||||||
|
editor.setEditable(true);
|
||||||
|
setData(els, aps, fls);
|
||||||
|
setModalOpen(false);
|
||||||
|
}}
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
/>
|
||||||
|
{elements.length > 0 && (
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
className={`excalidraw-button ${isSelected ? 'selected' : ''}`}>
|
||||||
|
<ExcalidrawImage
|
||||||
|
imageContainerRef={imageContainerRef}
|
||||||
|
className="image"
|
||||||
|
elements={elements}
|
||||||
|
files={files}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
{isSelected && (
|
||||||
|
<div
|
||||||
|
className="image-edit-button"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={openModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(isSelected || isResizing) && (
|
||||||
|
<ImageResizer
|
||||||
|
buttonRef={captionButtonRef}
|
||||||
|
showCaption={true}
|
||||||
|
setShowCaption={() => null}
|
||||||
|
imageRef={imageContainerRef}
|
||||||
|
editor={editor}
|
||||||
|
onResizeStart={onResizeStart}
|
||||||
|
onResizeEnd={onResizeEnd}
|
||||||
|
captionsEnabled={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {exportToSvg} from '@excalidraw/excalidraw';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
const removeStyleFromSvg_HACK = (svg) => {
|
||||||
|
const styleTag = svg?.firstElementChild?.firstElementChild;
|
||||||
|
|
||||||
|
// Generated SVG is getting double-sized by height and width attributes
|
||||||
|
// We want to match the real size of the SVG element
|
||||||
|
const viewBox = svg.getAttribute('viewBox');
|
||||||
|
if (viewBox != null) {
|
||||||
|
const viewBoxDimensions = viewBox.split(' ');
|
||||||
|
svg.setAttribute('width', viewBoxDimensions[2]);
|
||||||
|
svg.setAttribute('height', viewBoxDimensions[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (styleTag && styleTag.tagName === 'style') {
|
||||||
|
styleTag.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @explorer-desc
|
||||||
|
* A component for rendering Excalidraw elements as a static image
|
||||||
|
*/
|
||||||
|
export default function ExcalidrawImage({
|
||||||
|
elements,
|
||||||
|
files,
|
||||||
|
imageContainerRef,
|
||||||
|
appState,
|
||||||
|
rootClassName = null,
|
||||||
|
}) {
|
||||||
|
const [Svg, setSvg] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const setContent = async () => {
|
||||||
|
const svg = await exportToSvg({
|
||||||
|
appState,
|
||||||
|
elements,
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
removeStyleFromSvg_HACK(svg);
|
||||||
|
|
||||||
|
svg.setAttribute('width', '100%');
|
||||||
|
svg.setAttribute('height', '100%');
|
||||||
|
svg.setAttribute('display', 'block');
|
||||||
|
|
||||||
|
setSvg(svg);
|
||||||
|
};
|
||||||
|
setContent();
|
||||||
|
}, [elements, files, appState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={imageContainerRef}
|
||||||
|
className={rootClassName ?? ''}
|
||||||
|
dangerouslySetInnerHTML={{__html: Svg?.outerHTML ?? ''}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
import './ExcalidrawModal.less';
|
||||||
|
|
||||||
|
import {Excalidraw} from '@excalidraw/excalidraw';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {ReactPortal, useEffect, useLayoutEffect, useRef, useState} from 'react';
|
||||||
|
import {createPortal} from 'react-dom';
|
||||||
|
|
||||||
|
import Button from '../../plugins/Input/Button';
|
||||||
|
import Modal from '../../plugins/Input/Modal';
|
||||||
|
|
||||||
|
export const useCallbackRefState = () => {
|
||||||
|
const [refValue, setRefValue] =
|
||||||
|
React.useState(null);
|
||||||
|
const refCallback = React.useCallback(
|
||||||
|
(value) => setRefValue(value),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return [refValue, refCallback];
|
||||||
|
};
|
||||||
|
export default function ExcalidrawModal({
|
||||||
|
closeOnClickOutside = false,
|
||||||
|
onSave,
|
||||||
|
initialElements,
|
||||||
|
initialAppState,
|
||||||
|
initialFiles,
|
||||||
|
isShown = false,
|
||||||
|
onDelete,
|
||||||
|
onClose,
|
||||||
|
}) {
|
||||||
|
const excaliDrawModelRef = useRef(null);
|
||||||
|
const [excalidrawAPI, excalidrawAPIRefCallback] = useCallbackRefState();
|
||||||
|
const [discardModalOpen, setDiscardModalOpen] = useState(false);
|
||||||
|
const [elements, setElements] =
|
||||||
|
useState(initialElements);
|
||||||
|
const [files, setFiles] = useState(initialFiles);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (excaliDrawModelRef.current !== null) {
|
||||||
|
excaliDrawModelRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let modalOverlayElement= null;
|
||||||
|
|
||||||
|
const clickOutsideHandler = (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (
|
||||||
|
excaliDrawModelRef.current !== null &&
|
||||||
|
!excaliDrawModelRef.current.contains(target) &&
|
||||||
|
closeOnClickOutside
|
||||||
|
) {
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (excaliDrawModelRef.current !== null) {
|
||||||
|
modalOverlayElement = excaliDrawModelRef.current?.parentElement;
|
||||||
|
if (modalOverlayElement !== null) {
|
||||||
|
modalOverlayElement?.addEventListener('click', clickOutsideHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (modalOverlayElement !== null) {
|
||||||
|
modalOverlayElement?.removeEventListener('click', clickOutsideHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [closeOnClickOutside, onDelete]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const currentModalRef = excaliDrawModelRef.current;
|
||||||
|
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentModalRef !== null) {
|
||||||
|
currentModalRef.addEventListener('keydown', onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentModalRef !== null) {
|
||||||
|
currentModalRef.removeEventListener('keydown', onKeyDown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [elements, files, onDelete]);
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (elements.filter((el) => !el.isDeleted).length > 0) {
|
||||||
|
const appState = excalidrawAPI?.getAppState();
|
||||||
|
// We only need a subset of the state
|
||||||
|
const partialState = {
|
||||||
|
exportBackground: appState.exportBackground,
|
||||||
|
exportScale: appState.exportScale,
|
||||||
|
exportWithDarkMode: appState.theme === 'dark',
|
||||||
|
isBindingEnabled: appState.isBindingEnabled,
|
||||||
|
isLoading: appState.isLoading,
|
||||||
|
name: appState.name,
|
||||||
|
theme: appState.theme,
|
||||||
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
|
viewModeEnabled: appState.viewModeEnabled,
|
||||||
|
zenModeEnabled: appState.zenModeEnabled,
|
||||||
|
zoom: appState.zoom,
|
||||||
|
};
|
||||||
|
onSave(elements, partialState, files);
|
||||||
|
} else {
|
||||||
|
// delete node if the scene is clear
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const discard = () => {
|
||||||
|
if (elements.filter((el) => !el.isDeleted).length === 0) {
|
||||||
|
// delete node if the scene is clear
|
||||||
|
onDelete();
|
||||||
|
} else {
|
||||||
|
//Otherwise, show confirmation dialog before closing
|
||||||
|
setDiscardModalOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function ShowDiscardDialog() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Discard"
|
||||||
|
onClose={() => {
|
||||||
|
setDiscardModalOpen(false);
|
||||||
|
}}
|
||||||
|
closeOnClickOutside={false}>
|
||||||
|
Are you sure you want to discard the changes?
|
||||||
|
<div className="ExcalidrawModal__discardModal">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setDiscardModalOpen(false);
|
||||||
|
onClose();
|
||||||
|
}}>
|
||||||
|
Discard
|
||||||
|
</Button>{' '}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setDiscardModalOpen(false);
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isShown === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (
|
||||||
|
els,
|
||||||
|
_,
|
||||||
|
fls,
|
||||||
|
) => {
|
||||||
|
setElements(els);
|
||||||
|
setFiles(fls);
|
||||||
|
};
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="ExcalidrawModal__overlay" role="dialog">
|
||||||
|
<div
|
||||||
|
className="ExcalidrawModal__modal"
|
||||||
|
ref={excaliDrawModelRef}
|
||||||
|
tabIndex={-1}>
|
||||||
|
<div className="ExcalidrawModal__row">
|
||||||
|
{discardModalOpen && <ShowDiscardDialog />}
|
||||||
|
<Excalidraw
|
||||||
|
onChange={onChange}
|
||||||
|
excalidrawAPI={excalidrawAPIRefCallback}
|
||||||
|
initialData={{
|
||||||
|
appState: initialAppState || {isLoading: false},
|
||||||
|
elements: initialElements,
|
||||||
|
files: initialFiles,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="ExcalidrawModal__actions">
|
||||||
|
<button className="action-button" onClick={discard}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
<button className="action-button" onClick={save}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ExcalidrawModal__overlay {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
flex-direction: column;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
flex-grow: 0px;
|
||||||
|
flex-shrink: 1px;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: rgba(40, 40, 40, 0.6);
|
||||||
|
}
|
||||||
|
.ExcalidrawModal__actions {
|
||||||
|
text-align: end;
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.ExcalidrawModal__actions button {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.ExcalidrawModal__row {
|
||||||
|
position: relative;
|
||||||
|
padding: 40px 5px 5px;
|
||||||
|
width: 70vw;
|
||||||
|
height: 70vh;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.ExcalidrawModal__row > div {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.ExcalidrawModal__modal {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
top: 50px;
|
||||||
|
width: auto;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
.ExcalidrawModal__discardModal {
|
||||||
|
margin-top: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
import {DecoratorNode} from 'lexical';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Suspense} from 'react';
|
||||||
|
|
||||||
|
const ExcalidrawComponent = React.lazy(() => import('./ExcalidrawComponent'));
|
||||||
|
|
||||||
|
|
||||||
|
function convertExcalidrawElement(
|
||||||
|
domNode,
|
||||||
|
){
|
||||||
|
const excalidrawData = domNode.getAttribute('data-lexical-excalidraw-json');
|
||||||
|
if (excalidrawData) {
|
||||||
|
const node = $createExcalidrawNode();
|
||||||
|
node.__data = excalidrawData;
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExcalidrawNode extends DecoratorNode {
|
||||||
|
__data;
|
||||||
|
__width;
|
||||||
|
__height;
|
||||||
|
|
||||||
|
static getType() {
|
||||||
|
return 'excalidraw';
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node) {
|
||||||
|
return new ExcalidrawNode(node.__data, node.__key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode) {
|
||||||
|
return new ExcalidrawNode(serializedNode.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJSON() {
|
||||||
|
return {
|
||||||
|
data: this.__data,
|
||||||
|
height: this.__height,
|
||||||
|
type: 'excalidraw',
|
||||||
|
version: 1,
|
||||||
|
width: this.__width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data = '[]', key) {
|
||||||
|
super(key);
|
||||||
|
this.__data = data;
|
||||||
|
this.__width = 'inherit';
|
||||||
|
this.__height = 'inherit';
|
||||||
|
}
|
||||||
|
|
||||||
|
// View
|
||||||
|
createDOM(config) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
const theme = config.theme;
|
||||||
|
const className = theme.image;
|
||||||
|
|
||||||
|
span.style.width =
|
||||||
|
this.__width === 'inherit' ? 'inherit' : `${this.__width}px`;
|
||||||
|
span.style.height =
|
||||||
|
this.__height === 'inherit' ? 'inherit' : `${this.__height}px`;
|
||||||
|
|
||||||
|
if (className !== undefined) {
|
||||||
|
span.className = className;
|
||||||
|
}
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDOM() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static importDOM() {
|
||||||
|
return {
|
||||||
|
span: (domNode ) => {
|
||||||
|
if (!domNode.hasAttribute('data-lexical-excalidraw-json')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
conversion: convertExcalidrawElement,
|
||||||
|
priority: 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
exportDOM(editor) {
|
||||||
|
const element = document.createElement('span');
|
||||||
|
|
||||||
|
element.style.display = 'inline-block';
|
||||||
|
|
||||||
|
const content = editor.getElementByKey(this.getKey());
|
||||||
|
if (content !== null) {
|
||||||
|
const svg = content.querySelector('svg');
|
||||||
|
if (svg !== null) {
|
||||||
|
element.innerHTML = svg.outerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.style.width =
|
||||||
|
this.__width === 'inherit' ? 'inherit' : `${this.__width}px`;
|
||||||
|
element.style.height =
|
||||||
|
this.__height === 'inherit' ? 'inherit' : `${this.__height}px`;
|
||||||
|
|
||||||
|
element.setAttribute('data-lexical-excalidraw-json', this.__data);
|
||||||
|
return {element};
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(data) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWidth(width) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeight(height) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate(editor, config) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ExcalidrawComponent nodeKey={this.getKey()} data={this.__data} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createExcalidrawNode() {
|
||||||
|
return new ExcalidrawNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isExcalidrawNode(
|
||||||
|
node,
|
||||||
|
) {
|
||||||
|
return node instanceof ExcalidrawNode;
|
||||||
|
}
|
|
@ -251,7 +251,7 @@ export default function ImageComponent({
|
||||||
</Placeholder>}
|
</Placeholder>}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
/>
|
/>
|
||||||
{showNestedEditorTreeView === true ? <TreeViewPlugin/> : null}
|
{ <TreeViewPlugin/>}
|
||||||
</LexicalNestedComposer>
|
</LexicalNestedComposer>
|
||||||
</div>)}
|
</div>)}
|
||||||
{resizable && $isNodeSelection(selection) && isFocused && (<ImageResizer
|
{resizable && $isNodeSelection(selection) && isFocused && (<ImageResizer
|
||||||
|
|
|
@ -6,7 +6,10 @@ import {ImageNode} from "./ImageNode";
|
||||||
import {HorizontalRuleNode} from "@lexical/react/LexicalHorizontalRuleNode";
|
import {HorizontalRuleNode} from "@lexical/react/LexicalHorizontalRuleNode";
|
||||||
import {InlineImageNode} from "./InlineImageNode";
|
import {InlineImageNode} from "./InlineImageNode";
|
||||||
import {TableNode,TableCellNode, TableRowNode} from "@lexical/table";
|
import {TableNode,TableCellNode, TableRowNode} from "@lexical/table";
|
||||||
|
import {ExcalidrawNode} from "./ExcalidrawNode";
|
||||||
|
import {EmojiNode} from "./EmojiNode";
|
||||||
|
import {HashtagNode} from '@lexical/hashtag';
|
||||||
|
import {KeywordNode} from "./KeywordNode";
|
||||||
const UsefulNodes=[
|
const UsefulNodes=[
|
||||||
HeadingNode,
|
HeadingNode,
|
||||||
ListNode,
|
ListNode,
|
||||||
|
@ -22,5 +25,9 @@ const UsefulNodes=[
|
||||||
InlineImageNode,
|
InlineImageNode,
|
||||||
LinkNode,
|
LinkNode,
|
||||||
HorizontalRuleNode,
|
HorizontalRuleNode,
|
||||||
|
ExcalidrawNode,
|
||||||
|
EmojiNode,
|
||||||
|
HashtagNode,
|
||||||
|
KeywordNode
|
||||||
]
|
]
|
||||||
export default UsefulNodes;
|
export default UsefulNodes;
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||||
|
import {$wrapNodeInElement} from '@lexical/utils';
|
||||||
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
|
$insertNodes,
|
||||||
|
$isRootOrShadowRoot,
|
||||||
|
COMMAND_PRIORITY_EDITOR,
|
||||||
|
createCommand,
|
||||||
|
LexicalCommand,
|
||||||
|
} from 'lexical';
|
||||||
|
import {useEffect} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$createExcalidrawNode,
|
||||||
|
ExcalidrawNode,
|
||||||
|
} from '../../nodes/ExcalidrawNode';
|
||||||
|
|
||||||
|
export const INSERT_EXCALIDRAW_COMMAND = createCommand(
|
||||||
|
'INSERT_EXCALIDRAW_COMMAND',
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function ExcalidrawPlugin() {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor.hasNodes([ExcalidrawNode])) {
|
||||||
|
throw new Error(
|
||||||
|
'ExcalidrawPlugin: ExcalidrawNode not registered on editor',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor.registerCommand(
|
||||||
|
INSERT_EXCALIDRAW_COMMAND,
|
||||||
|
() => {
|
||||||
|
const excalidrawNode = $createExcalidrawNode();
|
||||||
|
|
||||||
|
$insertNodes([excalidrawNode]);
|
||||||
|
if ($isRootOrShadowRoot(excalidrawNode.getParentOrThrow())) {
|
||||||
|
$wrapNodeInElement(excalidrawNode, $createParagraphNode).selectEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_EDITOR,
|
||||||
|
);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import useModal from "../hook/userModal";
|
||||||
import {InsertTableDialog} from "./TablePlugin";
|
import {InsertTableDialog} from "./TablePlugin";
|
||||||
import {INSERT_HORIZONTAL_RULE_COMMAND} from "@lexical/react/LexicalHorizontalRuleNode";
|
import {INSERT_HORIZONTAL_RULE_COMMAND} from "@lexical/react/LexicalHorizontalRuleNode";
|
||||||
import {InsertInlineImageDialog} from "./InlineImagePlugin";
|
import {InsertInlineImageDialog} from "./InlineImagePlugin";
|
||||||
|
import {INSERT_EXCALIDRAW_COMMAND} from "./ExcalidrawPlugin";
|
||||||
|
|
||||||
const LowPriority = 1;
|
const LowPriority = 1;
|
||||||
|
|
||||||
|
@ -753,17 +754,6 @@ export default function ToolbarPlugin() {
|
||||||
<i className="icon image"/>
|
<i className="icon image"/>
|
||||||
<span className="text">行内图片</span>
|
<span className="text">行内图片</span>
|
||||||
</DropDownItem>
|
</DropDownItem>
|
||||||
<DropDownItem
|
|
||||||
onClick={() =>
|
|
||||||
insertGifOnClick({
|
|
||||||
altText: 'Cat typing on a laptop',
|
|
||||||
src: catTypingGif,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="item">
|
|
||||||
<i className="icon gif"/>
|
|
||||||
<span className="text">动态图片</span>
|
|
||||||
</DropDownItem>
|
|
||||||
<DropDownItem
|
<DropDownItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
activeEditor.dispatchCommand(
|
activeEditor.dispatchCommand(
|
||||||
|
|
Loading…
Reference in New Issue