feat:editorConfig使用局部变量
This commit is contained in:
parent
7bb24704cf
commit
4a995d69a5
|
@ -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
|
||||
|
|
|
@ -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 {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}/>
|
||||
{/*文件操作导入文件*/}
|
||||
|
|
|
@ -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 {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
|
||||
|
|
|
@ -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",
|
||||
rtl: "rtl",
|
||||
placeholder: "editor-placeholder",
|
||||
|
@ -66,5 +66,5 @@ const exampleTheme = {
|
|||
}
|
||||
};
|
||||
|
||||
export default exampleTheme;
|
||||
export default firstTheme;
|
||||
|
Loading…
Reference in New Issue