feat:添加画图
This commit is contained in:
parent
8dd57ec5b4
commit
4f6c3915ee
|
@ -25,6 +25,7 @@
|
|||
"@electron-forge/maker-rpm": "^6.0.4",
|
||||
"@electron-forge/maker-squirrel": "^6.0.4",
|
||||
"@electron-forge/maker-zip": "^6.0.4",
|
||||
"@excalidraw/excalidraw": "^0.17.0",
|
||||
"@lexical/react": "^0.12.6",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
|
@ -4071,6 +4072,16 @@
|
|||
"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": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"devDependencies": {
|
||||
"@ant-design/pro-components": "^2.3.57",
|
||||
"@craco/craco": "^6.0.0",
|
||||
"@excalidraw/excalidraw": "^0.17.0",
|
||||
"@electron-forge/cli": "^6.0.4",
|
||||
"@electron-forge/maker-deb": "^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 {TablePlugin} from "@lexical/react/LexicalTablePlugin";
|
||||
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
|
||||
import ExcalidrawPlugin from "./plugins/ExcalidrawPlugin";
|
||||
function Placeholder() {
|
||||
return <div className="editor-placeholder">Enter some rich text...</div>;
|
||||
}
|
||||
|
@ -80,8 +81,6 @@ export default function Hlexical(props) {
|
|||
/>
|
||||
{/* 表格单元格操作 */}
|
||||
<TableCellActionMenuPlugin/>
|
||||
|
||||
|
||||
<TabIndentationPlugin />
|
||||
{/*markdown 快捷键*/}
|
||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS}/>
|
||||
|
@ -94,13 +93,9 @@ export default function Hlexical(props) {
|
|||
{/*页分割线*/}
|
||||
|
||||
{/*目录加载*/}
|
||||
{/* 表格加载 */}
|
||||
{/*<TableOfContentsPlugin />*/}
|
||||
{/*<LexicalTableOfContents>*/}
|
||||
{/* {(tableOfContentsArray) => {*/}
|
||||
{/* return <MyCustomTableOfContetsPlugin tableOfContents={tableOfContentsArray} />;*/}
|
||||
{/* }}*/}
|
||||
{/*</LexicalTableOfContents>*/}
|
||||
|
||||
{/* 画图 */}
|
||||
<ExcalidrawPlugin />
|
||||
|
||||
<ImportFilePlugin 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>}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
{showNestedEditorTreeView === true ? <TreeViewPlugin/> : null}
|
||||
{ <TreeViewPlugin/>}
|
||||
</LexicalNestedComposer>
|
||||
</div>)}
|
||||
{resizable && $isNodeSelection(selection) && isFocused && (<ImageResizer
|
||||
|
|
|
@ -6,7 +6,10 @@ import {ImageNode} from "./ImageNode";
|
|||
import {HorizontalRuleNode} from "@lexical/react/LexicalHorizontalRuleNode";
|
||||
import {InlineImageNode} from "./InlineImageNode";
|
||||
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=[
|
||||
HeadingNode,
|
||||
ListNode,
|
||||
|
@ -22,5 +25,9 @@ const UsefulNodes=[
|
|||
InlineImageNode,
|
||||
LinkNode,
|
||||
HorizontalRuleNode,
|
||||
ExcalidrawNode,
|
||||
EmojiNode,
|
||||
HashtagNode,
|
||||
KeywordNode
|
||||
]
|
||||
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 {INSERT_HORIZONTAL_RULE_COMMAND} from "@lexical/react/LexicalHorizontalRuleNode";
|
||||
import {InsertInlineImageDialog} from "./InlineImagePlugin";
|
||||
import {INSERT_EXCALIDRAW_COMMAND} from "./ExcalidrawPlugin";
|
||||
|
||||
const LowPriority = 1;
|
||||
|
||||
|
@ -753,17 +754,6 @@ export default function ToolbarPlugin() {
|
|||
<i className="icon image"/>
|
||||
<span className="text">行内图片</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() =>
|
||||
insertGifOnClick({
|
||||
altText: 'Cat typing on a laptop',
|
||||
src: catTypingGif,
|
||||
})
|
||||
}
|
||||
className="item">
|
||||
<i className="icon gif"/>
|
||||
<span className="text">动态图片</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
activeEditor.dispatchCommand(
|
||||
|
|
Loading…
Reference in New Issue