@eventuras/scribo 0.8.2 → 0.10.7
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/dist/App.d.ts +11 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/MarkdownEditor.d.ts +18 -0
- package/dist/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor2.js +111 -0
- package/dist/MarkdownEditor2.js.map +1 -0
- package/dist/MarkdownInput.d.ts +27 -0
- package/dist/MarkdownInput.d.ts.map +1 -0
- package/dist/MarkdownInput.js +57 -0
- package/dist/MarkdownInput.js.map +1 -0
- package/dist/callout.js +5 -0
- package/dist/context/SharedHistoryContext.d.ts +11 -0
- package/dist/context/SharedHistoryContext.d.ts.map +1 -0
- package/dist/context/SharedHistoryContext.js +12 -0
- package/dist/context/SharedHistoryContext.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +4 -0
- package/dist/nodes/EditorNodes.d.ts +4 -0
- package/dist/nodes/EditorNodes.d.ts.map +1 -0
- package/dist/nodes/EditorNodes.js +39 -0
- package/dist/nodes/EditorNodes.js.map +1 -0
- package/dist/nodes/callout/CalloutComponent.d.ts +6 -0
- package/dist/nodes/callout/CalloutComponent.d.ts.map +1 -0
- package/dist/nodes/callout/CalloutComponent.js +53 -0
- package/dist/nodes/callout/CalloutComponent.js.map +1 -0
- package/dist/nodes/callout/CalloutNode.d.ts +27 -0
- package/dist/nodes/callout/CalloutNode.d.ts.map +1 -0
- package/dist/nodes/callout/CalloutNode.js +153 -0
- package/dist/nodes/callout/CalloutNode.js.map +1 -0
- package/dist/nodes/callout/CalloutTransformer.d.ts +3 -0
- package/dist/nodes/callout/CalloutTransformer.d.ts.map +1 -0
- package/dist/nodes/callout/CalloutTransformer.js +48 -0
- package/dist/nodes/callout/CalloutTransformer.js.map +1 -0
- package/dist/nodes/callout/calloutPlugin.d.ts +3 -0
- package/dist/nodes/callout/calloutPlugin.d.ts.map +1 -0
- package/dist/nodes/callout/calloutPlugin.js +34 -0
- package/dist/nodes/callout/calloutPlugin.js.map +1 -0
- package/dist/nodes/callout/index.d.ts +6 -0
- package/dist/nodes/callout/index.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleItemComponent.d.ts +9 -0
- package/dist/nodes/schedule/ScheduleItemComponent.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleItemComponent.js +167 -0
- package/dist/nodes/schedule/ScheduleItemComponent.js.map +1 -0
- package/dist/nodes/schedule/ScheduleItemNode.d.ts +31 -0
- package/dist/nodes/schedule/ScheduleItemNode.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleItemNode.js +94 -0
- package/dist/nodes/schedule/ScheduleItemNode.js.map +1 -0
- package/dist/nodes/schedule/ScheduleTransformer.d.ts +22 -0
- package/dist/nodes/schedule/ScheduleTransformer.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleTransformer.js +60 -0
- package/dist/nodes/schedule/ScheduleTransformer.js.map +1 -0
- package/dist/nodes/schedule/index.d.ts +5 -0
- package/dist/nodes/schedule/index.d.ts.map +1 -0
- package/dist/nodes/schedule/schedulePlugin.d.ts +3 -0
- package/dist/nodes/schedule/schedulePlugin.d.ts.map +1 -0
- package/dist/nodes/schedule/schedulePlugin.js +33 -0
- package/dist/nodes/schedule/schedulePlugin.js.map +1 -0
- package/dist/plugins/AutoLinkPlugin.d.ts +3 -0
- package/dist/plugins/AutoLinkPlugin.d.ts.map +1 -0
- package/dist/plugins/AutoLinkPlugin.js +22 -0
- package/dist/plugins/AutoLinkPlugin.js.map +1 -0
- package/dist/plugins/FloatingLinkEditorPlugin.d.ts +7 -0
- package/dist/plugins/FloatingLinkEditorPlugin.d.ts.map +1 -0
- package/dist/plugins/FloatingLinkEditorPlugin2.js +302 -0
- package/dist/plugins/FloatingLinkEditorPlugin2.js.map +1 -0
- package/dist/plugins/HistoryPlugin.d.ts +26 -0
- package/dist/plugins/HistoryPlugin.d.ts.map +1 -0
- package/dist/plugins/LinkPlugin.d.ts +3 -0
- package/dist/plugins/LinkPlugin.d.ts.map +1 -0
- package/dist/plugins/LinkPlugin.js +18 -0
- package/dist/plugins/LinkPlugin.js.map +1 -0
- package/dist/plugins/ToolbarPlugin.d.ts +7 -0
- package/dist/plugins/ToolbarPlugin.d.ts.map +1 -0
- package/dist/plugins/ToolbarPlugin.js +372 -0
- package/dist/plugins/ToolbarPlugin.js.map +1 -0
- package/dist/schedule.js +4 -0
- package/dist/scribo.css +1977 -0
- package/dist/themes/EditorTheme.d.ts +4 -0
- package/dist/themes/EditorTheme.d.ts.map +1 -0
- package/dist/themes/EditorTheme2.js +107 -0
- package/dist/themes/EditorTheme2.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/ContentEditable.d.ts +6 -0
- package/dist/ui/ContentEditable.d.ts.map +1 -0
- package/dist/ui/ContentEditable2.js +14 -0
- package/dist/ui/ContentEditable2.js.map +1 -0
- package/dist/ui/DropDown.d.ts +18 -0
- package/dist/ui/DropDown.d.ts.map +1 -0
- package/dist/ui/DropDown.js +162 -0
- package/dist/ui/DropDown.js.map +1 -0
- package/dist/ui/Placeholder.d.ts +6 -0
- package/dist/ui/Placeholder.d.ts.map +1 -0
- package/dist/ui/Placeholder2.js +13 -0
- package/dist/ui/Placeholder2.js.map +1 -0
- package/dist/utils/environment.d.ts +24 -0
- package/dist/utils/environment.d.ts.map +1 -0
- package/dist/utils/environment.js +20 -0
- package/dist/utils/environment.js.map +1 -0
- package/dist/utils/getSelectedNode.d.ts +3 -0
- package/dist/utils/getSelectedNode.d.ts.map +1 -0
- package/dist/utils/getSelectedNode.js +23 -0
- package/dist/utils/getSelectedNode.js.map +1 -0
- package/dist/utils/setFloatingElemPosition.d.ts +2 -0
- package/dist/utils/setFloatingElemPosition.d.ts.map +1 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.d.ts +2 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.d.ts.map +1 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.js +33 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.js.map +1 -0
- package/dist/utils/url.d.ts +11 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +33 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/vite.svg +1 -0
- package/package.json +57 -39
- package/src/MarkdownEditor.css +130 -27
- package/src/MarkdownEditor.tsx +82 -31
- package/src/MarkdownInput.tsx +9 -10
- package/src/context/SharedHistoryContext.tsx +8 -16
- package/src/main.tsx +3 -3
- package/src/nodes/EditorNodes.ts +10 -10
- package/src/nodes/callout/Callout.css +94 -0
- package/src/nodes/callout/CalloutComponent.tsx +61 -0
- package/src/nodes/callout/CalloutNode.ts +197 -0
- package/src/nodes/callout/CalloutTransformer.ts +87 -0
- package/src/nodes/callout/calloutPlugin.ts +37 -0
- package/src/nodes/callout/index.ts +5 -0
- package/src/nodes/schedule/ScheduleItem.css +140 -0
- package/src/nodes/schedule/ScheduleItemComponent.tsx +195 -0
- package/src/nodes/schedule/ScheduleItemNode.ts +135 -0
- package/src/nodes/schedule/ScheduleTransformer.ts +80 -0
- package/src/nodes/schedule/index.ts +4 -0
- package/src/nodes/schedule/schedulePlugin.ts +33 -0
- package/src/plugins/AutoLinkPlugin.tsx +5 -10
- package/src/plugins/FloatingLinkEditorPlugin.tsx +42 -66
- package/src/plugins/HistoryPlugin.tsx +20 -30
- package/src/plugins/LinkPlugin.tsx +2 -3
- package/src/plugins/ToolbarPlugin.tsx +129 -141
- package/src/themes/EditorTheme.css +82 -62
- package/src/themes/EditorTheme.ts +89 -90
- package/src/types.ts +27 -0
- package/src/ui/ContentEditable.tsx +5 -5
- package/src/ui/DropDown.tsx +23 -41
- package/src/ui/Placeholder.tsx +21 -21
- package/src/utils/environment.ts +6 -12
- package/src/utils/getSelectedNode.ts +3 -5
- package/src/utils/setFloatingElemPosition.ts +2 -5
- package/src/utils/setFloatingElemPositionForLinkEditor.ts +1 -1
- package/src/utils/url.ts +4 -10
- package/tsconfig.json +8 -0
- package/CHANGELOG.md +0 -59
- package/README.md +0 -126
- package/RELEASE.md +0 -102
- package/eslint.config.js +0 -4
- package/src/App.css +0 -35
- package/src/App.tsx +0 -42
- package/src/index.tsx +0 -13
- package/tsconfig.node.json +0 -11
- package/vite.config.site.ts +0 -19
- package/vite.config.ts +0 -33
- /package/{src → dist}/vite-env.d.ts +0 -0
package/src/MarkdownEditor.tsx
CHANGED
|
@@ -2,27 +2,26 @@ import {
|
|
|
2
2
|
$convertFromMarkdownString,
|
|
3
3
|
$convertToMarkdownString,
|
|
4
4
|
TRANSFORMERS,
|
|
5
|
-
} from
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
} from 'lexical';
|
|
9
|
-
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
10
|
-
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
|
|
11
|
-
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
|
|
12
|
-
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
13
|
-
import { EditorState } from "lexical";
|
|
14
|
-
import React, { useState } from "react";
|
|
15
|
-
|
|
16
|
-
import EditorNodes from "./nodes/EditorNodes";
|
|
17
|
-
import ToolbarPlugin from "./plugins/ToolbarPlugin";
|
|
18
|
-
import EditorTheme from "./themes/EditorTheme";
|
|
19
|
-
import ContentEditable from "./ui/ContentEditable";
|
|
20
|
-
import Placeholder from "./ui/Placeholder";
|
|
5
|
+
} from '@lexical/markdown';
|
|
6
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
7
|
+
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
21
8
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
9
|
+
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
|
10
|
+
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
11
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
12
|
+
import { $createParagraphNode, $createTextNode, $getRoot, type EditorState } from 'lexical';
|
|
13
|
+
import type React from 'react';
|
|
14
|
+
import { useMemo, useState } from 'react';
|
|
25
15
|
import { useSharedHistoryContext } from './context/SharedHistoryContext';
|
|
16
|
+
import EditorNodes from './nodes/EditorNodes';
|
|
17
|
+
import AutoLinkPlugin from './plugins/AutoLinkPlugin';
|
|
18
|
+
import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin';
|
|
19
|
+
import LinkPlugin from './plugins/LinkPlugin';
|
|
20
|
+
import ToolbarPlugin from './plugins/ToolbarPlugin';
|
|
21
|
+
import EditorTheme from './themes/EditorTheme';
|
|
22
|
+
import type { ScriboPlugin } from './types';
|
|
23
|
+
import ContentEditable from './ui/ContentEditable';
|
|
24
|
+
import Placeholder from './ui/Placeholder';
|
|
26
25
|
|
|
27
26
|
export type onChangeMisc = {
|
|
28
27
|
plainText: string;
|
|
@@ -30,6 +29,7 @@ export type onChangeMisc = {
|
|
|
30
29
|
export interface MarkdownEditorProps {
|
|
31
30
|
initialMarkdown?: string;
|
|
32
31
|
className?: string;
|
|
32
|
+
plugins?: ScriboPlugin[];
|
|
33
33
|
onChange?: (markdown: string, misc: onChangeMisc) => void;
|
|
34
34
|
onBlur?: () => void;
|
|
35
35
|
placeholder?: string;
|
|
@@ -41,8 +41,9 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = (props) => {
|
|
|
41
41
|
const { historyState } = useSharedHistoryContext();
|
|
42
42
|
const [isLinkEditMode, setIsLinkEditMode] = useState<boolean>(false);
|
|
43
43
|
|
|
44
|
-
const [floatingAnchorElem, setFloatingAnchorElem] =
|
|
45
|
-
|
|
44
|
+
const [floatingAnchorElem, setFloatingAnchorElem] = useState<HTMLDivElement | undefined>(
|
|
45
|
+
undefined
|
|
46
|
+
);
|
|
46
47
|
|
|
47
48
|
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
|
|
48
49
|
if (_floatingAnchorElem !== null) {
|
|
@@ -50,33 +51,76 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = (props) => {
|
|
|
50
51
|
}
|
|
51
52
|
};
|
|
52
53
|
|
|
54
|
+
// Collect nodes and transformers from plugins
|
|
55
|
+
const pluginNodes = useMemo(
|
|
56
|
+
() => (props.plugins ?? []).flatMap((p) => p.nodes ?? []),
|
|
57
|
+
[props.plugins]
|
|
58
|
+
);
|
|
59
|
+
const allTransformers = useMemo(
|
|
60
|
+
() => [...(props.plugins ?? []).flatMap((p) => p.transformers ?? []), ...TRANSFORMERS],
|
|
61
|
+
[props.plugins]
|
|
62
|
+
);
|
|
63
|
+
const toolbarButtons = useMemo(
|
|
64
|
+
() => (props.plugins ?? []).flatMap((p) => p.toolbarButtons ?? []),
|
|
65
|
+
[props.plugins]
|
|
66
|
+
);
|
|
67
|
+
const editorPlugins = useMemo(
|
|
68
|
+
() => (props.plugins ?? []).flatMap((p) => p.editorPlugins ?? []),
|
|
69
|
+
[props.plugins]
|
|
70
|
+
);
|
|
71
|
+
|
|
53
72
|
const internalOnChange = (editorState: EditorState) => {
|
|
54
73
|
editorState.read(() => {
|
|
55
|
-
const markdown = $convertToMarkdownString(
|
|
74
|
+
const markdown = $convertToMarkdownString(allTransformers);
|
|
56
75
|
const plainText = $getRoot().getTextContent();
|
|
57
|
-
props.onChange
|
|
76
|
+
props.onChange?.(markdown, { plainText });
|
|
58
77
|
});
|
|
59
78
|
};
|
|
60
79
|
|
|
61
80
|
const initialConfig = {
|
|
62
|
-
namespace:
|
|
63
|
-
nodes: [...EditorNodes],
|
|
81
|
+
namespace: 'ScriboMarkdown',
|
|
82
|
+
nodes: [...EditorNodes, ...pluginNodes],
|
|
64
83
|
onError: (error: Error) => {
|
|
65
84
|
throw error;
|
|
66
85
|
},
|
|
67
86
|
theme: EditorTheme,
|
|
68
|
-
editorState:
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
editorState: props.initialMarkdown
|
|
88
|
+
? () => {
|
|
89
|
+
// Unescape backslash-escaped markdown characters
|
|
90
|
+
const unescapedMarkdown = (props.initialMarkdown ?? '').replaceAll(
|
|
91
|
+
/\\([*_`[\]()#+-])/g,
|
|
92
|
+
'$1'
|
|
93
|
+
);
|
|
94
|
+
try {
|
|
95
|
+
$convertFromMarkdownString(unescapedMarkdown, allTransformers);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Markdown parsing threw (e.g. a transformer bug). Fall back to plain
|
|
98
|
+
// text so the field stays editable and content isn't lost — better
|
|
99
|
+
// than crashing the whole tree and propagating to the page boundary.
|
|
100
|
+
console.error('Scribo: markdown parse failed, falling back to plain text', err);
|
|
101
|
+
const root = $getRoot();
|
|
102
|
+
root.clear();
|
|
103
|
+
const paragraph = $createParagraphNode();
|
|
104
|
+
paragraph.append($createTextNode(unescapedMarkdown));
|
|
105
|
+
root.append(paragraph);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
: undefined,
|
|
71
109
|
};
|
|
72
110
|
|
|
73
111
|
return (
|
|
74
112
|
<div className={props.className}>
|
|
75
113
|
<LexicalComposer initialConfig={initialConfig}>
|
|
76
|
-
<div
|
|
77
|
-
|
|
114
|
+
<div
|
|
115
|
+
className="editor-shell"
|
|
116
|
+
onBlur={props.onBlur}
|
|
117
|
+
data-testid={props['data-testid']}
|
|
118
|
+
id={props.id}
|
|
119
|
+
>
|
|
120
|
+
<ToolbarPlugin setIsLinkEditMode={setIsLinkEditMode} toolbarButtons={toolbarButtons} />
|
|
78
121
|
<div className="editor-container rich-text">
|
|
79
122
|
<HistoryPlugin externalHistoryState={historyState} />
|
|
123
|
+
<MarkdownShortcutPlugin transformers={allTransformers} />
|
|
80
124
|
<AutoLinkPlugin />
|
|
81
125
|
<LinkPlugin />
|
|
82
126
|
<FloatingLinkEditorPlugin
|
|
@@ -87,7 +131,11 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = (props) => {
|
|
|
87
131
|
<RichTextPlugin
|
|
88
132
|
contentEditable={
|
|
89
133
|
<div className="editor-scroller">
|
|
90
|
-
<div
|
|
134
|
+
<div
|
|
135
|
+
className="editor"
|
|
136
|
+
ref={onRef}
|
|
137
|
+
id={props.id ? `${props.id}-editable` : undefined}
|
|
138
|
+
>
|
|
91
139
|
<ContentEditable />
|
|
92
140
|
</div>
|
|
93
141
|
</div>
|
|
@@ -97,6 +145,9 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = (props) => {
|
|
|
97
145
|
/>
|
|
98
146
|
</div>
|
|
99
147
|
{props.onChange && <OnChangePlugin onChange={internalOnChange} />}
|
|
148
|
+
{editorPlugins.map((Plugin) => (
|
|
149
|
+
<Plugin key={Plugin.displayName ?? Plugin.name} />
|
|
150
|
+
))}
|
|
100
151
|
</div>
|
|
101
152
|
</LexicalComposer>
|
|
102
153
|
</div>
|
package/src/MarkdownInput.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import MarkdownEditor from './MarkdownEditor';
|
|
3
2
|
import { useRef, useState } from 'react';
|
|
3
|
+
import MarkdownEditor from './MarkdownEditor';
|
|
4
|
+
import type { ScriboPlugin } from './types';
|
|
4
5
|
|
|
5
6
|
export type MarkdownInputProps = {
|
|
6
7
|
id?: string;
|
|
7
8
|
name: string;
|
|
8
9
|
maxLength?: number;
|
|
10
|
+
plugins?: ScriboPlugin[];
|
|
9
11
|
label?: string;
|
|
10
12
|
placeholder?: string;
|
|
11
13
|
defaultValue?: string;
|
|
@@ -28,7 +30,7 @@ const MarkdownInput = (props: MarkdownInputProps) => {
|
|
|
28
30
|
const plainTextRef = useRef('');
|
|
29
31
|
const id = props.id ?? props.name;
|
|
30
32
|
|
|
31
|
-
const handleChange = (markdown: string, { plainText }: { plainText: string
|
|
33
|
+
const handleChange = (markdown: string, { plainText }: { plainText: string }) => {
|
|
32
34
|
plainTextRef.current = plainText;
|
|
33
35
|
setValue(markdown);
|
|
34
36
|
|
|
@@ -37,9 +39,9 @@ const MarkdownInput = (props: MarkdownInputProps) => {
|
|
|
37
39
|
props.onChange(markdown);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
// Validate maxLength
|
|
41
|
-
if (props.maxLength &&
|
|
42
|
-
setError(`Maximum ${props.maxLength} characters allowed`);
|
|
42
|
+
// Validate maxLength against markdown content (what gets stored)
|
|
43
|
+
if (props.maxLength && markdown.length > props.maxLength) {
|
|
44
|
+
setError(`Maximum ${props.maxLength} characters allowed (currently ${markdown.length})`);
|
|
43
45
|
} else {
|
|
44
46
|
setError(null);
|
|
45
47
|
}
|
|
@@ -53,14 +55,11 @@ const MarkdownInput = (props: MarkdownInputProps) => {
|
|
|
53
55
|
</label>
|
|
54
56
|
)}
|
|
55
57
|
{/* Hidden input to store value - controlled by React state */}
|
|
56
|
-
<input
|
|
57
|
-
type="hidden"
|
|
58
|
-
name={props.name}
|
|
59
|
-
value={value}
|
|
60
|
-
/>
|
|
58
|
+
<input type="hidden" name={props.name} value={value} />
|
|
61
59
|
<MarkdownEditor
|
|
62
60
|
onChange={handleChange}
|
|
63
61
|
className={props.editorClassName}
|
|
62
|
+
plugins={props.plugins}
|
|
64
63
|
initialMarkdown={props.defaultValue}
|
|
65
64
|
placeholder={props.placeholder}
|
|
66
65
|
data-testid={props['data-testid']}
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {HistoryState} from '@lexical/react/LexicalHistoryPlugin';
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import {createContext, ReactNode, useContext, useMemo} from 'react';
|
|
9
|
+
import type { HistoryState } from '@lexical/react/LexicalHistoryPlugin';
|
|
10
|
+
import { createEmptyHistoryState } from '@lexical/react/LexicalHistoryPlugin';
|
|
11
|
+
import type * as React from 'react';
|
|
12
|
+
import type { JSX } from 'react';
|
|
13
|
+
import { createContext, type ReactNode, useContext, useMemo } from 'react';
|
|
15
14
|
|
|
16
15
|
type ContextShape = {
|
|
17
16
|
historyState?: HistoryState;
|
|
@@ -19,18 +18,11 @@ type ContextShape = {
|
|
|
19
18
|
|
|
20
19
|
const Context: React.Context<ContextShape> = createContext({});
|
|
21
20
|
|
|
22
|
-
export const SharedHistoryContext = ({
|
|
23
|
-
|
|
24
|
-
}: {
|
|
25
|
-
children: ReactNode;
|
|
26
|
-
}): JSX.Element => {
|
|
27
|
-
const historyContext = useMemo(
|
|
28
|
-
() => ({historyState: createEmptyHistoryState()}),
|
|
29
|
-
[],
|
|
30
|
-
);
|
|
21
|
+
export const SharedHistoryContext = ({ children }: { children: ReactNode }): JSX.Element => {
|
|
22
|
+
const historyContext = useMemo(() => ({ historyState: createEmptyHistoryState() }), []);
|
|
31
23
|
return <Context.Provider value={historyContext}>{children}</Context.Provider>;
|
|
32
24
|
};
|
|
33
25
|
|
|
34
26
|
export const useSharedHistoryContext = (): ContextShape => {
|
|
35
27
|
return useContext(Context);
|
|
36
|
-
};
|
|
28
|
+
};
|
package/src/main.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
1
|
import './MarkdownEditor.css';
|
|
3
|
-
import MarkdownEditor from
|
|
4
|
-
import MarkdownInput from
|
|
2
|
+
import MarkdownEditor from './MarkdownEditor';
|
|
3
|
+
import MarkdownInput from './MarkdownInput';
|
|
5
4
|
|
|
5
|
+
export type { ScriboPlugin, ScriboToolbarButton } from './types';
|
|
6
6
|
export { MarkdownEditor, MarkdownInput };
|
package/src/nodes/EditorNodes.ts
CHANGED
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
* Modified from: https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/PlaygroundNodes.ts
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { CodeHighlightNode, CodeNode } from
|
|
11
|
-
import { HashtagNode } from
|
|
12
|
-
import { AutoLinkNode, LinkNode } from
|
|
13
|
-
import { ListItemNode, ListNode } from
|
|
14
|
-
import { MarkNode } from
|
|
15
|
-
import { OverflowNode } from
|
|
16
|
-
import { HorizontalRuleNode } from
|
|
17
|
-
import { HeadingNode, QuoteNode } from
|
|
18
|
-
import { TableCellNode, TableNode, TableRowNode } from
|
|
19
|
-
import type { Klass, LexicalNode } from
|
|
10
|
+
import { CodeHighlightNode, CodeNode } from '@lexical/code';
|
|
11
|
+
import { HashtagNode } from '@lexical/hashtag';
|
|
12
|
+
import { AutoLinkNode, LinkNode } from '@lexical/link';
|
|
13
|
+
import { ListItemNode, ListNode } from '@lexical/list';
|
|
14
|
+
import { MarkNode } from '@lexical/mark';
|
|
15
|
+
import { OverflowNode } from '@lexical/overflow';
|
|
16
|
+
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
17
|
+
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
18
|
+
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
|
|
19
|
+
import type { Klass, LexicalNode } from 'lexical';
|
|
20
20
|
|
|
21
21
|
// Using 'as any' to work around Lexical version conflicts between workspace installs
|
|
22
22
|
const EditorNodes: Array<Klass<LexicalNode>> = [
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* Callout container (ElementNode) */
|
|
2
|
+
.callout-editor {
|
|
3
|
+
position: relative;
|
|
4
|
+
border-left: 4px solid;
|
|
5
|
+
border-radius: 0.375rem;
|
|
6
|
+
padding: 2rem 0.75rem 0.75rem;
|
|
7
|
+
margin: 0.5rem 0;
|
|
8
|
+
min-height: 3rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.callout-editor--note {
|
|
12
|
+
border-color: #2563eb;
|
|
13
|
+
background: rgba(37, 99, 235, 0.06);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.callout-editor--tip {
|
|
17
|
+
border-color: #16a34a;
|
|
18
|
+
background: rgba(22, 163, 74, 0.06);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.callout-editor--important {
|
|
22
|
+
border-color: #7c3aed;
|
|
23
|
+
background: rgba(124, 58, 237, 0.06);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.callout-editor--warning {
|
|
27
|
+
border-color: #d97706;
|
|
28
|
+
background: rgba(217, 119, 6, 0.06);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.callout-editor--caution {
|
|
32
|
+
border-color: #dc2626;
|
|
33
|
+
background: rgba(220, 38, 38, 0.06);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Toolbar rendered directly inside callout element */
|
|
37
|
+
.callout-toolbar {
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0.25rem;
|
|
40
|
+
left: 0.5rem;
|
|
41
|
+
right: 0.5rem;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 0.25rem;
|
|
45
|
+
user-select: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.callout-toolbar-icon {
|
|
49
|
+
font-size: 0.875rem;
|
|
50
|
+
line-height: 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.callout-toolbar-type {
|
|
54
|
+
font: inherit;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
font-size: 0.75rem;
|
|
57
|
+
border: none;
|
|
58
|
+
background: transparent;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
padding: 0.125rem 0.25rem;
|
|
61
|
+
border-radius: 0.25rem;
|
|
62
|
+
color: inherit;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.callout-toolbar-type:hover {
|
|
66
|
+
background: rgba(0, 0, 0, 0.06);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.callout-toolbar-remove {
|
|
70
|
+
margin-left: auto;
|
|
71
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
72
|
+
background: #f7f7f7;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
padding: 0 0.375rem;
|
|
75
|
+
color: #595959;
|
|
76
|
+
font-size: 0.875rem;
|
|
77
|
+
line-height: 1.5;
|
|
78
|
+
border-radius: 0.25rem;
|
|
79
|
+
transition:
|
|
80
|
+
color 0.15s,
|
|
81
|
+
background 0.15s,
|
|
82
|
+
border-color 0.15s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.callout-toolbar-remove:hover {
|
|
86
|
+
color: #991b1b;
|
|
87
|
+
background: #f3e4e4;
|
|
88
|
+
border-color: rgba(153, 27, 27, 0.35);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Content paragraphs inside callout */
|
|
92
|
+
.callout-editor p {
|
|
93
|
+
margin: 0.25rem 0;
|
|
94
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import './Callout.css';
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import { $createParagraphNode, $getNodeByKey } from 'lexical';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
import { $isCalloutNode, CALLOUT_TYPES, type CalloutType } from './CalloutNode';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plugin that listens for DOM custom events from CalloutNode toolbar
|
|
10
|
+
* and updates the Lexical state accordingly.
|
|
11
|
+
*/
|
|
12
|
+
export const CalloutPlugin = () => {
|
|
13
|
+
const [editor] = useLexicalComposerContext();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const rootElement = editor.getRootElement()?.parentElement;
|
|
17
|
+
if (!rootElement) return;
|
|
18
|
+
|
|
19
|
+
const handleTypeChange = (e: Event) => {
|
|
20
|
+
const { nodeKey, calloutType } = (e as CustomEvent).detail;
|
|
21
|
+
if (!nodeKey || !CALLOUT_TYPES.includes(calloutType)) return;
|
|
22
|
+
editor.update(() => {
|
|
23
|
+
const node = $getNodeByKey(nodeKey);
|
|
24
|
+
if ($isCalloutNode(node)) {
|
|
25
|
+
node.setCalloutType(calloutType as CalloutType);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleRemove = (e: Event) => {
|
|
31
|
+
const { nodeKey } = (e as CustomEvent).detail;
|
|
32
|
+
if (!nodeKey) return;
|
|
33
|
+
editor.update(() => {
|
|
34
|
+
const node = $getNodeByKey(nodeKey);
|
|
35
|
+
if (!$isCalloutNode(node)) return;
|
|
36
|
+
|
|
37
|
+
const children = node.getChildren();
|
|
38
|
+
if (children.length > 0) {
|
|
39
|
+
for (const child of children) {
|
|
40
|
+
node.insertBefore(child);
|
|
41
|
+
}
|
|
42
|
+
node.remove();
|
|
43
|
+
} else {
|
|
44
|
+
const p = $createParagraphNode();
|
|
45
|
+
node.replace(p);
|
|
46
|
+
p.selectEnd();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
rootElement.addEventListener('callout-type-change', handleTypeChange);
|
|
52
|
+
rootElement.addEventListener('callout-remove', handleRemove);
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
rootElement.removeEventListener('callout-type-change', handleTypeChange);
|
|
56
|
+
rootElement.removeEventListener('callout-remove', handleRemove);
|
|
57
|
+
};
|
|
58
|
+
}, [editor]);
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
$applyNodeReplacement,
|
|
3
|
+
$createParagraphNode,
|
|
4
|
+
type DOMConversionMap,
|
|
5
|
+
type DOMExportOutput,
|
|
6
|
+
ElementNode,
|
|
7
|
+
type LexicalNode,
|
|
8
|
+
type NodeKey,
|
|
9
|
+
type SerializedElementNode,
|
|
10
|
+
type Spread,
|
|
11
|
+
} from 'lexical';
|
|
12
|
+
|
|
13
|
+
export const CALLOUT_TYPES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'] as const;
|
|
14
|
+
export type CalloutType = (typeof CALLOUT_TYPES)[number];
|
|
15
|
+
|
|
16
|
+
const CALLOUT_ICONS: Record<CalloutType, string> = {
|
|
17
|
+
NOTE: 'ℹ️',
|
|
18
|
+
TIP: '💡',
|
|
19
|
+
IMPORTANT: '❗',
|
|
20
|
+
WARNING: '⚠️',
|
|
21
|
+
CAUTION: '🔴',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SerializedCalloutNode = Spread<{ calloutType: CalloutType }, SerializedElementNode>;
|
|
25
|
+
|
|
26
|
+
export class CalloutNode extends ElementNode {
|
|
27
|
+
__calloutType: CalloutType;
|
|
28
|
+
|
|
29
|
+
static getType(): string {
|
|
30
|
+
return 'callout';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static clone(node: CalloutNode): CalloutNode {
|
|
34
|
+
return new CalloutNode(node.__calloutType, node.__key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(calloutType: CalloutType = 'NOTE', key?: NodeKey) {
|
|
38
|
+
super(key);
|
|
39
|
+
this.__calloutType = calloutType;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getCalloutType(): CalloutType {
|
|
43
|
+
return this.getLatest().__calloutType;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setCalloutType(calloutType: CalloutType): void {
|
|
47
|
+
const self = this.getWritable();
|
|
48
|
+
self.__calloutType = calloutType;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- DOM ---
|
|
52
|
+
|
|
53
|
+
createDOM(): HTMLElement {
|
|
54
|
+
const wrapper = document.createElement('div');
|
|
55
|
+
wrapper.dataset.calloutKey = this.__key;
|
|
56
|
+
this._applyDOMClass(wrapper);
|
|
57
|
+
|
|
58
|
+
// Non-editable toolbar
|
|
59
|
+
const toolbar = document.createElement('div');
|
|
60
|
+
toolbar.className = 'callout-toolbar';
|
|
61
|
+
toolbar.contentEditable = 'false';
|
|
62
|
+
|
|
63
|
+
const icon = document.createElement('span');
|
|
64
|
+
icon.className = 'callout-toolbar-icon';
|
|
65
|
+
icon.textContent = CALLOUT_ICONS[this.__calloutType];
|
|
66
|
+
toolbar.appendChild(icon);
|
|
67
|
+
|
|
68
|
+
const select = document.createElement('select');
|
|
69
|
+
select.className = 'callout-toolbar-type';
|
|
70
|
+
select.setAttribute('aria-label', 'Callout type');
|
|
71
|
+
for (const type of CALLOUT_TYPES) {
|
|
72
|
+
const option = document.createElement('option');
|
|
73
|
+
option.value = type;
|
|
74
|
+
option.textContent = type.charAt(0) + type.slice(1).toLowerCase();
|
|
75
|
+
if (type === this.__calloutType) option.selected = true;
|
|
76
|
+
select.appendChild(option);
|
|
77
|
+
}
|
|
78
|
+
select.addEventListener('change', () => {
|
|
79
|
+
wrapper.dispatchEvent(
|
|
80
|
+
new CustomEvent('callout-type-change', {
|
|
81
|
+
bubbles: true,
|
|
82
|
+
detail: { nodeKey: wrapper.dataset.calloutKey, calloutType: select.value },
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
toolbar.appendChild(select);
|
|
87
|
+
|
|
88
|
+
const removeBtn = document.createElement('button');
|
|
89
|
+
removeBtn.type = 'button';
|
|
90
|
+
removeBtn.className = 'callout-toolbar-remove';
|
|
91
|
+
removeBtn.textContent = '×';
|
|
92
|
+
removeBtn.title = 'Remove callout';
|
|
93
|
+
removeBtn.setAttribute('aria-label', 'Remove callout');
|
|
94
|
+
removeBtn.addEventListener('click', () => {
|
|
95
|
+
wrapper.dispatchEvent(
|
|
96
|
+
new CustomEvent('callout-remove', {
|
|
97
|
+
bubbles: true,
|
|
98
|
+
detail: { nodeKey: wrapper.dataset.calloutKey },
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
toolbar.appendChild(removeBtn);
|
|
103
|
+
|
|
104
|
+
wrapper.appendChild(toolbar);
|
|
105
|
+
return wrapper;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private _applyDOMClass(dom: HTMLElement): void {
|
|
109
|
+
dom.className = `callout-editor callout-editor--${this.__calloutType.toLowerCase()}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
updateDOM(prevNode: CalloutNode, dom: HTMLElement): boolean {
|
|
113
|
+
if (prevNode.__calloutType !== this.__calloutType) {
|
|
114
|
+
this._applyDOMClass(dom);
|
|
115
|
+
const icon = dom.querySelector('.callout-toolbar-icon');
|
|
116
|
+
if (icon) icon.textContent = CALLOUT_ICONS[this.__calloutType];
|
|
117
|
+
const select = dom.querySelector('.callout-toolbar-type') as HTMLSelectElement | null;
|
|
118
|
+
if (select) select.value = this.__calloutType;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
exportDOM(): DOMExportOutput {
|
|
124
|
+
const el = document.createElement('blockquote');
|
|
125
|
+
el.dataset.calloutType = this.__calloutType;
|
|
126
|
+
el.className = `callout callout--${this.__calloutType.toLowerCase()}`;
|
|
127
|
+
return { element: el };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static importDOM(): DOMConversionMap | null {
|
|
131
|
+
return {
|
|
132
|
+
blockquote: (node: HTMLElement) => {
|
|
133
|
+
const type = node.dataset.calloutType;
|
|
134
|
+
if (!type || !CALLOUT_TYPES.includes(type as CalloutType)) return null;
|
|
135
|
+
return {
|
|
136
|
+
conversion: () => ({
|
|
137
|
+
node: $createCalloutNode(type as CalloutType),
|
|
138
|
+
}),
|
|
139
|
+
priority: 1,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Serialization ---
|
|
146
|
+
|
|
147
|
+
static importJSON(json: SerializedCalloutNode): CalloutNode {
|
|
148
|
+
return $createCalloutNode(json.calloutType);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exportJSON(): SerializedCalloutNode {
|
|
152
|
+
return {
|
|
153
|
+
...super.exportJSON(),
|
|
154
|
+
type: 'callout',
|
|
155
|
+
calloutType: this.__calloutType,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Behavior ---
|
|
160
|
+
|
|
161
|
+
canIndent(): boolean {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
collapseAtStart(): boolean {
|
|
166
|
+
const children = this.getChildren();
|
|
167
|
+
const parent = this.getParent();
|
|
168
|
+
if (parent) {
|
|
169
|
+
for (const child of children) {
|
|
170
|
+
this.insertBefore(child);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.remove();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
insertNewAfter(): ElementNode {
|
|
178
|
+
const lastChild = this.getLastChild();
|
|
179
|
+
if (lastChild?.getTextContent() === '') {
|
|
180
|
+
const p = $createParagraphNode();
|
|
181
|
+
this.insertAfter(p);
|
|
182
|
+
lastChild.remove();
|
|
183
|
+
return p;
|
|
184
|
+
}
|
|
185
|
+
const p = $createParagraphNode();
|
|
186
|
+
this.append(p);
|
|
187
|
+
return p;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function $createCalloutNode(calloutType: CalloutType = 'NOTE'): CalloutNode {
|
|
192
|
+
return $applyNodeReplacement(new CalloutNode(calloutType));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function $isCalloutNode(node: LexicalNode | null | undefined): node is CalloutNode {
|
|
196
|
+
return node instanceof CalloutNode;
|
|
197
|
+
}
|