@blocklet/editor 2.0.7 → 2.0.10

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.
@@ -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,3 @@
1
+ import { Klass, LexicalNode } from 'lexical';
2
+ declare const MarkdownEditorNodes: Array<Klass<LexicalNode>>;
3
+ export default MarkdownEditorNodes;
@@ -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,5 @@
1
+ import type { TextMatchTransformer } from '@lexical/markdown';
2
+ export declare function MediaUrlFixerPlugin({ mediaUrlPrefix, transformers, }: {
3
+ mediaUrlPrefix: string;
4
+ transformers?: Array<TextMatchTransformer>;
5
+ }): null;
@@ -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 `![${node.getAltText()}](${src})`;
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,3 @@
1
+ import type { LexicalEditor } from 'lexical';
2
+ export declare const uploadFile: (editor: LexicalEditor) => void;
3
+ export default function ToolbarPlugin(): import("react/jsx-runtime").JSX.Element | null;
@@ -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,4 @@
1
+ import React from 'react';
2
+ export declare function MarkdownEditorThemeProvider({ children }: {
3
+ children: React.ReactNode;
4
+ }): import("react/jsx-runtime").JSX.Element;
@@ -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 `![${node.getAltText()}](${node.getSrc()})`;
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, } from 'lexical';
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.7",
3
+ "version": "2.0.10",
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.7",
43
+ "@blocklet/pdf": "2.0.10",
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": "adb1238f23069ab36fc87899d124c2f8e871bd28"
116
+ "gitHead": "1c91ad35cf6c9164f1800083b86b8067bd7d5db0"
117
117
  }