@eventuras/scribo 0.8.3 → 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 +55 -38
- package/src/MarkdownEditor.css +61 -47
- package/src/MarkdownEditor.tsx +76 -29
- 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 +9 -5
- 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 -65
- 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/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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
$convertFromMarkdownString,
|
|
3
|
+
type MultilineElementTransformer,
|
|
4
|
+
TRANSFORMERS,
|
|
5
|
+
} from '@lexical/markdown';
|
|
6
|
+
import { $createParagraphNode } from 'lexical';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
$createCalloutNode,
|
|
10
|
+
$isCalloutNode,
|
|
11
|
+
CALLOUT_TYPES,
|
|
12
|
+
CalloutNode,
|
|
13
|
+
type CalloutType,
|
|
14
|
+
} from './CalloutNode';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GitHub-flavored callout syntax:
|
|
18
|
+
*
|
|
19
|
+
* ```markdown
|
|
20
|
+
* > [!NOTE]
|
|
21
|
+
* > Content line 1 with **bold** and [links](url)
|
|
22
|
+
* > Content line 2
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Supports: NOTE, TIP, IMPORTANT, WARNING, CAUTION
|
|
26
|
+
*/
|
|
27
|
+
const CALLOUT_START_REGEX = /^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/;
|
|
28
|
+
const BLOCKQUOTE_LINE_REGEX = /^>\s?(.*)$/;
|
|
29
|
+
|
|
30
|
+
export const CALLOUT_TRANSFORMER: MultilineElementTransformer = {
|
|
31
|
+
type: 'multiline-element',
|
|
32
|
+
dependencies: [CalloutNode],
|
|
33
|
+
|
|
34
|
+
regExpStart: CALLOUT_START_REGEX,
|
|
35
|
+
|
|
36
|
+
regExpEnd: {
|
|
37
|
+
regExp: /^(?!>\s?)/, // Ends when a line doesn't start with >
|
|
38
|
+
optional: true,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
export: (node, exportChildren) => {
|
|
42
|
+
if (!$isCalloutNode(node)) return null;
|
|
43
|
+
|
|
44
|
+
const calloutType = node.getCalloutType();
|
|
45
|
+
const childContent = exportChildren(node);
|
|
46
|
+
const lines = childContent.split('\n');
|
|
47
|
+
const quoted = lines.map((line) => `> ${line}`).join('\n');
|
|
48
|
+
return `> [!${calloutType}]\n${quoted}`;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
replace: (parentNode, _children, startMatch, _endMatch, linesInBetween, isImport) => {
|
|
52
|
+
const calloutType = startMatch[1] as CalloutType;
|
|
53
|
+
if (!CALLOUT_TYPES.includes(calloutType)) return false;
|
|
54
|
+
|
|
55
|
+
// Strip blockquote prefixes to get plain markdown content
|
|
56
|
+
const contentLines = (linesInBetween ?? [])
|
|
57
|
+
.map((line) => {
|
|
58
|
+
const match = line.match(BLOCKQUOTE_LINE_REGEX);
|
|
59
|
+
return match ? (match[1] ?? '') : null;
|
|
60
|
+
})
|
|
61
|
+
.filter((line): line is string => line !== null);
|
|
62
|
+
|
|
63
|
+
const innerMarkdown = contentLines.join('\n').trim();
|
|
64
|
+
|
|
65
|
+
const calloutNode = $createCalloutNode(calloutType);
|
|
66
|
+
|
|
67
|
+
if (innerMarkdown) {
|
|
68
|
+
// Parse inner markdown directly into calloutNode to avoid corrupting
|
|
69
|
+
// the outer root state with a global root.clear()
|
|
70
|
+
$convertFromMarkdownString(innerMarkdown, TRANSFORMERS, calloutNode);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Ensure at least one empty paragraph for editing
|
|
74
|
+
if (calloutNode.getChildrenSize() === 0) {
|
|
75
|
+
calloutNode.append($createParagraphNode());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// On import, parentNode is the RootNode (which can't be replaced — Lexical #53).
|
|
79
|
+
// On keyboard-shortcut, parentNode is a ParagraphNode we should replace.
|
|
80
|
+
if (isImport) {
|
|
81
|
+
parentNode.append(calloutNode);
|
|
82
|
+
} else {
|
|
83
|
+
parentNode.replace(calloutNode);
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { $findMatchingParent } from '@lexical/utils';
|
|
2
|
+
import { $createParagraphNode, $getSelection, $isRangeSelection } from 'lexical';
|
|
3
|
+
|
|
4
|
+
import type { ScriboPlugin } from '../../types';
|
|
5
|
+
import { CalloutPlugin } from './CalloutComponent';
|
|
6
|
+
import { $createCalloutNode, CalloutNode } from './CalloutNode';
|
|
7
|
+
import { CALLOUT_TRANSFORMER } from './CalloutTransformer';
|
|
8
|
+
|
|
9
|
+
export const calloutPlugin: ScriboPlugin = {
|
|
10
|
+
name: 'callout',
|
|
11
|
+
nodes: [CalloutNode],
|
|
12
|
+
transformers: [CALLOUT_TRANSFORMER],
|
|
13
|
+
editorPlugins: [CalloutPlugin],
|
|
14
|
+
toolbarButtons: [
|
|
15
|
+
{
|
|
16
|
+
key: 'callout',
|
|
17
|
+
label: 'Callout',
|
|
18
|
+
iconClass: 'alert',
|
|
19
|
+
onSelect: (editor) => {
|
|
20
|
+
editor.update(() => {
|
|
21
|
+
const selection = $getSelection();
|
|
22
|
+
if (!$isRangeSelection(selection)) return;
|
|
23
|
+
|
|
24
|
+
const anchor = selection.anchor.getNode();
|
|
25
|
+
const topLevel =
|
|
26
|
+
$findMatchingParent(anchor, (n) => n.getParent()?.getType() === 'root') ?? anchor;
|
|
27
|
+
|
|
28
|
+
const calloutNode = $createCalloutNode('NOTE');
|
|
29
|
+
const p = $createParagraphNode();
|
|
30
|
+
calloutNode.append(p);
|
|
31
|
+
topLevel.insertAfter(calloutNode);
|
|
32
|
+
p.selectEnd();
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CalloutPlugin } from './CalloutComponent';
|
|
2
|
+
export type { CalloutType, SerializedCalloutNode } from './CalloutNode';
|
|
3
|
+
export { $createCalloutNode, $isCalloutNode, CALLOUT_TYPES, CalloutNode } from './CalloutNode';
|
|
4
|
+
export { CALLOUT_TRANSFORMER } from './CalloutTransformer';
|
|
5
|
+
export { calloutPlugin } from './calloutPlugin';
|