@blocklet/editor 2.0.6 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/main/markdown-editor/editor.d.ts +14 -0
- package/lib/main/markdown-editor/editor.js +119 -0
- package/lib/main/markdown-editor/index.d.ts +17 -0
- package/lib/main/markdown-editor/index.js +47 -0
- package/lib/main/markdown-editor/nodes.d.ts +3 -0
- package/lib/main/markdown-editor/nodes.js +27 -0
- package/lib/main/markdown-editor/plugins/MediaUrlFixerPlugin.d.ts +5 -0
- package/lib/main/markdown-editor/plugins/MediaUrlFixerPlugin.js +35 -0
- package/lib/main/markdown-editor/plugins/ToolbarPlugin.d.ts +3 -0
- package/lib/main/markdown-editor/plugins/ToolbarPlugin.js +358 -0
- package/lib/main/markdown-editor/theme.d.ts +4 -0
- package/lib/main/markdown-editor/theme.js +61 -0
- package/lib/main/markdown-editor/transformers.d.ts +12 -0
- package/lib/main/markdown-editor/transformers.js +184 -0
- package/lib/main/plugins/ComponentPickerPlugin/index.js +14 -1
- package/lib/main/plugins/ImagesPlugin/index.js +2 -2
- package/package.json +3 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MutableRefObject, ReactNode } from 'react';
|
|
2
|
+
import { LexicalEditor } from 'lexical';
|
|
3
|
+
export interface EditorProps {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
placeholder?: ReactNode;
|
|
6
|
+
onChange?: (markdown: string) => void;
|
|
7
|
+
autoFocus?: boolean;
|
|
8
|
+
showToolbar?: boolean;
|
|
9
|
+
editorRef?: React.RefCallback<LexicalEditor> | MutableRefObject<LexicalEditor | null>;
|
|
10
|
+
onReady?: () => void;
|
|
11
|
+
enableHeadingsIdPlugin?: boolean;
|
|
12
|
+
mediaUrlPrefix?: string;
|
|
13
|
+
}
|
|
14
|
+
export default function Editor({ children, placeholder, onChange, autoFocus, showToolbar, editorRef, onReady, enableHeadingsIdPlugin, mediaUrlPrefix, }: EditorProps): JSX.Element;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '@emotion/css';
|
|
3
|
+
import styled from '@emotion/styled';
|
|
4
|
+
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
|
5
|
+
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
|
|
6
|
+
import { ClearEditorPlugin } from '@lexical/react/LexicalClearEditorPlugin';
|
|
7
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
8
|
+
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
|
|
9
|
+
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
|
10
|
+
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
|
11
|
+
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
12
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
13
|
+
import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin';
|
|
14
|
+
import { useEffect, useState } from 'react';
|
|
15
|
+
import { $convertToMarkdownString } from '@lexical/markdown';
|
|
16
|
+
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
|
17
|
+
import useHasNodes from '../hooks/useHasNodes';
|
|
18
|
+
import AutoLinkPlugin from '../plugins/AutoLinkPlugin';
|
|
19
|
+
import ClickableLinkPlugin from '../plugins/ClickableLinkPlugin';
|
|
20
|
+
import CodeActionMenuPlugin from '../plugins/CodeActionMenuPlugin';
|
|
21
|
+
import CodeHighlightPlugin from '../plugins/CodeHighlightPlugin';
|
|
22
|
+
import DragDropPaste from '../plugins/DragDropPastePlugin';
|
|
23
|
+
import DraggableBlockPlugin from '../plugins/DraggableBlockPlugin';
|
|
24
|
+
import FloatingLinkEditorPlugin from '../plugins/FloatingLinkEditorPlugin';
|
|
25
|
+
import FloatingTextFormatToolbarPlugin from '../plugins/FloatingTextFormatToolbarPlugin';
|
|
26
|
+
import HorizontalRulePlugin from '../plugins/HorizontalRulePlugin';
|
|
27
|
+
import ImagesPlugin from '../plugins/ImagesPlugin';
|
|
28
|
+
import ListMaxIndentLevelPlugin from '../plugins/ListMaxIndentLevelPlugin';
|
|
29
|
+
import MarkdownShortcutPlugin from '../plugins/MarkdownShortcutPlugin';
|
|
30
|
+
import TabFocusPlugin from '../plugins/TabFocusPlugin';
|
|
31
|
+
import ContentEditable from '../ui/ContentEditable';
|
|
32
|
+
import Placeholder from '../ui/Placeholder';
|
|
33
|
+
import { useEditorConfig } from '../../config';
|
|
34
|
+
import SelectBlockPlugin from '../../ext/SelectBlockPlugin';
|
|
35
|
+
import RemoveListPlugin from '../../ext/RemoveListPlugin';
|
|
36
|
+
import MarkdownHeadTextPlugin from '../../ext/HeadTextPlugin';
|
|
37
|
+
import { EditorRefPlugin } from '../../ext/LexicalEditorRefPlugin';
|
|
38
|
+
import { HeadingsIdPlugin } from '../../ext/HeadingsIdPlugin';
|
|
39
|
+
import { EditorReadyPlugin } from '../../ext/EditorReadyPlugin';
|
|
40
|
+
import { BlurTextPlugin } from '../../ext/BlurTextPlugin';
|
|
41
|
+
import ToolbarPlugin from './plugins/ToolbarPlugin';
|
|
42
|
+
import { TRANSFORMERS, IMAGE } from './transformers';
|
|
43
|
+
import { MediaUrlFixerPlugin } from './plugins/MediaUrlFixerPlugin';
|
|
44
|
+
export default function Editor({ children, placeholder, onChange, autoFocus = true, showToolbar = true, editorRef, onReady, enableHeadingsIdPlugin, mediaUrlPrefix, }) {
|
|
45
|
+
const [editor] = useLexicalComposerContext();
|
|
46
|
+
const [editable, setEditable] = useState(false);
|
|
47
|
+
const config = useEditorConfig();
|
|
48
|
+
const hasUploader = !!config.uploader;
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setEditable(editor.isEditable());
|
|
51
|
+
return editor.registerEditableListener((e) => {
|
|
52
|
+
setEditable(e);
|
|
53
|
+
});
|
|
54
|
+
}, [editor]);
|
|
55
|
+
const hasNodes = useHasNodes();
|
|
56
|
+
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
|
|
57
|
+
const onRef = (_floatingAnchorElem) => {
|
|
58
|
+
if (_floatingAnchorElem !== null) {
|
|
59
|
+
setFloatingAnchorElem(_floatingAnchorElem);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (hasNodes('image') && !hasUploader) {
|
|
64
|
+
console.warn('You need to configure uploader to use ImagesPlugin');
|
|
65
|
+
}
|
|
66
|
+
}, [hasNodes('image'), hasUploader]);
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
68
|
+
const handleOnChange = (editorState, editor) => {
|
|
69
|
+
if (onChange) {
|
|
70
|
+
editor.update(() => {
|
|
71
|
+
const markdown = $convertToMarkdownString(TRANSFORMERS);
|
|
72
|
+
onChange(markdown);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return (_jsxs(_Fragment, { children: [editable && showToolbar && _jsx(ToolbarPlugin, {}), hasNodes('image') && hasUploader && _jsx(DragDropPaste, {}), autoFocus && _jsx(AutoFocusPlugin, { defaultSelection: "rootEnd" }), _jsx(ClearEditorPlugin, {}), hasNodes('autolink') && _jsx(AutoLinkPlugin, {}), _jsx(EditorContent, { ref: onRef, className: cx('be-content', editable && 'editable'), children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cx('be-editable', 'notranslate') }), placeholder: _jsx(Placeholder, { className: "be-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), _jsx(MarkdownShortcutPlugin, {}), _jsx(TabIndentationPlugin, {}), hasNodes('code', 'code-highlight') && _jsx(CodeHighlightPlugin, {}), hasNodes('list', 'listitem') && _jsx(ListPlugin, {}), hasNodes('list', 'listitem') && _jsx(CheckListPlugin, {}), hasNodes('list', 'listitem') && _jsx(ListMaxIndentLevelPlugin, { maxDepth: 7 }), hasNodes('image') && hasUploader && _jsx(ImagesPlugin, {}), hasNodes('link') && _jsx(LinkPlugin, {}), !editable && _jsx(BlurTextPlugin, {}), hasNodes('link') && _jsx(ClickableLinkPlugin, {}), hasNodes('horizontalrule') && _jsx(HorizontalRulePlugin, {}), _jsx(TabFocusPlugin, {}), onChange && _jsx(OnChangePlugin, { onChange: handleOnChange }), floatingAnchorElem && editable && (_jsxs(_Fragment, { children: [_jsx(DraggableBlockPlugin, { anchorElem: floatingAnchorElem }), hasNodes('code') && _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem }), hasNodes('link') && _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem })] })), enableHeadingsIdPlugin && _jsx(HeadingsIdPlugin, {}), mediaUrlPrefix && _jsx(MediaUrlFixerPlugin, { mediaUrlPrefix: mediaUrlPrefix, transformers: [IMAGE] }), _jsx(SelectBlockPlugin, {}), _jsx(RemoveListPlugin, {}), _jsx(MarkdownHeadTextPlugin, {}), editorRef && _jsx(EditorRefPlugin, { editorRef: editorRef }), onReady && _jsx(EditorReadyPlugin, { onReady: onReady }), hasNodes('table', 'tablerow', 'tablecell') && editable && _jsx(TablePlugin, {}), children] }));
|
|
77
|
+
}
|
|
78
|
+
const EditorContent = styled.div `
|
|
79
|
+
position: relative;
|
|
80
|
+
.be-editable {
|
|
81
|
+
code {
|
|
82
|
+
white-space: pre-wrap;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&.editable {
|
|
87
|
+
> .be-editable {
|
|
88
|
+
padding: 8px 24px;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
> .be-editable {
|
|
93
|
+
hr {
|
|
94
|
+
padding: 2px 2px;
|
|
95
|
+
border: none;
|
|
96
|
+
margin: 1em 0;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
hr:after {
|
|
101
|
+
content: '';
|
|
102
|
+
display: block;
|
|
103
|
+
height: 2px;
|
|
104
|
+
background-color: #ccc;
|
|
105
|
+
line-height: 2px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hr.selected {
|
|
109
|
+
outline: 2px solid rgb(60, 132, 244);
|
|
110
|
+
user-select: none;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 增大最外层 list item 的 marginLeft, https://github.com/blocklet/discuss-kit/issues/2123
|
|
114
|
+
> ol > li,
|
|
115
|
+
> ul > li {
|
|
116
|
+
margin-left: 32px;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LexicalEditor } from 'lexical';
|
|
2
|
+
import React, { MutableRefObject, ReactNode } from 'react';
|
|
3
|
+
export interface MarkdownEditorProps extends Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'placeholder' | 'onChange'> {
|
|
4
|
+
placeholder?: ReactNode;
|
|
5
|
+
editable?: boolean;
|
|
6
|
+
editorState?: string;
|
|
7
|
+
initialContent?: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
onChange?: (markdown: string) => void;
|
|
10
|
+
autoFocus?: boolean;
|
|
11
|
+
showToolbar?: boolean;
|
|
12
|
+
editorRef?: React.RefCallback<LexicalEditor> | MutableRefObject<LexicalEditor | null>;
|
|
13
|
+
onReady?: () => void;
|
|
14
|
+
enableHeadingsIdPlugin?: boolean;
|
|
15
|
+
mediaUrlPrefix?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function MarkdownEditor({ editorState, editable, ...props }: MarkdownEditorProps): JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '@emotion/css';
|
|
3
|
+
import styled from '@emotion/styled';
|
|
4
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
5
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
6
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
7
|
+
import { $convertFromMarkdownString } from '@lexical/markdown';
|
|
8
|
+
import Editor from './editor';
|
|
9
|
+
import nodes from './nodes';
|
|
10
|
+
import { useCustomTheme } from '../themes/customTheme';
|
|
11
|
+
import { MarkdownEditorThemeProvider } from './theme';
|
|
12
|
+
import { TRANSFORMERS } from './transformers';
|
|
13
|
+
export function MarkdownEditor({ editorState, editable = true, ...props }) {
|
|
14
|
+
const theme = useCustomTheme();
|
|
15
|
+
const initialConfig = {
|
|
16
|
+
namespace: 'MarkdownEditor',
|
|
17
|
+
editorState: editorState ? () => $convertFromMarkdownString(editorState, TRANSFORMERS) : undefined,
|
|
18
|
+
nodes,
|
|
19
|
+
editable,
|
|
20
|
+
onError: (error) => {
|
|
21
|
+
throw error;
|
|
22
|
+
},
|
|
23
|
+
theme,
|
|
24
|
+
};
|
|
25
|
+
return (_jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(EditorShell, { ...props, editable: editable }) }));
|
|
26
|
+
}
|
|
27
|
+
function EditorShell({ placeholder, children, editable, onChange, autoFocus, showToolbar = true, editorRef, onReady, enableHeadingsIdPlugin, mediaUrlPrefix, ...props }) {
|
|
28
|
+
const [editor] = useLexicalComposerContext();
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
editor.setEditable(editable ?? true);
|
|
31
|
+
}, [editable]);
|
|
32
|
+
const shellRef = useRef(null);
|
|
33
|
+
const onShellClick = useCallback((e) => {
|
|
34
|
+
if (e.target === shellRef.current) {
|
|
35
|
+
editor.focus();
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
return (_jsx(MarkdownEditorThemeProvider, { children: _jsx(EditorRoot, { ...props, className: cx(props.className, 'be-shell'), ref: shellRef, onClick: onShellClick, children: _jsx(Editor, { onChange: onChange, placeholder: placeholder, autoFocus: autoFocus, showToolbar: showToolbar, editorRef: editorRef, onReady: onReady, enableHeadingsIdPlugin: enableHeadingsIdPlugin, mediaUrlPrefix: mediaUrlPrefix, children: children }) }) }));
|
|
39
|
+
}
|
|
40
|
+
const EditorRoot = styled.div `
|
|
41
|
+
margin: 0 auto;
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
position: relative;
|
|
44
|
+
line-height: 1.7;
|
|
45
|
+
font-weight: 400;
|
|
46
|
+
text-align: left;
|
|
47
|
+
`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CodeHighlightNode, CodeNode } from '@lexical/code';
|
|
2
|
+
import { AutoLinkNode, LinkNode } from '@lexical/link';
|
|
3
|
+
import { ListItemNode, ListNode } from '@lexical/list';
|
|
4
|
+
import { MarkNode } from '@lexical/mark';
|
|
5
|
+
import { OverflowNode } from '@lexical/overflow';
|
|
6
|
+
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
7
|
+
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
8
|
+
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
|
|
9
|
+
import { ImageNode } from '../nodes/ImageNode';
|
|
10
|
+
const MarkdownEditorNodes = [
|
|
11
|
+
HeadingNode,
|
|
12
|
+
ListNode,
|
|
13
|
+
ListItemNode,
|
|
14
|
+
QuoteNode,
|
|
15
|
+
CodeNode,
|
|
16
|
+
TableNode,
|
|
17
|
+
TableCellNode,
|
|
18
|
+
TableRowNode,
|
|
19
|
+
CodeHighlightNode,
|
|
20
|
+
AutoLinkNode,
|
|
21
|
+
LinkNode,
|
|
22
|
+
OverflowNode,
|
|
23
|
+
ImageNode,
|
|
24
|
+
HorizontalRuleNode,
|
|
25
|
+
MarkNode,
|
|
26
|
+
];
|
|
27
|
+
export default MarkdownEditorNodes;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import joinUrl from 'url-join';
|
|
4
|
+
import { $isImageNode, ImageNode } from '../../nodes/ImageNode';
|
|
5
|
+
export function MediaUrlFixerPlugin({ mediaUrlPrefix, transformers, }) {
|
|
6
|
+
const [editor] = useLexicalComposerContext();
|
|
7
|
+
const ensureLeadingSlash = (str) => (str.startsWith('/') ? str : `/${str}`);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
transformers?.forEach((transformer) => {
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
transformer.export = (node) => {
|
|
12
|
+
if (!$isImageNode(node)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
let src = node.getSrc();
|
|
16
|
+
if (src?.startsWith(mediaUrlPrefix)) {
|
|
17
|
+
src = ensureLeadingSlash(src.replace(mediaUrlPrefix, ''));
|
|
18
|
+
}
|
|
19
|
+
return ``;
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}, []);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
function imageNodeTransform(node) {
|
|
25
|
+
const targetNode = node;
|
|
26
|
+
const src = targetNode.getSrc();
|
|
27
|
+
// 仅处理以 "/" 为前缀的 image src, 并且避免重复处理
|
|
28
|
+
if (src?.startsWith('/') && !src.startsWith(mediaUrlPrefix)) {
|
|
29
|
+
targetNode.setSrc(joinUrl(mediaUrlPrefix, src));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return editor.registerNodeTransform(ImageNode, imageNodeTransform);
|
|
33
|
+
}, [editor]);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { $createCodeNode, $isCodeNode, CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP, getLanguageFriendlyName, } from '@lexical/code';
|
|
4
|
+
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
|
5
|
+
import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode, REMOVE_LIST_COMMAND, } from '@lexical/list';
|
|
6
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
7
|
+
import { $createHeadingNode, $createQuoteNode, $isHeadingNode } from '@lexical/rich-text';
|
|
8
|
+
import { $setBlocksType } from '@lexical/selection';
|
|
9
|
+
import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
|
|
10
|
+
import { $createParagraphNode, $getNodeByKey, $getSelection, $isRangeSelection, $isRootOrShadowRoot, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_CRITICAL, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical';
|
|
11
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
12
|
+
import { IS_APPLE } from '../../../shared/environment';
|
|
13
|
+
import useHasNodes from '../../hooks/useHasNodes';
|
|
14
|
+
import useModal from '../../hooks/useModal';
|
|
15
|
+
import DropDown, { DropDownItem } from '../../ui/DropDown';
|
|
16
|
+
import { getSelectedNode } from '../../utils/getSelectedNode';
|
|
17
|
+
import { sanitizeUrl } from '../../utils/sanitizeUrl';
|
|
18
|
+
import { INSERT_COMPONENT_COMMAND } from '../../plugins/ComponentPickerPlugin';
|
|
19
|
+
import { INSERT_IMAGE_COMMAND, isImage } from '../../plugins/ImagesPlugin';
|
|
20
|
+
import { InsertTableDialog } from '../../plugins/TablePlugin';
|
|
21
|
+
export const uploadFile = (editor) => {
|
|
22
|
+
if (window?.uploaderRef) {
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
const uploader = window?.uploaderRef?.current?.getUploader();
|
|
25
|
+
uploader.open();
|
|
26
|
+
uploader.onUploadSuccess(({ file, response }) => {
|
|
27
|
+
// missing the source: function-upload-file
|
|
28
|
+
if (file.source !== 'function-upload-file') {
|
|
29
|
+
const { data } = response;
|
|
30
|
+
if (data?.filename) {
|
|
31
|
+
if (isImage(data.filename) || isImage(data.originalname)) {
|
|
32
|
+
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
|
|
33
|
+
src: data.url,
|
|
34
|
+
altText: data.originalname,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
throw new Error('Missing required `window.uploaderRef`');
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const blockTypeToBlockName = {
|
|
46
|
+
bullet: 'Bulleted List',
|
|
47
|
+
check: 'Check List',
|
|
48
|
+
code: 'Code Block',
|
|
49
|
+
h1: 'Heading 1',
|
|
50
|
+
h2: 'Heading 2',
|
|
51
|
+
h3: 'Heading 3',
|
|
52
|
+
h4: 'Heading 4',
|
|
53
|
+
h5: 'Heading 5',
|
|
54
|
+
h6: 'Heading 6',
|
|
55
|
+
number: 'Numbered List',
|
|
56
|
+
paragraph: 'Normal',
|
|
57
|
+
quote: 'Quote',
|
|
58
|
+
};
|
|
59
|
+
function getCodeLanguageOptions() {
|
|
60
|
+
const options = [];
|
|
61
|
+
for (const [lang, friendlyName] of Object.entries(CODE_LANGUAGE_FRIENDLY_NAME_MAP)) {
|
|
62
|
+
options.push([lang, friendlyName]);
|
|
63
|
+
}
|
|
64
|
+
return options;
|
|
65
|
+
}
|
|
66
|
+
const CODE_LANGUAGE_OPTIONS = getCodeLanguageOptions();
|
|
67
|
+
function dropDownActiveClass(active) {
|
|
68
|
+
if (active)
|
|
69
|
+
return 'active dropdown-item-active';
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
function BlockFormatDropDown({ editor, blockType, disabled = false, }) {
|
|
73
|
+
const hasNodes = useHasNodes();
|
|
74
|
+
const formatParagraph = () => {
|
|
75
|
+
editor.update(() => {
|
|
76
|
+
const selection = $getSelection();
|
|
77
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const formatHeading = (headingSize) => {
|
|
81
|
+
if (blockType !== headingSize) {
|
|
82
|
+
editor.update(() => {
|
|
83
|
+
const selection = $getSelection();
|
|
84
|
+
$setBlocksType(selection, () => $createHeadingNode(headingSize));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const formatBulletList = () => {
|
|
89
|
+
if (blockType !== 'bullet') {
|
|
90
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const formatCheckList = () => {
|
|
97
|
+
if (blockType !== 'check') {
|
|
98
|
+
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const formatNumberedList = () => {
|
|
105
|
+
if (blockType !== 'number') {
|
|
106
|
+
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const formatQuote = () => {
|
|
113
|
+
if (blockType !== 'quote') {
|
|
114
|
+
editor.update(() => {
|
|
115
|
+
const selection = $getSelection();
|
|
116
|
+
$setBlocksType(selection, () => $createQuoteNode());
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const formatCode = () => {
|
|
121
|
+
if (blockType !== 'code') {
|
|
122
|
+
editor.update(() => {
|
|
123
|
+
let selection = $getSelection();
|
|
124
|
+
if (selection !== null) {
|
|
125
|
+
if (selection.isCollapsed()) {
|
|
126
|
+
$setBlocksType(selection, () => $createCodeNode());
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const textContent = selection.getTextContent();
|
|
130
|
+
const codeNode = $createCodeNode();
|
|
131
|
+
selection.insertNodes([codeNode]);
|
|
132
|
+
selection = $getSelection();
|
|
133
|
+
if ($isRangeSelection(selection))
|
|
134
|
+
selection.insertRawText(textContent);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const icons = {
|
|
141
|
+
paragraph: 'tabler:align-justified',
|
|
142
|
+
h1: 'tabler:h-1',
|
|
143
|
+
h2: 'tabler:h-2',
|
|
144
|
+
h3: 'tabler:h-3',
|
|
145
|
+
bullet: 'tabler:list',
|
|
146
|
+
number: 'tabler:list-numbers',
|
|
147
|
+
check: 'tabler:list-details',
|
|
148
|
+
quote: 'tabler:quote',
|
|
149
|
+
code: 'tabler:code',
|
|
150
|
+
};
|
|
151
|
+
return (_jsxs(DropDown, { disabled: disabled, buttonClassName: "toolbar-item", buttonIconClassName: "iconify", buttonIconData: icons[blockType], buttonLabel: blockTypeToBlockName[blockType], buttonAriaLabel: "Formatting options for text style", children: [_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'paragraph')}`, onClick: formatParagraph, children: [_jsx("i", { className: "iconify", "data-icon": icons.paragraph }), _jsx("span", { className: "text", children: "Normal" })] }), hasNodes('heading') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'h1')}`, onClick: () => formatHeading('h1'), children: [_jsx("i", { className: "iconify", "data-icon": icons.h1 }), _jsx("span", { className: "text", children: "Heading 1" })] })), hasNodes('heading') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'h2')}`, onClick: () => formatHeading('h2'), children: [_jsx("i", { className: "iconify", "data-icon": icons.h2 }), _jsx("span", { className: "text", children: "Heading 2" })] })), hasNodes('heading') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'h3')}`, onClick: () => formatHeading('h3'), children: [_jsx("i", { className: "iconify", "data-icon": icons.h3 }), _jsx("span", { className: "text", children: "Heading 3" })] })), hasNodes('list') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'bullet')}`, onClick: formatBulletList, children: [_jsx("i", { className: "iconify", "data-icon": icons.bullet }), _jsx("span", { className: "text", children: "Bullet List" })] })), hasNodes('list') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'number')}`, onClick: formatNumberedList, children: [_jsx("i", { className: "iconify", "data-icon": icons.number }), _jsx("span", { className: "text", children: "Numbered List" })] })), hasNodes('list') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'check')}`, onClick: formatCheckList, children: [_jsx("i", { className: "iconify", "data-icon": icons.check }), _jsx("span", { className: "text", children: "Check List" })] })), hasNodes('quote') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'quote')}`, onClick: formatQuote, children: [_jsx("i", { className: "iconify", "data-icon": icons.quote }), _jsx("span", { className: "text", children: "Quote" })] })), hasNodes('code') && (_jsxs(DropDownItem, { className: `item ${dropDownActiveClass(blockType === 'code')}`, onClick: formatCode, children: [_jsx("i", { className: "iconify", "data-icon": icons.code }), _jsx("span", { className: "text", children: "Code Block" })] }))] }));
|
|
152
|
+
}
|
|
153
|
+
export default function ToolbarPlugin() {
|
|
154
|
+
const [editor] = useLexicalComposerContext();
|
|
155
|
+
const [activeEditor, setActiveEditor] = useState(editor);
|
|
156
|
+
const [blockType, setBlockType] = useState('paragraph');
|
|
157
|
+
const [selectedElementKey, setSelectedElementKey] = useState(null);
|
|
158
|
+
const [isLink, setIsLink] = useState(false);
|
|
159
|
+
const [isBold, setIsBold] = useState(false);
|
|
160
|
+
const [isItalic, setIsItalic] = useState(false);
|
|
161
|
+
const [isCode, setIsCode] = useState(false);
|
|
162
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
163
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
164
|
+
const [modal, showModal] = useModal();
|
|
165
|
+
const [codeLanguage, setCodeLanguage] = useState('');
|
|
166
|
+
const [isEditable, setIsEditable] = useState(() => editor.isEditable());
|
|
167
|
+
const hasNodes = useHasNodes();
|
|
168
|
+
const updateToolbar = useCallback(() => {
|
|
169
|
+
const selection = $getSelection();
|
|
170
|
+
if ($isRangeSelection(selection)) {
|
|
171
|
+
const anchorNode = selection.anchor.getNode();
|
|
172
|
+
let element = anchorNode.getKey() === 'root'
|
|
173
|
+
? anchorNode
|
|
174
|
+
: $findMatchingParent(anchorNode, (e) => {
|
|
175
|
+
const parent = e.getParent();
|
|
176
|
+
return parent !== null && $isRootOrShadowRoot(parent);
|
|
177
|
+
});
|
|
178
|
+
if (element === null) {
|
|
179
|
+
element = anchorNode.getTopLevelElementOrThrow();
|
|
180
|
+
}
|
|
181
|
+
const elementKey = element.getKey();
|
|
182
|
+
const elementDOM = activeEditor.getElementByKey(elementKey);
|
|
183
|
+
// Update text format
|
|
184
|
+
setIsBold(selection.hasFormat('bold'));
|
|
185
|
+
setIsItalic(selection.hasFormat('italic'));
|
|
186
|
+
setIsCode(selection.hasFormat('code'));
|
|
187
|
+
// Update links
|
|
188
|
+
const node = getSelectedNode(selection);
|
|
189
|
+
const parent = node.getParent();
|
|
190
|
+
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
191
|
+
setIsLink(true);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
setIsLink(false);
|
|
195
|
+
}
|
|
196
|
+
if (elementDOM !== null) {
|
|
197
|
+
setSelectedElementKey(elementKey);
|
|
198
|
+
if ($isListNode(element)) {
|
|
199
|
+
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
|
|
200
|
+
const type = parentList ? parentList.getListType() : element.getListType();
|
|
201
|
+
setBlockType(type);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
|
|
205
|
+
if (type in blockTypeToBlockName) {
|
|
206
|
+
setBlockType(type);
|
|
207
|
+
}
|
|
208
|
+
if ($isCodeNode(element)) {
|
|
209
|
+
const language = element.getLanguage();
|
|
210
|
+
setCodeLanguage(language ? CODE_LANGUAGE_MAP[language] || language : '');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}, [activeEditor]);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
return editor.registerCommand(SELECTION_CHANGE_COMMAND, (_payload, newEditor) => {
|
|
218
|
+
updateToolbar();
|
|
219
|
+
setActiveEditor(newEditor);
|
|
220
|
+
return false;
|
|
221
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
222
|
+
}, [editor, updateToolbar]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
return mergeRegister(editor.registerEditableListener((editable) => {
|
|
225
|
+
setIsEditable(editable);
|
|
226
|
+
}), activeEditor.registerUpdateListener(({ editorState }) => {
|
|
227
|
+
editorState.read(() => {
|
|
228
|
+
updateToolbar();
|
|
229
|
+
});
|
|
230
|
+
}), activeEditor.registerCommand(CAN_UNDO_COMMAND, (payload) => {
|
|
231
|
+
setCanUndo(payload);
|
|
232
|
+
return false;
|
|
233
|
+
}, COMMAND_PRIORITY_CRITICAL), activeEditor.registerCommand(CAN_REDO_COMMAND, (payload) => {
|
|
234
|
+
setCanRedo(payload);
|
|
235
|
+
return false;
|
|
236
|
+
}, COMMAND_PRIORITY_CRITICAL));
|
|
237
|
+
}, [activeEditor, editor, updateToolbar]);
|
|
238
|
+
const insertLink = useCallback(() => {
|
|
239
|
+
if (!isLink) {
|
|
240
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://'));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
244
|
+
}
|
|
245
|
+
}, [editor, isLink]);
|
|
246
|
+
const onCodeLanguageSelect = useCallback((value) => {
|
|
247
|
+
activeEditor.update(() => {
|
|
248
|
+
if (selectedElementKey !== null) {
|
|
249
|
+
const node = $getNodeByKey(selectedElementKey);
|
|
250
|
+
if ($isCodeNode(node)) {
|
|
251
|
+
node.setLanguage(value);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}, [activeEditor, selectedElementKey]);
|
|
256
|
+
const items = [
|
|
257
|
+
// 'component',
|
|
258
|
+
'block',
|
|
259
|
+
'bold',
|
|
260
|
+
'italic',
|
|
261
|
+
'media',
|
|
262
|
+
'table',
|
|
263
|
+
'code',
|
|
264
|
+
'link',
|
|
265
|
+
];
|
|
266
|
+
const menus = [];
|
|
267
|
+
for (const item of items) {
|
|
268
|
+
switch (item) {
|
|
269
|
+
case 'component': {
|
|
270
|
+
menus.push(_jsx(FormatButton, { disabled: !isEditable, onClick: () => {
|
|
271
|
+
// add '/' to editor
|
|
272
|
+
editor.dispatchCommand(INSERT_COMPONENT_COMMAND, '/');
|
|
273
|
+
}, className: "toolbar-item spaced", title: "Component Picker", "aria-label": "Open component picker", children: _jsx("i", { className: "iconify", "data-icon": "tabler:circle-plus" }) }, item));
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case 'block': {
|
|
277
|
+
if (blockType in blockTypeToBlockName && activeEditor === editor) {
|
|
278
|
+
menus.push(_jsx(BlockFormatDropDown, { disabled: !isEditable, blockType: blockType, editor: editor }, "block")
|
|
279
|
+
// <Divider key="block-divider" />
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (blockType === 'code') {
|
|
283
|
+
menus.push(_jsx(DropDown, { disabled: !isEditable, buttonClassName: "toolbar-item code-language", buttonLabel: getLanguageFriendlyName(codeLanguage), buttonAriaLabel: "Select language", children: CODE_LANGUAGE_OPTIONS.map(([value, name]) => {
|
|
284
|
+
return (_jsx(DropDownItem, { className: `item ${dropDownActiveClass(value === codeLanguage)}`, onClick: () => onCodeLanguageSelect(value), children: _jsx("span", { className: "text", children: name }) }, value));
|
|
285
|
+
}) }, "block-code"));
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case 'bold': {
|
|
290
|
+
if (blockType !== 'code') {
|
|
291
|
+
menus.push(_jsx(FormatButton, { disabled: !isEditable, onClick: () => {
|
|
292
|
+
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
|
293
|
+
}, className: `toolbar-item spaced ${isBold ? 'active' : ''}`, title: IS_APPLE ? 'Bold (⌘B)' : 'Bold (Ctrl+B)', "aria-label": `Format text as bold. Shortcut: ${IS_APPLE ? '⌘B' : 'Ctrl+B'}`, children: _jsx("i", { className: "iconify", "data-icon": "tabler:bold" }) }, item));
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 'italic': {
|
|
298
|
+
if (blockType !== 'code') {
|
|
299
|
+
menus.push(_jsx(FormatButton, { disabled: !isEditable, onClick: () => {
|
|
300
|
+
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
|
301
|
+
}, className: `toolbar-item spaced ${isItalic ? 'active' : ''}`, title: IS_APPLE ? 'Italic (⌘I)' : 'Italic (Ctrl+I)', "aria-label": `Format text as italics. Shortcut: ${IS_APPLE ? '⌘I' : 'Ctrl+I'}`, children: _jsx("i", { className: "iconify", "data-icon": "tabler:italic" }) }, item));
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case 'code': {
|
|
306
|
+
if (blockType !== 'code') {
|
|
307
|
+
menus.push(_jsx(FormatButton, { disabled: !isEditable, onClick: () => {
|
|
308
|
+
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
|
309
|
+
}, className: `toolbar-item toolbar-item-code ${isCode ? 'active' : ''}`, title: "Insert code block", "aria-label": "Insert code block", children: _jsx("i", { className: "iconify", "data-icon": "tabler:code" }) }, item));
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'link': {
|
|
314
|
+
if (hasNodes('link') && blockType !== 'code') {
|
|
315
|
+
menus.push(_jsx(FormatButton, { disabled: !isEditable, onClick: insertLink, className: `toolbar-item spaced toolbar-item-link ${isLink ? 'active' : ''}`, "aria-label": "Insert link", title: "Insert link", children: _jsx("i", { className: "iconify", "data-icon": "tabler:link" }) }, item));
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'media': {
|
|
320
|
+
if (blockType !== 'code') {
|
|
321
|
+
menus.push(_jsx(FormatButton, { disabled: !isEditable, onClick: () => {
|
|
322
|
+
uploadFile(editor);
|
|
323
|
+
}, className: `toolbar-item spaced ${isCode ? 'active' : ''}`, title: "Upload Image", "aria-label": "Upload Image", children: _jsx("i", { className: "iconify", "data-icon": "tabler:photo" }) }, item));
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 'table': {
|
|
328
|
+
if (hasNodes('link') && blockType !== 'code') {
|
|
329
|
+
menus.push(_jsx(FormatButton, { disabled: !isEditable, onClick: () => {
|
|
330
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
|
331
|
+
showModal('Insert Table', (onClose) => _jsx(InsertTableDialog, { activeEditor: editor, onClose: onClose }));
|
|
332
|
+
}, className: "toolbar-item spaced toolbar-item-table", "aria-label": "Insert table", title: "Insert table", children: _jsx("i", { className: "iconify", "data-icon": "tabler:table" }) }, item));
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
default:
|
|
337
|
+
console.warn(`Unsupported toolbar item ${item}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (menus.length === 0) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return (_jsxs(Toolbar, { className: "toolbar", children: [menus, modal] }));
|
|
344
|
+
}
|
|
345
|
+
const Toolbar = styled.div `
|
|
346
|
+
z-index: 10;
|
|
347
|
+
position: sticky;
|
|
348
|
+
top: 0;
|
|
349
|
+
height: auto;
|
|
350
|
+
`;
|
|
351
|
+
const FormatButton = styled.button `
|
|
352
|
+
display: flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
font-size: 20px;
|
|
355
|
+
.iconify {
|
|
356
|
+
opacity: 0.6;
|
|
357
|
+
}
|
|
358
|
+
`;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ThemeProvider, useTheme } from '@mui/material/styles';
|
|
3
|
+
import { create } from '@arcblock/ux/lib/Theme';
|
|
4
|
+
const typography = {
|
|
5
|
+
h1: {
|
|
6
|
+
fontSize: '1.875rem',
|
|
7
|
+
lineHeight: 1.2,
|
|
8
|
+
fontWeight: 800,
|
|
9
|
+
letterSpacing: '-.025em',
|
|
10
|
+
},
|
|
11
|
+
h2: {
|
|
12
|
+
fontSize: '1.5rem',
|
|
13
|
+
lineHeight: 1.3333333,
|
|
14
|
+
fontWeight: 700,
|
|
15
|
+
letterSpacing: '-.025em',
|
|
16
|
+
},
|
|
17
|
+
h3: {
|
|
18
|
+
fontSize: '1.25rem',
|
|
19
|
+
lineHeight: 1.4,
|
|
20
|
+
fontWeight: 600,
|
|
21
|
+
letterSpacing: '-.025em',
|
|
22
|
+
},
|
|
23
|
+
h4: {
|
|
24
|
+
fontSize: '1.125rem',
|
|
25
|
+
lineHeight: 1.5,
|
|
26
|
+
fontWeight: 600,
|
|
27
|
+
},
|
|
28
|
+
h5: {
|
|
29
|
+
fontSize: '1rem',
|
|
30
|
+
lineHeight: 1.75,
|
|
31
|
+
fontWeight: 400,
|
|
32
|
+
},
|
|
33
|
+
h6: {
|
|
34
|
+
fontSize: '1rem',
|
|
35
|
+
lineHeight: 1.75,
|
|
36
|
+
fontWeight: 400,
|
|
37
|
+
},
|
|
38
|
+
subtitle1: {
|
|
39
|
+
fontSize: '1rem',
|
|
40
|
+
lineHeight: 1.75,
|
|
41
|
+
fontWeight: 400,
|
|
42
|
+
},
|
|
43
|
+
subtitle2: {
|
|
44
|
+
fontSize: '1rem',
|
|
45
|
+
lineHeight: 1.75,
|
|
46
|
+
fontWeight: 400,
|
|
47
|
+
},
|
|
48
|
+
body1: {
|
|
49
|
+
fontSize: '1rem',
|
|
50
|
+
lineHeight: 1.75,
|
|
51
|
+
},
|
|
52
|
+
fontWeightLight: 300,
|
|
53
|
+
fontWeightRegular: 400,
|
|
54
|
+
fontWeightMedium: 500,
|
|
55
|
+
fontWeightBold: 700,
|
|
56
|
+
};
|
|
57
|
+
export function MarkdownEditorThemeProvider({ children }) {
|
|
58
|
+
const theme = useTheme();
|
|
59
|
+
const merged = create({ ...theme, typography });
|
|
60
|
+
return _jsx(ThemeProvider, { theme: merged, children: children });
|
|
61
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import { ElementTransformer, TextMatchTransformer, Transformer } from '@lexical/markdown';
|
|
9
|
+
export declare const HR: ElementTransformer;
|
|
10
|
+
export declare const IMAGE: TextMatchTransformer;
|
|
11
|
+
export declare const TABLE: ElementTransformer;
|
|
12
|
+
export declare const TRANSFORMERS: Array<Transformer>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, ELEMENT_TRANSFORMERS, TEXT_FORMAT_TRANSFORMERS, TEXT_MATCH_TRANSFORMERS, } from '@lexical/markdown';
|
|
9
|
+
import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode, } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
10
|
+
import { $createTableCellNode, $createTableNode, $createTableRowNode, $isTableCellNode, $isTableNode, $isTableRowNode, TableCellHeaderStates, TableCellNode, TableNode, TableRowNode, } from '@lexical/table';
|
|
11
|
+
import { $isParagraphNode, $isTextNode } from 'lexical';
|
|
12
|
+
import { $createImageNode, $isImageNode, ImageNode } from '../nodes/ImageNode';
|
|
13
|
+
export const HR = {
|
|
14
|
+
dependencies: [HorizontalRuleNode],
|
|
15
|
+
export: (node) => {
|
|
16
|
+
return $isHorizontalRuleNode(node) ? '***' : null;
|
|
17
|
+
},
|
|
18
|
+
regExp: /^(---|\*\*\*|___)\s?$/,
|
|
19
|
+
replace: (parentNode, _1, _2, isImport) => {
|
|
20
|
+
const line = $createHorizontalRuleNode();
|
|
21
|
+
// TODO: Get rid of isImport flag
|
|
22
|
+
if (isImport || parentNode.getNextSibling() != null) {
|
|
23
|
+
parentNode.replace(line);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
parentNode.insertBefore(line);
|
|
27
|
+
}
|
|
28
|
+
line.selectNext();
|
|
29
|
+
},
|
|
30
|
+
type: 'element',
|
|
31
|
+
};
|
|
32
|
+
export const IMAGE = {
|
|
33
|
+
dependencies: [ImageNode],
|
|
34
|
+
export: (node) => {
|
|
35
|
+
if (!$isImageNode(node)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return `})`;
|
|
39
|
+
},
|
|
40
|
+
importRegExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))/,
|
|
41
|
+
regExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))$/,
|
|
42
|
+
replace: (textNode, match) => {
|
|
43
|
+
const [, altText, src] = match;
|
|
44
|
+
const imageNode = $createImageNode({
|
|
45
|
+
altText,
|
|
46
|
+
maxWidth: 800,
|
|
47
|
+
src,
|
|
48
|
+
});
|
|
49
|
+
textNode.replace(imageNode);
|
|
50
|
+
},
|
|
51
|
+
trigger: ')',
|
|
52
|
+
type: 'text-match',
|
|
53
|
+
};
|
|
54
|
+
// Very primitive table setup
|
|
55
|
+
const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
|
|
56
|
+
const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/;
|
|
57
|
+
export const TABLE = {
|
|
58
|
+
dependencies: [TableNode, TableRowNode, TableCellNode],
|
|
59
|
+
export: (node) => {
|
|
60
|
+
if (!$isTableNode(node)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const output = [];
|
|
64
|
+
for (const row of node.getChildren()) {
|
|
65
|
+
const rowOutput = [];
|
|
66
|
+
if (!$isTableRowNode(row)) {
|
|
67
|
+
// eslint-disable-next-line no-continue
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let isHeaderRow = false;
|
|
71
|
+
for (const cell of row.getChildren()) {
|
|
72
|
+
// It's TableCellNode so it's just to make flow happy
|
|
73
|
+
if ($isTableCellNode(cell)) {
|
|
74
|
+
rowOutput.push($convertToMarkdownString(TRANSFORMERS, cell).replace(/\n/g, '\\n'));
|
|
75
|
+
if (cell.__headerState === TableCellHeaderStates.ROW) {
|
|
76
|
+
isHeaderRow = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
output.push(`| ${rowOutput.join(' | ')} |`);
|
|
81
|
+
if (isHeaderRow) {
|
|
82
|
+
output.push(`| ${rowOutput.map((_) => '---').join(' | ')} |`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return output.join('\n');
|
|
86
|
+
},
|
|
87
|
+
regExp: TABLE_ROW_REG_EXP,
|
|
88
|
+
replace: (parentNode, _1, match) => {
|
|
89
|
+
// Header row
|
|
90
|
+
if (TABLE_ROW_DIVIDER_REG_EXP.test(match[0])) {
|
|
91
|
+
const table = parentNode.getPreviousSibling();
|
|
92
|
+
if (!table || !$isTableNode(table)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const rows = table.getChildren();
|
|
96
|
+
const lastRow = rows[rows.length - 1];
|
|
97
|
+
if (!lastRow || !$isTableRowNode(lastRow)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Add header state to row cells
|
|
101
|
+
lastRow.getChildren().forEach((cell) => {
|
|
102
|
+
if (!$isTableCellNode(cell)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
cell.toggleHeaderStyle(TableCellHeaderStates.ROW);
|
|
106
|
+
});
|
|
107
|
+
// Remove line
|
|
108
|
+
parentNode.remove();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const matchCells = mapToTableCells(match[0]);
|
|
112
|
+
if (matchCells == null) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const rows = [matchCells];
|
|
116
|
+
let sibling = parentNode.getPreviousSibling();
|
|
117
|
+
let maxCells = matchCells.length;
|
|
118
|
+
while (sibling) {
|
|
119
|
+
if (!$isParagraphNode(sibling)) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (sibling.getChildrenSize() !== 1) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const firstChild = sibling.getFirstChild();
|
|
126
|
+
if (!$isTextNode(firstChild)) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
const cells = mapToTableCells(firstChild.getTextContent());
|
|
130
|
+
if (cells == null) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
maxCells = Math.max(maxCells, cells.length);
|
|
134
|
+
rows.unshift(cells);
|
|
135
|
+
const previousSibling = sibling.getPreviousSibling();
|
|
136
|
+
sibling.remove();
|
|
137
|
+
sibling = previousSibling;
|
|
138
|
+
}
|
|
139
|
+
const table = $createTableNode();
|
|
140
|
+
for (const cells of rows) {
|
|
141
|
+
const tableRow = $createTableRowNode();
|
|
142
|
+
table.append(tableRow);
|
|
143
|
+
for (let i = 0; i < maxCells; i++) {
|
|
144
|
+
tableRow.append(i < cells.length ? cells[i] : createTableCell(''));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const previousSibling = parentNode.getPreviousSibling();
|
|
148
|
+
if ($isTableNode(previousSibling) && getTableColumnsSize(previousSibling) === maxCells) {
|
|
149
|
+
previousSibling.append(...table.getChildren());
|
|
150
|
+
parentNode.remove();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
parentNode.replace(table);
|
|
154
|
+
}
|
|
155
|
+
table.selectEnd();
|
|
156
|
+
},
|
|
157
|
+
type: 'element',
|
|
158
|
+
};
|
|
159
|
+
function getTableColumnsSize(table) {
|
|
160
|
+
const row = table.getFirstChild();
|
|
161
|
+
return $isTableRowNode(row) ? row.getChildrenSize() : 0;
|
|
162
|
+
}
|
|
163
|
+
const createTableCell = (textContent) => {
|
|
164
|
+
textContent = textContent.replace(/\\n/g, '\n');
|
|
165
|
+
const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
|
|
166
|
+
$convertFromMarkdownString(textContent, TRANSFORMERS, cell);
|
|
167
|
+
return cell;
|
|
168
|
+
};
|
|
169
|
+
const mapToTableCells = (textContent) => {
|
|
170
|
+
const match = textContent.match(TABLE_ROW_REG_EXP);
|
|
171
|
+
if (!match || !match[1]) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return match[1].split('|').map((text) => createTableCell(text));
|
|
175
|
+
};
|
|
176
|
+
export const TRANSFORMERS = [
|
|
177
|
+
TABLE,
|
|
178
|
+
HR,
|
|
179
|
+
IMAGE,
|
|
180
|
+
CHECK_LIST,
|
|
181
|
+
...ELEMENT_TRANSFORMERS,
|
|
182
|
+
...TEXT_FORMAT_TRANSFORMERS,
|
|
183
|
+
...TEXT_MATCH_TRANSFORMERS,
|
|
184
|
+
];
|
|
@@ -15,7 +15,7 @@ import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontal
|
|
|
15
15
|
import { $createHeadingNode, $createQuoteNode } from '@lexical/rich-text';
|
|
16
16
|
import { $wrapNodes } from '@lexical/selection';
|
|
17
17
|
import { INSERT_TABLE_COMMAND } from '@lexical/table';
|
|
18
|
-
import { $createParagraphNode, $getSelection, $isRangeSelection, FORMAT_ELEMENT_COMMAND, createCommand, $insertNodes, $createTextNode, COMMAND_PRIORITY_EDITOR, } from 'lexical';
|
|
18
|
+
import { $createParagraphNode, $getSelection, $isRangeSelection, FORMAT_ELEMENT_COMMAND, createCommand, $insertNodes, $createTextNode, COMMAND_PRIORITY_EDITOR, $setSelection, } from 'lexical';
|
|
19
19
|
import { mergeRegister } from '@lexical/utils';
|
|
20
20
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
21
21
|
import { Box } from '@mui/material';
|
|
@@ -385,6 +385,7 @@ export const uploadFile = (editor) => {
|
|
|
385
385
|
// @ts-ignore
|
|
386
386
|
const uploader = window?.uploaderRef?.current?.getUploader();
|
|
387
387
|
uploader.open();
|
|
388
|
+
let selection = null;
|
|
388
389
|
// listen to all upload success
|
|
389
390
|
uploader.onUploadSuccess(({ file, response }) => {
|
|
390
391
|
// missing the source: function-upload-file
|
|
@@ -414,9 +415,21 @@ export const uploadFile = (editor) => {
|
|
|
414
415
|
mimetype: data.mimetype,
|
|
415
416
|
});
|
|
416
417
|
}
|
|
418
|
+
// 上传图片成功后 -> 插入图片结点 -> 记住 selection (便于点击 Uploader 中的 Done 后恢复 selection)
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
selection = editor.getEditorState().read($getSelection);
|
|
421
|
+
}, 10);
|
|
417
422
|
}
|
|
418
423
|
}
|
|
419
424
|
});
|
|
425
|
+
// 点击 Uploader 中的 Done 后恢复 selection
|
|
426
|
+
uploader.onClose(() => {
|
|
427
|
+
if (selection) {
|
|
428
|
+
editor.update(() => {
|
|
429
|
+
$setSelection(selection.clone());
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
});
|
|
420
433
|
}
|
|
421
434
|
else {
|
|
422
435
|
throw new Error('Missing required `window.uploaderRef`');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
9
9
|
import { mergeRegister } from '@lexical/utils';
|
|
10
|
-
import { $createParagraphNode, $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand
|
|
10
|
+
import { $createParagraphNode, $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical';
|
|
11
11
|
import { useEffect } from 'react';
|
|
12
12
|
import { $createImageNode, ImageNode } from '../../nodes/ImageNode';
|
|
13
13
|
export const INSERT_IMAGE_COMMAND = createCommand('INSERT_IMAGE_COMMAND');
|
|
@@ -21,7 +21,7 @@ export default function ImagesPlugin({ captionsEnabled }) {
|
|
|
21
21
|
const imageNode = $createImageNode(payload);
|
|
22
22
|
// 插入段落再插入图片, 确保图片独自占一行 (#2505)
|
|
23
23
|
const p = $createParagraphNode();
|
|
24
|
-
$insertNodes([p, imageNode]);
|
|
24
|
+
$insertNodes([p, imageNode, $createParagraphNode()]);
|
|
25
25
|
p.remove();
|
|
26
26
|
return true;
|
|
27
27
|
}, COMMAND_PRIORITY_EDITOR));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "npm run storybook",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@arcblock/ux": "^2.9.77",
|
|
41
41
|
"@blocklet/embed": "^0.1.11",
|
|
42
42
|
"@blocklet/pages-kit": "^0.2.302",
|
|
43
|
-
"@blocklet/pdf": "2.0.
|
|
43
|
+
"@blocklet/pdf": "2.0.9",
|
|
44
44
|
"@excalidraw/excalidraw": "^0.14.2",
|
|
45
45
|
"@iconify/iconify": "^3.0.1",
|
|
46
46
|
"@iconify/icons-tabler": "^1.2.95",
|
|
@@ -113,5 +113,5 @@
|
|
|
113
113
|
"react": "*",
|
|
114
114
|
"react-dom": "*"
|
|
115
115
|
},
|
|
116
|
-
"gitHead": "
|
|
116
|
+
"gitHead": "07328b8eb9afa61b68261bb604d3e50f7cca6eba"
|
|
117
117
|
}
|