feat:editorConfig使用局部变量
This commit is contained in:
parent
7bb24704cf
commit
4a995d69a5
|
@ -301,7 +301,7 @@ const ItemTree = (prop) => {
|
||||||
if (dirFlag){
|
if (dirFlag){
|
||||||
menuItem.push(getMenuItem('1',<DirAddDir fileDir={key} />))
|
menuItem.push(getMenuItem('1',<DirAddDir fileDir={key} />))
|
||||||
menuItem.push(getMenuItem('2',"添加文件",[
|
menuItem.push(getMenuItem('2',"添加文件",[
|
||||||
getMenuItem("2-1",<DirAddFile fileDir={key} fileExt=".markdown"/>),
|
getMenuItem("2-1",<DirAddFile fileDir={key} fileExt=".md"/>),
|
||||||
getMenuItem("2-2",<DirAddFile fileDir={key} fileExt=".lexical"/>)]
|
getMenuItem("2-2",<DirAddFile fileDir={key} fileExt=".lexical"/>)]
|
||||||
),null, 'group'
|
),null, 'group'
|
||||||
)
|
)
|
||||||
|
@ -318,7 +318,7 @@ const ItemTree = (prop) => {
|
||||||
console.log('onClick',e)
|
console.log('onClick',e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// onMouseLeave={e => {setState("");}}
|
onMouseLeave={e => {setState("");}}
|
||||||
items={menuItem}>
|
items={menuItem}>
|
||||||
</Menu>,
|
</Menu>,
|
||||||
document.body
|
document.body
|
||||||
|
|
|
@ -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 (<div>
|
||||||
|
<Modal
|
||||||
|
open={isOpen}
|
||||||
|
//onOk={handleOk}
|
||||||
|
onCancel={onClose}
|
||||||
|
title={title}
|
||||||
|
// closeOnClickOutside={closeOnClickOutside}
|
||||||
|
>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</Modal></div>
|
||||||
|
);
|
||||||
|
}, [modalContent, onClose,isOpen]);
|
||||||
|
|
||||||
|
const showModal = useCallback((
|
||||||
|
title,
|
||||||
|
getContent,
|
||||||
|
closeOnClickOutside = false,
|
||||||
|
) => {
|
||||||
|
setModalContent({
|
||||||
|
closeOnClickOutside,
|
||||||
|
content: getContent(onClose),
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [modal, showModal];
|
||||||
|
}
|
|
@ -1,24 +1,16 @@
|
||||||
import ExampleTheme from "./themes/ExampleTheme";
|
import FirstTheme from "./themes/FirstTheme";
|
||||||
import {LexicalComposer} from "@lexical/react/LexicalComposer";
|
import {LexicalComposer} from "@lexical/react/LexicalComposer";
|
||||||
import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
|
import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
|
||||||
import {ContentEditable} from "@lexical/react/LexicalContentEditable";
|
import {ContentEditable} from "@lexical/react/LexicalContentEditable";
|
||||||
import {HistoryPlugin} from "@lexical/react/LexicalHistoryPlugin";
|
import {HistoryPlugin} from "@lexical/react/LexicalHistoryPlugin";
|
||||||
import {AutoFocusPlugin} from "@lexical/react/LexicalAutoFocusPlugin";
|
import {AutoFocusPlugin} from "@lexical/react/LexicalAutoFocusPlugin";
|
||||||
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
||||||
import TreeViewPlugin from "./plugins/TreeViewPlugin";
|
|
||||||
import ToolbarPlugin from "./plugins/ToolbarPlugin";
|
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 {MarkdownShortcutPlugin} from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
||||||
import {
|
import {
|
||||||
TRANSFORMERS, $convertFromMarkdownString,
|
TRANSFORMERS, $convertFromMarkdownString,
|
||||||
} from "@lexical/markdown";
|
} from "@lexical/markdown";
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
import {useEffect, useState} from 'react';
|
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
|
||||||
import {importFile} from "../../../utils/File"
|
import {importFile} from "../../../utils/File"
|
||||||
|
|
||||||
import {isEmpty} from "../../../utils/ObjectUtils";
|
import {isEmpty} from "../../../utils/ObjectUtils";
|
||||||
|
@ -30,40 +22,23 @@ import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin";
|
||||||
import ImportFilePlugin from "./plugins/ImportFilePlugin";
|
import ImportFilePlugin from "./plugins/ImportFilePlugin";
|
||||||
import {TablePlugin} from "@lexical/react/LexicalTablePlugin";
|
import {TablePlugin} from "@lexical/react/LexicalTablePlugin";
|
||||||
import SaveFilePlugin from "./plugins/SaveFilePlugin";
|
import SaveFilePlugin from "./plugins/SaveFilePlugin";
|
||||||
|
import {TabIndentationPlugin} from "@lexical/react/LexicalTabIndentationPlugin";
|
||||||
|
import UsefulNodes from "./nodes/UsefulNodes";
|
||||||
|
import ImagesPlugin from "./plugins/ImagesPlugin";
|
||||||
function Placeholder() {
|
function Placeholder() {
|
||||||
return <div className="editor-placeholder">Enter some rich text...</div>;
|
return <div className="editor-placeholder">Enter some rich text...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
export default function Hlexical(props) {
|
||||||
|
let editorConfig = {
|
||||||
|
theme: FirstTheme,
|
||||||
|
onError(error) {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
nodes: UsefulNodes
|
||||||
|
};
|
||||||
console.log("Hlexical(props):this.props.filePath:", props.filePath)
|
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 => {
|
importFile(props.filePath).then(value => {
|
||||||
if (isEmpty(value)) {
|
if (isEmpty(value)) {
|
||||||
return
|
return
|
||||||
|
@ -94,8 +69,21 @@ export default function Hlexical(props) {
|
||||||
<AutoLinkPlugin/>
|
<AutoLinkPlugin/>
|
||||||
<ListMaxIndentLevelPlugin maxDepth={7}/>
|
<ListMaxIndentLevelPlugin maxDepth={7}/>
|
||||||
<TablePlugin/>
|
<TablePlugin/>
|
||||||
|
<TabIndentationPlugin />
|
||||||
{/*markdown 快捷键*/}
|
{/*markdown 快捷键*/}
|
||||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS}/>
|
<MarkdownShortcutPlugin transformers={TRANSFORMERS}/>
|
||||||
|
{/*图片加载*/}
|
||||||
|
<ImagesPlugin/>
|
||||||
|
{/*目录加载*/}
|
||||||
|
{/* 表格加载 */}
|
||||||
|
|
||||||
|
{/*<TableOfContentsPlugin />*/}
|
||||||
|
{/*<LexicalTableOfContents>*/}
|
||||||
|
{/* {(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,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 (
|
||||||
|
// <Suspense fallback={null}>
|
||||||
|
// <ImageComponent
|
||||||
|
// src={this.__src}
|
||||||
|
// altText={this.__altText}
|
||||||
|
// width={this.__width}
|
||||||
|
// height={this.__height}
|
||||||
|
// maxWidth={this.__maxWidth}
|
||||||
|
// nodeKey={this.getKey()}
|
||||||
|
// showCaption={this.__showCaption}
|
||||||
|
// caption={this.__caption}
|
||||||
|
// captionsEnabled={this.__captionsEnabled}
|
||||||
|
// resizable={true}
|
||||||
|
// />
|
||||||
|
// </Suspense>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,383 @@
|
||||||
|
import {DecoratorNode} from 'lexical';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Suspense} from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
export const cellHTMLCache = new Map();
|
||||||
|
export const cellTextContentCache = new Map();
|
||||||
|
|
||||||
|
const emptyEditorJSON =
|
||||||
|
'{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
|
||||||
|
|
||||||
|
const plainTextEditorJSON = (text) =>
|
||||||
|
text === ''
|
||||||
|
? emptyEditorJSON
|
||||||
|
: `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":${text},"type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`;
|
||||||
|
|
||||||
|
const TableComponent = React.lazy(
|
||||||
|
// @ts-ignore
|
||||||
|
() => import('./TableComponent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function createUID() {
|
||||||
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.replace(/[^a-z]+/g, '')
|
||||||
|
.substr(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCell(type) {
|
||||||
|
return {
|
||||||
|
colSpan: 1,
|
||||||
|
id: createUID(),
|
||||||
|
json: emptyEditorJSON,
|
||||||
|
type,
|
||||||
|
width: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRow() {
|
||||||
|
return {
|
||||||
|
cells: [],
|
||||||
|
height: null,
|
||||||
|
id: createUID(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function extractRowsFromHTML(tableElem) {
|
||||||
|
const rowElems = tableElem.querySelectorAll('tr');
|
||||||
|
const rows = [];
|
||||||
|
for (let y = 0; y < rowElems.length; y++) {
|
||||||
|
const rowElem = rowElems[y];
|
||||||
|
const cellElems = rowElem.querySelectorAll('td,th');
|
||||||
|
if (!cellElems || cellElems.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cells = [];
|
||||||
|
for (let x = 0; x < cellElems.length; x++) {
|
||||||
|
const cellElem = cellElems[x];
|
||||||
|
const isHeader = cellElem.nodeName === 'TH';
|
||||||
|
const cell = createCell(isHeader ? 'header' : 'normal');
|
||||||
|
cell.json = plainTextEditorJSON(
|
||||||
|
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
|
||||||
|
);
|
||||||
|
cells.push(cell);
|
||||||
|
}
|
||||||
|
const row = createRow();
|
||||||
|
row.cells = cells;
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertTableElement(domNode){
|
||||||
|
const rowElems = domNode.querySelectorAll('tr');
|
||||||
|
if (!rowElems || rowElems.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rows = [];
|
||||||
|
for (let y = 0; y < rowElems.length; y++) {
|
||||||
|
const rowElem = rowElems[y];
|
||||||
|
const cellElems = rowElem.querySelectorAll('td,th');
|
||||||
|
if (!cellElems || cellElems.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cells = [];
|
||||||
|
for (let x = 0; x < cellElems.length; x++) {
|
||||||
|
const cellElem = cellElems[x];
|
||||||
|
const isHeader = cellElem.nodeName === 'TH';
|
||||||
|
const cell = createCell(isHeader ? 'header' : 'normal');
|
||||||
|
cell.json = plainTextEditorJSON(
|
||||||
|
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
|
||||||
|
);
|
||||||
|
cells.push(cell);
|
||||||
|
}
|
||||||
|
const row = createRow();
|
||||||
|
row.cells = cells;
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
return {node: $createTableNode(rows)};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportTableCellsToHTML(
|
||||||
|
rows,
|
||||||
|
rect,
|
||||||
|
) {
|
||||||
|
const table = document.createElement('table');
|
||||||
|
const colGroup = document.createElement('colgroup');
|
||||||
|
const tBody = document.createElement('tbody');
|
||||||
|
const firstRow = rows[0];
|
||||||
|
|
||||||
|
for (
|
||||||
|
let x = rect != null ? rect.startX : 0;
|
||||||
|
x < (rect != null ? rect.endX + 1 : firstRow.cells.length);
|
||||||
|
x++
|
||||||
|
) {
|
||||||
|
const col = document.createElement('col');
|
||||||
|
colGroup.append(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
let y = rect != null ? rect.startY : 0;
|
||||||
|
y < (rect != null ? rect.endY + 1 : rows.length);
|
||||||
|
y++
|
||||||
|
) {
|
||||||
|
const row = rows[y];
|
||||||
|
const cells = row.cells;
|
||||||
|
const rowElem = document.createElement('tr');
|
||||||
|
|
||||||
|
for (
|
||||||
|
let x = rect != null ? rect.startX : 0;
|
||||||
|
x < (rect != null ? rect.endX + 1 : cells.length);
|
||||||
|
x++
|
||||||
|
) {
|
||||||
|
const cell = cells[x];
|
||||||
|
const cellElem = document.createElement(
|
||||||
|
cell.type === 'header' ? 'th' : 'td',
|
||||||
|
);
|
||||||
|
cellElem.innerHTML = cellHTMLCache.get(cell.json) || '';
|
||||||
|
rowElem.appendChild(cellElem);
|
||||||
|
}
|
||||||
|
tBody.appendChild(rowElem);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.appendChild(colGroup);
|
||||||
|
table.appendChild(tBody);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableNode extends DecoratorNode {
|
||||||
|
__rows;
|
||||||
|
|
||||||
|
static getType() {
|
||||||
|
return 'tablesheet';
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node) {
|
||||||
|
return new TableNode(Array.from(node.__rows), node.__key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode) {
|
||||||
|
return $createTableNode(serializedNode.rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJSON() {
|
||||||
|
return {
|
||||||
|
rows: this.__rows,
|
||||||
|
type: 'tablesheet',
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static importDOM() {
|
||||||
|
return {
|
||||||
|
table: (_node) => ({
|
||||||
|
conversion: convertTableElement,
|
||||||
|
priority: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
exportDOM() {
|
||||||
|
return {element: exportTableCellsToHTML(this.__rows)};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(rows, key) {
|
||||||
|
super(key);
|
||||||
|
this.__rows = rows || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
createDOM() {
|
||||||
|
return document.createElement('div');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDOM() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeRows(startX, startY, mergeRows) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
const endY = Math.min(rows.length, startY + mergeRows.length);
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
const row = rows[y];
|
||||||
|
const mergeRow = mergeRows[y - startY];
|
||||||
|
const cells = row.cells;
|
||||||
|
const cellsClone = Array.from(cells);
|
||||||
|
const rowClone = {...row, cells: cellsClone};
|
||||||
|
const mergeCells = mergeRow.cells;
|
||||||
|
const endX = Math.min(cells.length, startX + mergeCells.length);
|
||||||
|
for (let x = startX; x < endX; x++) {
|
||||||
|
const cell = cells[x];
|
||||||
|
const mergeCell = mergeCells[x - startX];
|
||||||
|
const cellClone = {...cell, json: mergeCell.json, type: mergeCell.type};
|
||||||
|
cellsClone[x] = cellClone;
|
||||||
|
}
|
||||||
|
rows[y] = rowClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCellJSON(x, y, json) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
const row = rows[y];
|
||||||
|
const cells = row.cells;
|
||||||
|
const cell = cells[x];
|
||||||
|
const cellsClone = Array.from(cells);
|
||||||
|
const cellClone = {...cell, json};
|
||||||
|
const rowClone = {...row, cells: cellsClone};
|
||||||
|
cellsClone[x] = cellClone;
|
||||||
|
rows[y] = rowClone;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCellType(x, y, type) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
const row = rows[y];
|
||||||
|
const cells = row.cells;
|
||||||
|
const cell = cells[x];
|
||||||
|
const cellsClone = Array.from(cells);
|
||||||
|
const cellClone = {...cell, type};
|
||||||
|
const rowClone = {...row, cells: cellsClone};
|
||||||
|
cellsClone[x] = cellClone;
|
||||||
|
rows[y] = rowClone;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertColumnAt(x) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
for (let y = 0; y < rows.length; y++) {
|
||||||
|
const row = rows[y];
|
||||||
|
const cells = row.cells;
|
||||||
|
const cellsClone = Array.from(cells);
|
||||||
|
const rowClone = {...row, cells: cellsClone};
|
||||||
|
const type = (cells[x] || cells[x - 1]).type;
|
||||||
|
cellsClone.splice(x, 0, createCell(type));
|
||||||
|
rows[y] = rowClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteColumnAt(x) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
for (let y = 0; y < rows.length; y++) {
|
||||||
|
const row = rows[y];
|
||||||
|
const cells = row.cells;
|
||||||
|
const cellsClone = Array.from(cells);
|
||||||
|
const rowClone = {...row, cells: cellsClone};
|
||||||
|
cellsClone.splice(x, 1);
|
||||||
|
rows[y] = rowClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addColumns(count) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
for (let y = 0; y < rows.length; y++) {
|
||||||
|
const row = rows[y];
|
||||||
|
const cells = row.cells;
|
||||||
|
const cellsClone = Array.from(cells);
|
||||||
|
const rowClone = {...row, cells: cellsClone};
|
||||||
|
const type = cells[cells.length - 1].type;
|
||||||
|
for (let x = 0; x < count; x++) {
|
||||||
|
cellsClone.push(createCell(type));
|
||||||
|
}
|
||||||
|
rows[y] = rowClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertRowAt(y) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
const prevRow = rows[y] || rows[y - 1];
|
||||||
|
const cellCount = prevRow.cells.length;
|
||||||
|
const row = createRow();
|
||||||
|
for (let x = 0; x < cellCount; x++) {
|
||||||
|
const cell = createCell(prevRow.cells[x].type);
|
||||||
|
row.cells.push(cell);
|
||||||
|
}
|
||||||
|
rows.splice(y, 0, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRowAt(y) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
rows.splice(y, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows(count) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
const prevRow = rows[rows.length - 1];
|
||||||
|
const cellCount = prevRow.cells.length;
|
||||||
|
|
||||||
|
for (let y = 0; y < count; y++) {
|
||||||
|
const row = createRow();
|
||||||
|
for (let x = 0; x < cellCount; x++) {
|
||||||
|
const cell = createCell(prevRow.cells[x].type);
|
||||||
|
row.cells.push(cell);
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColumnWidth(x, width) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
const rows = self.__rows;
|
||||||
|
for (let y = 0; y < rows.length; y++) {
|
||||||
|
const row = rows[y];
|
||||||
|
const cells = row.cells;
|
||||||
|
const cellsClone = Array.from(cells);
|
||||||
|
const rowClone = {...row, cells: cellsClone};
|
||||||
|
cellsClone[x].width = width;
|
||||||
|
rows[y] = rowClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate(_, config) {
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<TableComponent
|
||||||
|
nodeKey={this.__key}
|
||||||
|
theme={config.theme}
|
||||||
|
rows={this.__rows}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInline() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isTableNode(
|
||||||
|
node ,
|
||||||
|
){
|
||||||
|
return node instanceof TableNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createTableNode(rows) {
|
||||||
|
return new TableNode(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createTableNodeWithDimensions(
|
||||||
|
rowCount,
|
||||||
|
columnCount,
|
||||||
|
includeHeaders = true,
|
||||||
|
) {
|
||||||
|
const rows = [];
|
||||||
|
for (let y = 0; y < columnCount; y++) {
|
||||||
|
const row = createRow();
|
||||||
|
rows.push(row);
|
||||||
|
for (let x = 0; x < rowCount; x++) {
|
||||||
|
row.cells.push(
|
||||||
|
createCell(
|
||||||
|
includeHeaders === true && (y === 0 || x === 0) ? 'header' : 'normal',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new TableNode(rows);
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
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 {ImageNode} from "./ImageNode";
|
||||||
|
|
||||||
|
const UsefulNodes=[
|
||||||
|
HeadingNode,
|
||||||
|
ListNode,
|
||||||
|
ListItemNode,
|
||||||
|
QuoteNode,
|
||||||
|
CodeNode,
|
||||||
|
CodeHighlightNode,
|
||||||
|
TableNode,
|
||||||
|
TableCellNode,
|
||||||
|
TableRowNode,
|
||||||
|
AutoLinkNode,
|
||||||
|
ImageNode,
|
||||||
|
LinkNode
|
||||||
|
]
|
||||||
|
export default UsefulNodes;
|
|
@ -0,0 +1,356 @@
|
||||||
|
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||||
|
import {$wrapNodeInElement, mergeRegister} from '@lexical/utils';
|
||||||
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
|
$createRangeSelection,
|
||||||
|
$getSelection,
|
||||||
|
$insertNodes,
|
||||||
|
$isNodeSelection,
|
||||||
|
$isRootOrShadowRoot,
|
||||||
|
$setSelection,
|
||||||
|
COMMAND_PRIORITY_EDITOR,
|
||||||
|
COMMAND_PRIORITY_HIGH,
|
||||||
|
COMMAND_PRIORITY_LOW,
|
||||||
|
createCommand,
|
||||||
|
DRAGOVER_COMMAND,
|
||||||
|
DRAGSTART_COMMAND,
|
||||||
|
DROP_COMMAND,
|
||||||
|
LexicalCommand,
|
||||||
|
LexicalEditor,
|
||||||
|
} from 'lexical';
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
$createImageNode,
|
||||||
|
$isImageNode,
|
||||||
|
ImageNode,
|
||||||
|
} from '../../nodes/ImageNode';
|
||||||
|
|
||||||
|
import {DialogActions, DialogButtonsList} from '../Input/Dialog';
|
||||||
|
import FileInput from '../Input/FileInput';
|
||||||
|
import TextInput from '../Input/TextInput';
|
||||||
|
import {Button} from "antd";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const INSERT_IMAGE_COMMAND =
|
||||||
|
createCommand('INSERT_IMAGE_COMMAND');
|
||||||
|
|
||||||
|
export function InsertImageUriDialogBody({
|
||||||
|
onClick,
|
||||||
|
}) {
|
||||||
|
const [src, setSrc] = useState('');
|
||||||
|
const [altText, setAltText] = useState('');
|
||||||
|
|
||||||
|
const isDisabled = src === '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
label="Image URL"
|
||||||
|
placeholder="i.e. https://source.unsplash.com/random"
|
||||||
|
onChange={setSrc}
|
||||||
|
value={src}
|
||||||
|
data-test-id="image-modal-url-input"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Alt Text"
|
||||||
|
placeholder="Random unsplash image"
|
||||||
|
onChange={setAltText}
|
||||||
|
value={altText}
|
||||||
|
data-test-id="image-modal-alt-text-input"
|
||||||
|
/>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
data-test-id="image-modal-confirm-btn"
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={() => onClick({altText, src})}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<FileInput
|
||||||
|
label="Image Upload"
|
||||||
|
onChange={loadImage}
|
||||||
|
accept="image/*"
|
||||||
|
data-test-id="image-modal-file-upload"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Alt Text"
|
||||||
|
placeholder="Descriptive alternative text"
|
||||||
|
onChange={setAltText}
|
||||||
|
value={altText}
|
||||||
|
data-test-id="image-modal-alt-text-input"
|
||||||
|
/>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
data-test-id="image-modal-file-upload-btn"
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={() => onClick({altText, src})}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && (
|
||||||
|
<DialogButtonsList>
|
||||||
|
<Button
|
||||||
|
data-test-id="image-modal-option-sample"
|
||||||
|
onClick={() =>
|
||||||
|
onClick(
|
||||||
|
hasModifier.current
|
||||||
|
? {
|
||||||
|
altText:
|
||||||
|
'Daylight fir trees forest glacier green high ice landscape',
|
||||||
|
src: landscapeImage,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
altText: 'Yellow flower in tilt shift lens',
|
||||||
|
src: yellowFlowerImage,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Sample
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-test-id="image-modal-option-url"
|
||||||
|
onClick={() => setMode('url')}>
|
||||||
|
URL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-test-id="image-modal-option-file"
|
||||||
|
onClick={() => setMode('file')}>
|
||||||
|
File
|
||||||
|
</Button>
|
||||||
|
</DialogButtonsList>
|
||||||
|
)}
|
||||||
|
{mode === 'url' && <InsertImageUriDialogBody onClick={onClick} />}
|
||||||
|
{mode === 'file' && <InsertImageUploadedDialogBody onClick={onClick} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import './index.less';
|
||||||
|
export function DialogButtonsList({children}) {
|
||||||
|
return <div className="DialogButtonsList">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogActions({
|
||||||
|
'data-test-id': dataTestId,
|
||||||
|
children,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="DialogActions" data-test-id={dataTestId}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<button
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
ref={ref}
|
||||||
|
title={title}
|
||||||
|
type="button">
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<DropDownContext.Provider value={contextValue}>
|
||||||
|
<div className="dropdown" ref={dropDownRef} onKeyDown={handleKeyDown}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</DropDownContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={buttonAriaLabel || buttonLabel}
|
||||||
|
className={buttonClassName}
|
||||||
|
onClick={() => setShowDropDown(!showDropDown)}
|
||||||
|
ref={buttonRef}>
|
||||||
|
{buttonIconClassName && <span className={buttonIconClassName} />}
|
||||||
|
{buttonLabel && (
|
||||||
|
<span className="text dropdown-button-text">{buttonLabel}</span>
|
||||||
|
)}
|
||||||
|
<i className="chevron-down" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showDropDown &&
|
||||||
|
createPortal(
|
||||||
|
<DropDownItems dropDownRef={dropDownRef} onClose={handleClose}>
|
||||||
|
{children}
|
||||||
|
</DropDownItems>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export default function FileInput({
|
||||||
|
accept,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
'data-test-id': dataTestId,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="Input__wrapper">
|
||||||
|
<label className="Input__label">{label}</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept={accept}
|
||||||
|
className="Input__input"
|
||||||
|
onChange={(e) => onChange(e.target.files)}
|
||||||
|
data-test-id={dataTestId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<div className="Input__wrapper">
|
||||||
|
<label className="Input__label">{label}</label>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className="Input__input"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
data-test-id={dataTestId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<DropDown
|
||||||
|
buttonClassName="toolbar-item dialog-dropdown"
|
||||||
|
buttonLabel={buttonLabel}>
|
||||||
|
{LAYOUTS.map(({label, value}) => (
|
||||||
|
<DropDownItem
|
||||||
|
key={value}
|
||||||
|
className="item"
|
||||||
|
onClick={() => setLayout(value)}>
|
||||||
|
<span className="text">{label}</span>
|
||||||
|
</DropDownItem>
|
||||||
|
))}
|
||||||
|
</DropDown>
|
||||||
|
<Button onClick={onClick}>Insert</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import {useDispatch, useSelector} from "react-redux";
|
||||||
import md5 from "md5"
|
import md5 from "md5"
|
||||||
import {message} from "antd";
|
import {message} from "antd";
|
||||||
const {ipcRenderer} = window.require('electron')
|
const {ipcRenderer} = window.require('electron')
|
||||||
export default function SaveFilePlugin(props) {
|
const SaveFilePlugin=(props)=> {
|
||||||
let activeKey = useSelector(state => state.tableBarItem.activeKey);
|
let activeKey = useSelector(state => state.tableBarItem.activeKey);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [messageApi,contextHolder] = message.useMessage();
|
const [messageApi,contextHolder] = message.useMessage();
|
||||||
|
@ -18,6 +18,9 @@ export default function SaveFilePlugin(props) {
|
||||||
const [editorState,setEditorState]=useState();
|
const [editorState,setEditorState]=useState();
|
||||||
|
|
||||||
function onChange(editorState) {
|
function onChange(editorState) {
|
||||||
|
if (isEmpty(props.filePath)){
|
||||||
|
return
|
||||||
|
}
|
||||||
if (props.filePath.endsWith(".md")){
|
if (props.filePath.endsWith(".md")){
|
||||||
let read = editorState.read(() => $convertToMarkdownString(TRANSFORMERS));
|
let read = editorState.read(() => $convertToMarkdownString(TRANSFORMERS));
|
||||||
setEditorState(read)
|
setEditorState(read)
|
||||||
|
@ -83,7 +86,7 @@ export default function SaveFilePlugin(props) {
|
||||||
if (save) {
|
if (save) {
|
||||||
overWriteFile(filePath, resultSave)
|
overWriteFile(filePath, resultSave)
|
||||||
console.log("保存成功"+ filePath)
|
console.log("保存成功"+ filePath)
|
||||||
messageApi.open({type:"success",content:"保存成功:" + filePath})
|
messageApi.open({type:"success",content:"保存成功:" + filePath,duration:1})
|
||||||
}
|
}
|
||||||
}).catch(error =>
|
}).catch(error =>
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
@ -104,8 +107,9 @@ export default function SaveFilePlugin(props) {
|
||||||
},[editor,onChange]
|
},[editor,onChange]
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
export default SaveFilePlugin
|
||||||
|
|
|
@ -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 (
|
||||||
|
<CellContext.Provider
|
||||||
|
value={useMemo(
|
||||||
|
() => ({
|
||||||
|
cellEditorConfig: contextValue.cellEditorConfig,
|
||||||
|
cellEditorPlugins: contextValue.cellEditorPlugins,
|
||||||
|
set: (cellEditorConfig, cellEditorPlugins) => {
|
||||||
|
setContextValue({cellEditorConfig, cellEditorPlugins});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[contextValue.cellEditorConfig, contextValue.cellEditorPlugins],
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</CellContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
placeholder={'# of rows (1-500)'}
|
||||||
|
label="Rows"
|
||||||
|
onChange={setRows}
|
||||||
|
value={rows}
|
||||||
|
data-test-id="table-modal-rows"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder={'# of columns (1-50)'}
|
||||||
|
label="Columns"
|
||||||
|
onChange={setColumns}
|
||||||
|
value={columns}
|
||||||
|
data-test-id="table-modal-columns"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<DialogActions data-test-id="table-model-confirm-insert">
|
||||||
|
<Button disabled={isDisabled} onClick={onClick}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
placeholder={'# of rows (1-500)'}
|
||||||
|
label="Rows"
|
||||||
|
onChange={setRows}
|
||||||
|
value={rows}
|
||||||
|
data-test-id="table-modal-rows"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder={'# of columns (1-50)'}
|
||||||
|
label="Columns"
|
||||||
|
onChange={setColumns}
|
||||||
|
value={columns}
|
||||||
|
data-test-id="table-modal-columns"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<DialogActions data-test-id="table-model-confirm-insert">
|
||||||
|
<Button disabled={isDisabled} onClick={onClick}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
||||||
const exampleTheme = {
|
const firstTheme = {
|
||||||
ltr: "ltr",
|
ltr: "ltr",
|
||||||
rtl: "rtl",
|
rtl: "rtl",
|
||||||
placeholder: "editor-placeholder",
|
placeholder: "editor-placeholder",
|
||||||
|
@ -66,5 +66,5 @@ const exampleTheme = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default exampleTheme;
|
export default firstTheme;
|
||||||
|
|
Loading…
Reference in New Issue