feat:editorConfig使用局部变量

This commit is contained in:
shixiaohua 2024-02-26 09:24:03 +08:00
parent 7bb24704cf
commit 4a995d69a5
21 changed files with 4099 additions and 678 deletions

View File

@ -301,7 +301,7 @@ const ItemTree = (prop) => {
if (dirFlag){
menuItem.push(getMenuItem('1',<DirAddDir fileDir={key} />))
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"/>)]
),null, 'group'
)
@ -318,7 +318,7 @@ const ItemTree = (prop) => {
console.log('onClick',e)
}
}
// onMouseLeave={e => {setState("");}}
onMouseLeave={e => {setState("");}}
items={menuItem}>
</Menu>,
document.body

View File

@ -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];
}

View File

@ -1,24 +1,16 @@
import ExampleTheme from "./themes/ExampleTheme";
import FirstTheme from "./themes/FirstTheme";
import {LexicalComposer} from "@lexical/react/LexicalComposer";
import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
import {ContentEditable} from "@lexical/react/LexicalContentEditable";
import {HistoryPlugin} from "@lexical/react/LexicalHistoryPlugin";
import {AutoFocusPlugin} from "@lexical/react/LexicalAutoFocusPlugin";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import TreeViewPlugin from "./plugins/TreeViewPlugin";
import ToolbarPlugin from "./plugins/ToolbarPlugin";
import {HeadingNode, QuoteNode} from "@lexical/rich-text";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {ListItemNode, ListNode} from "@lexical/list";
import {CodeHighlightNode, CodeNode, $createCodeNode, $isCodeNode} from "@lexical/code";
import {AutoLinkNode, LinkNode} from "@lexical/link";
import {MarkdownShortcutPlugin} from "@lexical/react/LexicalMarkdownShortcutPlugin";
import {
TRANSFORMERS, $convertFromMarkdownString,
} from "@lexical/markdown";
import "./index.less"
import {useEffect, useState} from 'react';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {importFile} from "../../../utils/File"
import {isEmpty} from "../../../utils/ObjectUtils";
@ -30,40 +22,23 @@ import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin";
import ImportFilePlugin from "./plugins/ImportFilePlugin";
import {TablePlugin} from "@lexical/react/LexicalTablePlugin";
import SaveFilePlugin from "./plugins/SaveFilePlugin";
import {TabIndentationPlugin} from "@lexical/react/LexicalTabIndentationPlugin";
import UsefulNodes from "./nodes/UsefulNodes";
import ImagesPlugin from "./plugins/ImagesPlugin";
function Placeholder() {
return <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) {
let editorConfig = {
theme: FirstTheme,
onError(error) {
throw error;
},
nodes: UsefulNodes
};
console.log("Hlexical(props):this.props.filePath:", props.filePath)
if (props.filePath.endsWith(".md")){
if (!isEmpty(props.filePath)&&props.filePath.endsWith(".md")){
importFile(props.filePath).then(value => {
if (isEmpty(value)) {
return
@ -94,8 +69,21 @@ export default function Hlexical(props) {
<AutoLinkPlugin/>
<ListMaxIndentLevelPlugin maxDepth={7}/>
<TablePlugin/>
<TabIndentationPlugin />
{/*markdown 快捷键*/}
<MarkdownShortcutPlugin transformers={TRANSFORMERS}/>
{/*图片加载*/}
<ImagesPlugin/>
{/*目录加载*/}
{/* 表格加载 */}
{/*<TableOfContentsPlugin />*/}
{/*<LexicalTableOfContents>*/}
{/* {(tableOfContentsArray) => {*/}
{/* return <MyCustomTableOfContetsPlugin tableOfContents={tableOfContentsArray} />;*/}
{/* }}*/}
{/*</LexicalTableOfContents>*/}
<ImportFilePlugin filePath={props.filePath}/>
<SaveFilePlugin filePath={props.filePath}/>
{/*文件操作导入文件*/}

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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,
)}
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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>
</>
);
}

View File

@ -10,7 +10,7 @@ import {useDispatch, useSelector} from "react-redux";
import md5 from "md5"
import {message} from "antd";
const {ipcRenderer} = window.require('electron')
export default function SaveFilePlugin(props) {
const SaveFilePlugin=(props)=> {
let activeKey = useSelector(state => state.tableBarItem.activeKey);
const dispatch = useDispatch();
const [messageApi,contextHolder] = message.useMessage();
@ -18,6 +18,9 @@ export default function SaveFilePlugin(props) {
const [editorState,setEditorState]=useState();
function onChange(editorState) {
if (isEmpty(props.filePath)){
return
}
if (props.filePath.endsWith(".md")){
let read = editorState.read(() => $convertToMarkdownString(TRANSFORMERS));
setEditorState(read)
@ -83,7 +86,7 @@ export default function SaveFilePlugin(props) {
if (save) {
overWriteFile(filePath, resultSave)
console.log("保存成功"+ filePath)
messageApi.open({type:"success",content:"保存成功:" + filePath})
messageApi.open({type:"success",content:"保存成功:" + filePath,duration:1})
}
}).catch(error =>
console.error(error)
@ -104,8 +107,9 @@ export default function SaveFilePlugin(props) {
},[editor,onChange]
)
return (
<Fragment>
<>
{contextHolder}
</Fragment>
</>
)
}
export default SaveFilePlugin

View File

@ -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

View File

@ -1,4 +1,4 @@
const exampleTheme = {
const firstTheme = {
ltr: "ltr",
rtl: "rtl",
placeholder: "editor-placeholder",
@ -66,5 +66,5 @@ const exampleTheme = {
}
};
export default exampleTheme;
export default firstTheme;