@moldable-ai/editor 0.1.8 → 0.1.11
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/components/markdown-editor.d.ts +5 -0
- package/dist/components/markdown-editor.d.ts.map +1 -1
- package/dist/components/markdown-editor.js +15 -6
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/lexical/file-reference-plugin.d.ts +9 -0
- package/dist/lib/lexical/file-reference-plugin.d.ts.map +1 -0
- package/dist/lib/lexical/file-reference-plugin.js +99 -0
- package/dist/lib/lexical/headless-editor.d.ts.map +1 -1
- package/dist/lib/lexical/headless-editor.js +4 -0
- package/dist/lib/lexical/image-component.d.ts +8 -0
- package/dist/lib/lexical/image-component.d.ts.map +1 -0
- package/dist/lib/lexical/image-component.js +99 -0
- package/dist/lib/lexical/image-node.d.ts +36 -0
- package/dist/lib/lexical/image-node.d.ts.map +1 -0
- package/dist/lib/lexical/image-node.js +105 -0
- package/dist/lib/lexical/markdown-transformers.d.ts +4 -1
- package/dist/lib/lexical/markdown-transformers.d.ts.map +1 -1
- package/dist/lib/lexical/markdown-transformers.js +80 -0
- package/dist/lib/lexical/media-context.d.ts +7 -0
- package/dist/lib/lexical/media-context.d.ts.map +1 -0
- package/dist/lib/lexical/media-context.js +7 -0
- package/dist/lib/lexical/media-node.d.ts +32 -0
- package/dist/lib/lexical/media-node.d.ts.map +1 -0
- package/dist/lib/lexical/media-node.js +97 -0
- package/dist/lib/lexical/media-plugin.d.ts +12 -0
- package/dist/lib/lexical/media-plugin.d.ts.map +1 -0
- package/dist/lib/lexical/media-plugin.js +139 -0
- package/dist/lib/lexical/sync-plugin.d.ts +1 -0
- package/dist/lib/lexical/sync-plugin.d.ts.map +1 -1
- package/dist/lib/lexical/sync-plugin.js +13 -5
- package/package.json +2 -2
- package/src/styles/index.css +117 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type MarkdownReferenceOption } from '../lib/lexical/file-reference-plugin';
|
|
1
2
|
import { type MarkdownEditorHandle } from '../lib/lexical/find-replace-plugin';
|
|
3
|
+
import { type MarkdownMediaUpload } from '../lib/lexical/media-plugin';
|
|
2
4
|
export interface MarkdownEditorProps {
|
|
3
5
|
value: string;
|
|
4
6
|
onChange: (value: string) => void;
|
|
@@ -13,6 +15,9 @@ export interface MarkdownEditorProps {
|
|
|
13
15
|
findQuery?: string;
|
|
14
16
|
currentFindMatch?: number;
|
|
15
17
|
onFindMatchCountChange?: (count: number) => void;
|
|
18
|
+
onMediaUpload?: MarkdownMediaUpload;
|
|
19
|
+
resolveMediaUrl?: (src: string) => string;
|
|
20
|
+
referenceOptions?: MarkdownReferenceOption[];
|
|
16
21
|
}
|
|
17
22
|
export declare const MarkdownEditor: import("react").ForwardRefExoticComponent<MarkdownEditorProps & import("react").RefAttributes<MarkdownEditorHandle>>;
|
|
18
23
|
export type { MarkdownEditorHandle };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-editor.d.ts","sourceRoot":"","sources":["../../src/components/markdown-editor.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAA;
|
|
1
|
+
{"version":3,"file":"markdown-editor.d.ts","sourceRoot":"","sources":["../../src/components/markdown-editor.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAA;AAS3C,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,6BAA6B,CAAA;AAkBpC,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAChD,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,gBAAgB,CAAC,EAAE,uBAAuB,EAAE,CAAA;CAC7C;AAED,eAAO,MAAM,cAAc,sHAmJzB,CAAA;AAEF,YAAY,EAAE,oBAAoB,EAAE,CAAA"}
|
|
@@ -18,9 +18,14 @@ import { cn } from '@moldable-ai/ui';
|
|
|
18
18
|
import { LINK_MATCHERS } from '../lib/lexical/auto-link-config';
|
|
19
19
|
import { ClickableLinkPlugin } from '../lib/lexical/clickable-link-plugin';
|
|
20
20
|
import { editorTheme } from '../lib/lexical/editor-theme';
|
|
21
|
+
import { FileReferencePlugin, } from '../lib/lexical/file-reference-plugin';
|
|
21
22
|
import { FIND_REPLACE_UPDATE_TAG, FindReplacePlugin, } from '../lib/lexical/find-replace-plugin';
|
|
23
|
+
import { ImageNode } from '../lib/lexical/image-node';
|
|
22
24
|
import { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, } from '../lib/lexical/markdown-transformers';
|
|
23
|
-
import {
|
|
25
|
+
import { MediaProvider } from '../lib/lexical/media-context';
|
|
26
|
+
import { MediaNode } from '../lib/lexical/media-node';
|
|
27
|
+
import { MediaPasteDropPlugin, } from '../lib/lexical/media-plugin';
|
|
28
|
+
import { EXTERNAL_MARKDOWN_SYNC_TAG, SyncPlugin, } from '../lib/lexical/sync-plugin';
|
|
24
29
|
import { FloatingToolbar } from './floating-toolbar';
|
|
25
30
|
import { CodeHighlightNode, CodeNode } from '@lexical/code';
|
|
26
31
|
import { HorizontalRuleNode } from '@lexical/extension';
|
|
@@ -29,7 +34,7 @@ import { ListItemNode, ListNode } from '@lexical/list';
|
|
|
29
34
|
import { MarkNode } from '@lexical/mark';
|
|
30
35
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
31
36
|
import { $createParagraphNode, $getRoot } from 'lexical';
|
|
32
|
-
export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChange, placeholder = 'Write something...', disabled = false, autoFocus = false, className, contentClassName, minHeight = '150px', maxHeight = '50vh', hideMarkdownHint = false, findQuery = '', currentFindMatch = 0, onFindMatchCountChange, }, ref) {
|
|
37
|
+
export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChange, placeholder = 'Write something...', disabled = false, autoFocus = false, className, contentClassName, minHeight = '150px', maxHeight = '50vh', hideMarkdownHint = false, findQuery = '', currentFindMatch = 0, onFindMatchCountChange, onMediaUpload, resolveMediaUrl, referenceOptions, }, ref) {
|
|
33
38
|
const initialValueRef = useRef(value);
|
|
34
39
|
const initialConfig = {
|
|
35
40
|
namespace: 'MarkdownEditor',
|
|
@@ -40,8 +45,9 @@ export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChan
|
|
|
40
45
|
markdown: value,
|
|
41
46
|
transformers: markdownTransformers,
|
|
42
47
|
node: root,
|
|
43
|
-
shouldPreserveNewLines:
|
|
48
|
+
shouldPreserveNewLines: false,
|
|
44
49
|
});
|
|
50
|
+
root.selectEnd();
|
|
45
51
|
}
|
|
46
52
|
else {
|
|
47
53
|
root.append($createParagraphNode());
|
|
@@ -58,6 +64,8 @@ export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChan
|
|
|
58
64
|
LinkNode,
|
|
59
65
|
MarkNode,
|
|
60
66
|
HorizontalRuleNode,
|
|
67
|
+
ImageNode,
|
|
68
|
+
MediaNode,
|
|
61
69
|
],
|
|
62
70
|
onError: (error) => {
|
|
63
71
|
console.error('MarkdownEditor error:', error);
|
|
@@ -68,11 +76,12 @@ export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChan
|
|
|
68
76
|
const handleChange = useCallback((editorState, _editor, tags) => {
|
|
69
77
|
if (tags.has(FIND_REPLACE_UPDATE_TAG))
|
|
70
78
|
return;
|
|
79
|
+
if (tags.has(EXTERNAL_MARKDOWN_SYNC_TAG))
|
|
80
|
+
return;
|
|
71
81
|
editorState.read(() => {
|
|
72
|
-
// Use shouldPreserveNewLines: true to preserve empty paragraphs as extra newlines
|
|
73
82
|
const markdown = $convertToMarkdownString({
|
|
74
83
|
transformers: markdownTransformers,
|
|
75
|
-
shouldPreserveNewLines:
|
|
84
|
+
shouldPreserveNewLines: false,
|
|
76
85
|
});
|
|
77
86
|
// Only call onChange if the value actually changed
|
|
78
87
|
if (markdown !== initialValueRef.current) {
|
|
@@ -81,5 +90,5 @@ export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChan
|
|
|
81
90
|
}
|
|
82
91
|
});
|
|
83
92
|
}, [onChange]);
|
|
84
|
-
return (_jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsxs("div", { className: cn('relative h-full', className), "data-lexical-editor-container": true, children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cn('prose prose-sm caret-primary dark:prose-invert max-w-none resize-none overflow-auto bg-transparent outline-none', disabled && 'cursor-not-allowed opacity-50', contentClassName), style: { minHeight, maxHeight }, "aria-placeholder": placeholder, "data-lexical-editor": "true", placeholder: _jsx("div", { className:
|
|
93
|
+
return (_jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(MediaProvider, { value: { resolveMediaUrl }, children: _jsxs("div", { className: cn('relative h-full', className), "data-lexical-editor-container": true, children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cn('prose prose-sm caret-primary dark:prose-invert max-w-none resize-none overflow-auto bg-transparent outline-none', disabled && 'cursor-not-allowed opacity-50', contentClassName), style: { minHeight, maxHeight }, "aria-placeholder": placeholder, "data-lexical-editor": "true", placeholder: _jsx("div", { className: cn('pointer-events-none absolute inset-x-0 top-1 overflow-hidden whitespace-nowrap', contentClassName, 'text-muted-foreground text-sm opacity-75'), children: placeholder }) }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, {}), autoFocus && _jsx(AutoFocusPlugin, {}), _jsx(ListPlugin, {}), _jsx(CheckListPlugin, {}), _jsx(TabIndentationPlugin, {}), _jsx(LinkPlugin, {}), _jsx(AutoLinkPlugin, { matchers: LINK_MATCHERS }), _jsx(ClickableLinkPlugin, {}), _jsx(FloatingToolbar, {}), _jsx(MarkdownShortcutPlugin, { transformers: markdownTransformers }), _jsx(FileReferencePlugin, { references: referenceOptions }), _jsx(MediaPasteDropPlugin, { onMediaUpload: onMediaUpload }), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(FindReplacePlugin, { query: findQuery, currentIndex: currentFindMatch, onMatchCountChange: onFindMatchCountChange, editorRef: ref }), _jsx(SyncPlugin, { value: value, initialValueRef: initialValueRef }), !hideMarkdownHint && (_jsx("div", { className: "mt-1 flex justify-end", children: _jsx("span", { className: "text-muted-foreground/60 text-xs", children: "Markdown supported" }) }))] }) }) }));
|
|
85
94
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { MarkdownEditor, type MarkdownEditorHandle, type MarkdownEditorProps, } from './components/markdown-editor';
|
|
2
2
|
export { FloatingToolbar } from './components/floating-toolbar';
|
|
3
|
-
export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, } from './lib/lexical/markdown-transformers';
|
|
3
|
+
export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, IMAGE, OBSIDIAN_IMAGE_EMBED, MEDIA, } from './lib/lexical/markdown-transformers';
|
|
4
|
+
export { ImageNode, $createImageNode, $isImageNode, type ImagePayload, } from './lib/lexical/image-node';
|
|
5
|
+
export { MediaNode, $createMediaNode, $isMediaNode, type MediaKind, type MediaPayload, } from './lib/lexical/media-node';
|
|
6
|
+
export { MediaPasteDropPlugin, type MarkdownMediaKind, type MarkdownMediaUpload, type MarkdownMediaUploadResult, } from './lib/lexical/media-plugin';
|
|
7
|
+
export { FileReferencePlugin, type MarkdownReferenceOption, } from './lib/lexical/file-reference-plugin';
|
|
4
8
|
export { editorTheme } from './lib/lexical/editor-theme';
|
|
5
9
|
export { LINK_MATCHERS, URL_REGEX, EMAIL_REGEX, } from './lib/lexical/auto-link-config';
|
|
6
10
|
export { useFloatingToolbar, type FloatingToolbarState, } from './lib/lexical/floating-toolbar-plugin';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,GACzB,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAG/D,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACpB,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,GACzB,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAG/D,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACpB,EAAE,EACF,KAAK,EACL,oBAAoB,EACpB,KAAK,GACN,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,KAAK,SAAS,EACd,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,GAC/B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACL,mBAAmB,EACnB,KAAK,uBAAuB,GAC7B,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EACL,aAAa,EACb,SAAS,EACT,WAAW,GACZ,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,kBAAkB,EAClB,KAAK,oBAAoB,GAC1B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,EACL,mBAAmB,EACnB,SAAS,GACV,MAAM,qCAAqC,CAAA;AAG5C,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
export { MarkdownEditor, } from './components/markdown-editor';
|
|
3
3
|
export { FloatingToolbar } from './components/floating-toolbar';
|
|
4
4
|
// Lexical utilities (for advanced use cases)
|
|
5
|
-
export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, } from './lib/lexical/markdown-transformers';
|
|
5
|
+
export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, IMAGE, OBSIDIAN_IMAGE_EMBED, MEDIA, } from './lib/lexical/markdown-transformers';
|
|
6
|
+
export { ImageNode, $createImageNode, $isImageNode, } from './lib/lexical/image-node';
|
|
7
|
+
export { MediaNode, $createMediaNode, $isMediaNode, } from './lib/lexical/media-node';
|
|
8
|
+
export { MediaPasteDropPlugin, } from './lib/lexical/media-plugin';
|
|
9
|
+
export { FileReferencePlugin, } from './lib/lexical/file-reference-plugin';
|
|
6
10
|
export { editorTheme } from './lib/lexical/editor-theme';
|
|
7
11
|
export { LINK_MATCHERS, URL_REGEX, EMAIL_REGEX, } from './lib/lexical/auto-link-config';
|
|
8
12
|
export { useFloatingToolbar, } from './lib/lexical/floating-toolbar-plugin';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface MarkdownReferenceOption {
|
|
2
|
+
path: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
kind?: 'note' | 'file';
|
|
5
|
+
}
|
|
6
|
+
export declare function FileReferencePlugin({ references, }: {
|
|
7
|
+
references?: MarkdownReferenceOption[];
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=file-reference-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-reference-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/file-reference-plugin.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAqDD,wBAAgB,mBAAmB,CAAC,EAClC,UAAU,GACX,EAAE;IACD,UAAU,CAAC,EAAE,uBAAuB,EAAE,CAAA;CACvC,2CAsGA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { LexicalTypeaheadMenuPlugin, MenuOption, useBasicTypeaheadTriggerMatch, } from '@lexical/react/LexicalTypeaheadMenuPlugin';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import { cn } from '@moldable-ai/ui';
|
|
7
|
+
import { $createTextNode } from 'lexical';
|
|
8
|
+
class FileReferenceOption extends MenuOption {
|
|
9
|
+
path;
|
|
10
|
+
title;
|
|
11
|
+
isCreate;
|
|
12
|
+
constructor(path, title, isCreate = false) {
|
|
13
|
+
super(`${isCreate ? 'create' : 'reference'}:${path}`);
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.title = title;
|
|
16
|
+
this.isCreate = isCreate;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function referenceTarget(path) {
|
|
20
|
+
return path.replace(/\.md$/, '');
|
|
21
|
+
}
|
|
22
|
+
function titleFromPath(path) {
|
|
23
|
+
return path.split('/').pop()?.replace(/\.md$/, '') ?? path;
|
|
24
|
+
}
|
|
25
|
+
function slugifyReference(value) {
|
|
26
|
+
const segments = value
|
|
27
|
+
.split('/')
|
|
28
|
+
.map((segment) => segment
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9 -]/g, '')
|
|
32
|
+
.replace(/\s+/g, '-')
|
|
33
|
+
.replace(/^-+|-+$/g, ''))
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
return segments.join('/') || 'untitled';
|
|
36
|
+
}
|
|
37
|
+
function referenceScore(option, query) {
|
|
38
|
+
const needle = query.trim().toLowerCase();
|
|
39
|
+
if (!needle)
|
|
40
|
+
return 1;
|
|
41
|
+
const title = (option.title ?? titleFromPath(option.path)).toLowerCase();
|
|
42
|
+
const path = option.path.toLowerCase();
|
|
43
|
+
if (title === needle || path === needle || path === `${needle}.md`)
|
|
44
|
+
return 100;
|
|
45
|
+
if (title.startsWith(needle))
|
|
46
|
+
return 80;
|
|
47
|
+
if (path.startsWith(needle))
|
|
48
|
+
return 70;
|
|
49
|
+
if (title.includes(needle))
|
|
50
|
+
return 50;
|
|
51
|
+
if (path.includes(needle))
|
|
52
|
+
return 40;
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
export function FileReferencePlugin({ references, }) {
|
|
56
|
+
const [query, setQuery] = useState(null);
|
|
57
|
+
const triggerFn = useBasicTypeaheadTriggerMatch('@', {
|
|
58
|
+
allowWhitespace: true,
|
|
59
|
+
maxLength: 80,
|
|
60
|
+
minLength: 0,
|
|
61
|
+
});
|
|
62
|
+
const options = useMemo(() => {
|
|
63
|
+
const matching = (references ?? [])
|
|
64
|
+
.map((reference) => ({
|
|
65
|
+
reference,
|
|
66
|
+
score: referenceScore(reference, query ?? ''),
|
|
67
|
+
}))
|
|
68
|
+
.filter(({ score }) => score > 0)
|
|
69
|
+
.sort((a, b) => b.score - a.score || a.reference.path.localeCompare(b.reference.path))
|
|
70
|
+
.slice(0, 8)
|
|
71
|
+
.map(({ reference }) => new FileReferenceOption(reference.path, reference.title ?? titleFromPath(reference.path)));
|
|
72
|
+
const trimmed = query?.trim();
|
|
73
|
+
if (!trimmed)
|
|
74
|
+
return matching;
|
|
75
|
+
const createPath = `${slugifyReference(trimmed)}.md`;
|
|
76
|
+
const hasExact = (references ?? []).some((reference) => reference.path.toLowerCase() === createPath.toLowerCase() ||
|
|
77
|
+
reference.path.replace(/\.md$/, '').toLowerCase() ===
|
|
78
|
+
createPath.replace(/\.md$/, '').toLowerCase());
|
|
79
|
+
return hasExact
|
|
80
|
+
? matching
|
|
81
|
+
: [
|
|
82
|
+
...matching,
|
|
83
|
+
new FileReferenceOption(createPath, `Create ${trimmed}`, true),
|
|
84
|
+
].slice(0, 9);
|
|
85
|
+
}, [query, references]);
|
|
86
|
+
return (_jsx(LexicalTypeaheadMenuPlugin, { triggerFn: triggerFn, options: options, onQueryChange: setQuery, preselectFirstItem: true, onSelectOption: (option, textNodeContainingQuery, closeMenu) => {
|
|
87
|
+
if (!textNodeContainingQuery)
|
|
88
|
+
return;
|
|
89
|
+
const node = $createTextNode(`[[${referenceTarget(option.path)}]]`);
|
|
90
|
+
textNodeContainingQuery.replace(node);
|
|
91
|
+
node.selectEnd();
|
|
92
|
+
closeMenu();
|
|
93
|
+
}, menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
|
94
|
+
if (!anchorElementRef.current || options.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
return createPortal(_jsx("div", { className: "moldable-editor-reference-menu", children: options.map((option, index) => (_jsxs("button", { ref: option.setRefElement, type: "button", role: "option", "aria-selected": selectedIndex === index, className: cn('moldable-editor-reference-option', selectedIndex === index &&
|
|
97
|
+
'moldable-editor-reference-option-selected'), onMouseEnter: () => setHighlightedIndex(index), onMouseDown: (event) => event.preventDefault(), onClick: () => selectOptionAndCleanUp(option), children: [_jsx("span", { className: "moldable-editor-reference-title", children: option.title }), _jsx("span", { className: "moldable-editor-reference-path", children: option.isCreate ? 'New note' : option.path })] }, option.key))) }), anchorElementRef.current);
|
|
98
|
+
} }));
|
|
99
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headless-editor.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/headless-editor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"headless-editor.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/headless-editor.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,wBAAgB,4BAA4B,oCAqB3C"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { editorTheme } from './editor-theme';
|
|
2
|
+
import { ImageNode } from './image-node';
|
|
3
|
+
import { MediaNode } from './media-node';
|
|
2
4
|
import { CodeHighlightNode, CodeNode } from '@lexical/code';
|
|
3
5
|
import { HorizontalRuleNode } from '@lexical/extension';
|
|
4
6
|
import { createHeadlessEditor } from '@lexical/headless';
|
|
@@ -23,6 +25,8 @@ export function createMoldableHeadlessEditor() {
|
|
|
23
25
|
LinkNode,
|
|
24
26
|
MarkNode,
|
|
25
27
|
HorizontalRuleNode,
|
|
28
|
+
ImageNode,
|
|
29
|
+
MediaNode,
|
|
26
30
|
],
|
|
27
31
|
theme: editorTheme,
|
|
28
32
|
onError: (error) => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type NodeKey } from 'lexical';
|
|
2
|
+
export declare function ImageComponent({ altText, nodeKey, src, width, }: {
|
|
3
|
+
altText: string;
|
|
4
|
+
nodeKey: NodeKey;
|
|
5
|
+
src: string;
|
|
6
|
+
width?: number;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=image-component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-component.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/image-component.tsx"],"names":[],"mappings":"AAcA,OAAO,EAQL,KAAK,OAAO,EACb,MAAM,SAAS,CAAA;AAQhB,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,OAAO,EACP,GAAG,EACH,KAAK,GACN,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,2CAsIA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
+
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
|
|
5
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
6
|
+
import { $isImageNode } from './image-node';
|
|
7
|
+
import { useMediaResolver } from './media-context';
|
|
8
|
+
import { mergeRegister } from '@lexical/utils';
|
|
9
|
+
import { $getNodeByKey, $getSelection, $isNodeSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, } from 'lexical';
|
|
10
|
+
const MIN_IMAGE_WIDTH = 96;
|
|
11
|
+
function clamp(value, min, max) {
|
|
12
|
+
return Math.min(Math.max(value, min), max);
|
|
13
|
+
}
|
|
14
|
+
export function ImageComponent({ altText, nodeKey, src, width, }) {
|
|
15
|
+
const [editor] = useLexicalComposerContext();
|
|
16
|
+
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
|
|
17
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
18
|
+
const imageRef = useRef(null);
|
|
19
|
+
const resizeStartRef = useRef(null);
|
|
20
|
+
const resolveMediaUrl = useMediaResolver();
|
|
21
|
+
const resolvedSrc = resolveMediaUrl?.(src) ?? src;
|
|
22
|
+
const removeSelectedImage = useCallback(() => {
|
|
23
|
+
const selection = $getSelection();
|
|
24
|
+
if (!$isNodeSelection(selection) || !selection.has(nodeKey))
|
|
25
|
+
return false;
|
|
26
|
+
const node = $getNodeByKey(nodeKey);
|
|
27
|
+
if (!$isImageNode(node))
|
|
28
|
+
return false;
|
|
29
|
+
node.remove();
|
|
30
|
+
return true;
|
|
31
|
+
}, [nodeKey]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
return mergeRegister(editor.registerCommand(CLICK_COMMAND, (event) => {
|
|
34
|
+
if (isResizing)
|
|
35
|
+
return true;
|
|
36
|
+
if (event.target !== imageRef.current)
|
|
37
|
+
return false;
|
|
38
|
+
if (!event.shiftKey)
|
|
39
|
+
clearSelection();
|
|
40
|
+
setSelected(!isSelected);
|
|
41
|
+
return true;
|
|
42
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_DELETE_COMMAND, removeSelectedImage, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_BACKSPACE_COMMAND, removeSelectedImage, COMMAND_PRIORITY_LOW));
|
|
43
|
+
}, [
|
|
44
|
+
clearSelection,
|
|
45
|
+
editor,
|
|
46
|
+
isResizing,
|
|
47
|
+
isSelected,
|
|
48
|
+
removeSelectedImage,
|
|
49
|
+
setSelected,
|
|
50
|
+
]);
|
|
51
|
+
const handleResizePointerDown = (event) => {
|
|
52
|
+
const image = imageRef.current;
|
|
53
|
+
const root = editor.getRootElement();
|
|
54
|
+
if (!image || !root)
|
|
55
|
+
return;
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
const rootRect = root.getBoundingClientRect();
|
|
59
|
+
const imageRect = image.getBoundingClientRect();
|
|
60
|
+
resizeStartRef.current = {
|
|
61
|
+
maxWidth: Math.max(MIN_IMAGE_WIDTH, rootRect.width - 48),
|
|
62
|
+
startWidth: imageRect.width,
|
|
63
|
+
startX: event.clientX,
|
|
64
|
+
};
|
|
65
|
+
setIsResizing(true);
|
|
66
|
+
const handlePointerMove = (pointerEvent) => {
|
|
67
|
+
const start = resizeStartRef.current;
|
|
68
|
+
if (!start)
|
|
69
|
+
return;
|
|
70
|
+
const nextWidth = clamp(Math.round(start.startWidth + pointerEvent.clientX - start.startX), MIN_IMAGE_WIDTH, start.maxWidth);
|
|
71
|
+
if (imageRef.current)
|
|
72
|
+
imageRef.current.style.width = `${nextWidth}px`;
|
|
73
|
+
};
|
|
74
|
+
const handlePointerUp = () => {
|
|
75
|
+
const nextWidth = imageRef.current?.getBoundingClientRect().width;
|
|
76
|
+
resizeStartRef.current = null;
|
|
77
|
+
setIsResizing(false);
|
|
78
|
+
document.body.style.userSelect = '';
|
|
79
|
+
document.removeEventListener('pointermove', handlePointerMove);
|
|
80
|
+
document.removeEventListener('pointerup', handlePointerUp);
|
|
81
|
+
if (!nextWidth)
|
|
82
|
+
return;
|
|
83
|
+
editor.update(() => {
|
|
84
|
+
const node = $getNodeByKey(nodeKey);
|
|
85
|
+
if ($isImageNode(node))
|
|
86
|
+
node.setWidth(Math.round(nextWidth));
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
document.body.style.userSelect = 'none';
|
|
90
|
+
document.addEventListener('pointermove', handlePointerMove);
|
|
91
|
+
document.addEventListener('pointerup', handlePointerUp);
|
|
92
|
+
};
|
|
93
|
+
return (_jsxs("span", { className: [
|
|
94
|
+
'lexical-image-shell',
|
|
95
|
+
(isSelected || isResizing) && 'lexical-image-shell-selected',
|
|
96
|
+
]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(' '), draggable: isSelected && !isResizing, children: [_jsx("img", { ref: imageRef, src: resolvedSrc, alt: altText, className: "lexical-image", draggable: false, style: { width: width ? `${width}px` : undefined } }), (isSelected || isResizing) && (_jsx("span", { "aria-hidden": "true", className: "lexical-image-resize-handle", onPointerDown: handleResizePointerDown }))] }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical';
|
|
3
|
+
import { DecoratorNode } from 'lexical';
|
|
4
|
+
export type ImagePayload = {
|
|
5
|
+
altText?: string;
|
|
6
|
+
key?: NodeKey;
|
|
7
|
+
src: string;
|
|
8
|
+
width?: number;
|
|
9
|
+
};
|
|
10
|
+
export type SerializedImageNode = SerializedLexicalNode & {
|
|
11
|
+
altText: string;
|
|
12
|
+
src: string;
|
|
13
|
+
width?: number;
|
|
14
|
+
};
|
|
15
|
+
export declare class ImageNode extends DecoratorNode<JSX.Element> {
|
|
16
|
+
__altText: string;
|
|
17
|
+
__src: string;
|
|
18
|
+
__width?: number;
|
|
19
|
+
static getType(): string;
|
|
20
|
+
static clone(node: ImageNode): ImageNode;
|
|
21
|
+
static importJSON(serializedNode: SerializedImageNode): ImageNode;
|
|
22
|
+
static importDOM(): DOMConversionMap | null;
|
|
23
|
+
constructor(src: string, altText?: string, width?: number, key?: NodeKey);
|
|
24
|
+
exportJSON(): SerializedImageNode;
|
|
25
|
+
exportDOM(): DOMExportOutput;
|
|
26
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
27
|
+
updateDOM(): false;
|
|
28
|
+
getSrc(): string;
|
|
29
|
+
getAltText(): string;
|
|
30
|
+
getWidth(): number | undefined;
|
|
31
|
+
setWidth(width: number | undefined): this;
|
|
32
|
+
decorate(): JSX.Element;
|
|
33
|
+
}
|
|
34
|
+
export declare function $createImageNode({ altText, key, src, width, }: ImagePayload): ImageNode;
|
|
35
|
+
export declare function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode;
|
|
36
|
+
//# sourceMappingURL=image-node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-node.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/image-node.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,KAAK,EACV,gBAAgB,EAEhB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,OAAO,EACP,qBAAqB,EACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAyB,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9D,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG;IACxD,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAyBD,qBAAa,SAAU,SAAQ,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS;IAIxC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,mBAAmB,GAAG,SAAS;IAQjE,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;gBAS/B,GAAG,EAAE,MAAM,EAAE,OAAO,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO;IAOpE,UAAU,IAAI,mBAAmB;IASjC,SAAS,IAAI,eAAe;IAQ5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAO5C,SAAS,IAAI,KAAK;IAIlB,MAAM,IAAI,MAAM;IAIhB,UAAU,IAAI,MAAM;IAIpB,QAAQ,IAAI,MAAM,GAAG,SAAS;IAI9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAMzC,QAAQ,IAAI,GAAG,CAAC,OAAO;CAUxB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,GAAG,EACH,GAAG,EACH,KAAK,GACN,EAAE,YAAY,GAAG,SAAS,CAE1B;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GACnC,IAAI,IAAI,SAAS,CAEnB"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ImageComponent } from './image-component';
|
|
4
|
+
import { $applyNodeReplacement, DecoratorNode } from 'lexical';
|
|
5
|
+
function $convertImageElement(domNode) {
|
|
6
|
+
const image = domNode;
|
|
7
|
+
const src = image.getAttribute('src');
|
|
8
|
+
if (!src || src.startsWith('file:///'))
|
|
9
|
+
return null;
|
|
10
|
+
const widthAttribute = image.getAttribute('width');
|
|
11
|
+
const styleWidth = image.style.width.endsWith('px')
|
|
12
|
+
? Number.parseInt(image.style.width, 10)
|
|
13
|
+
: undefined;
|
|
14
|
+
const width = widthAttribute
|
|
15
|
+
? Number.parseInt(widthAttribute, 10)
|
|
16
|
+
: styleWidth;
|
|
17
|
+
return {
|
|
18
|
+
node: $createImageNode({
|
|
19
|
+
altText: image.getAttribute('alt') ?? '',
|
|
20
|
+
src,
|
|
21
|
+
width: Number.isFinite(width) ? width : undefined,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export class ImageNode extends DecoratorNode {
|
|
26
|
+
__altText;
|
|
27
|
+
__src;
|
|
28
|
+
__width;
|
|
29
|
+
static getType() {
|
|
30
|
+
return 'image';
|
|
31
|
+
}
|
|
32
|
+
static clone(node) {
|
|
33
|
+
return new ImageNode(node.__src, node.__altText, node.__width, node.__key);
|
|
34
|
+
}
|
|
35
|
+
static importJSON(serializedNode) {
|
|
36
|
+
return $createImageNode({
|
|
37
|
+
altText: serializedNode.altText,
|
|
38
|
+
src: serializedNode.src,
|
|
39
|
+
width: serializedNode.width,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
static importDOM() {
|
|
43
|
+
return {
|
|
44
|
+
img: () => ({
|
|
45
|
+
conversion: $convertImageElement,
|
|
46
|
+
priority: 0,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
constructor(src, altText = '', width, key) {
|
|
51
|
+
super(key);
|
|
52
|
+
this.__src = src;
|
|
53
|
+
this.__altText = altText;
|
|
54
|
+
this.__width = width;
|
|
55
|
+
}
|
|
56
|
+
exportJSON() {
|
|
57
|
+
return {
|
|
58
|
+
...super.exportJSON(),
|
|
59
|
+
altText: this.__altText,
|
|
60
|
+
src: this.__src,
|
|
61
|
+
width: this.__width,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
exportDOM() {
|
|
65
|
+
const element = document.createElement('img');
|
|
66
|
+
element.setAttribute('src', this.__src);
|
|
67
|
+
element.setAttribute('alt', this.__altText);
|
|
68
|
+
if (this.__width)
|
|
69
|
+
element.setAttribute('width', String(this.__width));
|
|
70
|
+
return { element };
|
|
71
|
+
}
|
|
72
|
+
createDOM(config) {
|
|
73
|
+
const element = document.createElement('span');
|
|
74
|
+
const className = config.theme.image;
|
|
75
|
+
if (typeof className === 'string')
|
|
76
|
+
element.className = className;
|
|
77
|
+
return element;
|
|
78
|
+
}
|
|
79
|
+
updateDOM() {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
getSrc() {
|
|
83
|
+
return this.getLatest().__src;
|
|
84
|
+
}
|
|
85
|
+
getAltText() {
|
|
86
|
+
return this.getLatest().__altText;
|
|
87
|
+
}
|
|
88
|
+
getWidth() {
|
|
89
|
+
return this.getLatest().__width;
|
|
90
|
+
}
|
|
91
|
+
setWidth(width) {
|
|
92
|
+
const writable = this.getWritable();
|
|
93
|
+
writable.__width = width;
|
|
94
|
+
return writable;
|
|
95
|
+
}
|
|
96
|
+
decorate() {
|
|
97
|
+
return (_jsx(ImageComponent, { altText: this.__altText, nodeKey: this.getKey(), src: this.__src, width: this.__width }));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function $createImageNode({ altText, key, src, width, }) {
|
|
101
|
+
return $applyNodeReplacement(new ImageNode(src, altText, width, key));
|
|
102
|
+
}
|
|
103
|
+
export function $isImageNode(node) {
|
|
104
|
+
return node instanceof ImageNode;
|
|
105
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { ElementTransformer, Transformer } from '@lexical/markdown';
|
|
1
|
+
import type { ElementTransformer, TextMatchTransformer, Transformer } from '@lexical/markdown';
|
|
2
2
|
import type { ElementNode } from 'lexical';
|
|
3
3
|
/**
|
|
4
4
|
* Horizontal rule transformer - supports ---, ***, and ___
|
|
5
5
|
*/
|
|
6
6
|
export declare const HR: ElementTransformer;
|
|
7
|
+
export declare const IMAGE: TextMatchTransformer;
|
|
8
|
+
export declare const OBSIDIAN_IMAGE_EMBED: TextMatchTransformer;
|
|
9
|
+
export declare const MEDIA: TextMatchTransformer;
|
|
7
10
|
/**
|
|
8
11
|
* All markdown transformers
|
|
9
12
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-transformers.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/markdown-transformers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"markdown-transformers.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/markdown-transformers.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,WAAW,EACZ,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,SAAS,CAAA;AAGvD;;GAEG;AACH,eAAO,MAAM,EAAE,EAAE,kBAgBhB,CAAA;AAUD,eAAO,MAAM,KAAK,EAAE,oBAuBnB,CAAA;AAED,eAAO,MAAM,oBAAoB,EAAE,oBA2BlC,CAAA;AAMD,eAAO,MAAM,KAAK,EAAE,oBAiBnB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,WAAW,CAUnD,CAAA;AAED;;GAEG;AACH,UAAU,0BAA0B;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,wBAAwB,CAAC,EAAE,OAAO,CAAA;CACnC;AAED;;GAEG;AACH,UAAU,wBAAwB;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,YAAmC,EACnC,IAAI,EACJ,sBAA6B,EAC7B,wBAAgC,GACjC,EAAE,0BAA0B,GAAG,IAAI,CAQnC;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,YAAmC,EACnC,IAAI,EACJ,sBAA6B,GAC9B,GAAE,wBAA6B,GAAG,MAAM,CAExC"}
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Markdown Transformers for Lexical Editor
|
|
3
3
|
* Provides import/export functionality with proper newline preservation
|
|
4
4
|
*/
|
|
5
|
+
import { $createImageNode, $isImageNode, ImageNode } from './image-node';
|
|
6
|
+
import { $createMediaNode, $isMediaNode, MediaNode, } from './media-node';
|
|
5
7
|
import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode, } from '@lexical/extension';
|
|
6
8
|
import { CHECK_LIST, ELEMENT_TRANSFORMERS, MULTILINE_ELEMENT_TRANSFORMERS, TEXT_FORMAT_TRANSFORMERS, TEXT_MATCH_TRANSFORMERS, $convertFromMarkdownString as lexicalConvertFromMarkdown, $convertToMarkdownString as lexicalConvertToMarkdown, } from '@lexical/markdown';
|
|
9
|
+
import { $createTextNode } from 'lexical';
|
|
7
10
|
/**
|
|
8
11
|
* Horizontal rule transformer - supports ---, ***, and ___
|
|
9
12
|
*/
|
|
@@ -25,6 +28,80 @@ export const HR = {
|
|
|
25
28
|
},
|
|
26
29
|
type: 'element',
|
|
27
30
|
};
|
|
31
|
+
function markdownImageText(altText, src, width) {
|
|
32
|
+
const escapedAlt = altText.replaceAll('[', '\\[').replaceAll(']', '\\]');
|
|
33
|
+
const escapedSrc = src.includes(' ') ? encodeURI(src) : src;
|
|
34
|
+
return width
|
|
35
|
+
? `{width=${width}}`
|
|
36
|
+
: ``;
|
|
37
|
+
}
|
|
38
|
+
export const IMAGE = {
|
|
39
|
+
dependencies: [ImageNode],
|
|
40
|
+
export: (node) => {
|
|
41
|
+
if (!$isImageNode(node))
|
|
42
|
+
return null;
|
|
43
|
+
return markdownImageText(node.getAltText(), node.getSrc(), node.getWidth());
|
|
44
|
+
},
|
|
45
|
+
importRegExp: /!\[([^\]]*)\]\(([^)\s]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)(?:\{width=(\d+)\})?/,
|
|
46
|
+
regExp: /!\[([^\]]*)\]\(([^)\s]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)(?:\{width=(\d+)\})?$/,
|
|
47
|
+
replace: (textNode, match) => {
|
|
48
|
+
const [, altText = '', src = '', , widthMatch] = match;
|
|
49
|
+
const width = widthMatch ? Number.parseInt(widthMatch, 10) : undefined;
|
|
50
|
+
textNode.replace($createImageNode({
|
|
51
|
+
altText,
|
|
52
|
+
src,
|
|
53
|
+
width: Number.isFinite(width) ? width : undefined,
|
|
54
|
+
}));
|
|
55
|
+
},
|
|
56
|
+
trigger: ')',
|
|
57
|
+
type: 'text-match',
|
|
58
|
+
};
|
|
59
|
+
export const OBSIDIAN_IMAGE_EMBED = {
|
|
60
|
+
dependencies: [ImageNode],
|
|
61
|
+
importRegExp: /!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/,
|
|
62
|
+
regExp: /!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/,
|
|
63
|
+
replace: (textNode, match) => {
|
|
64
|
+
const [, src = '', labelOrWidth = ''] = match;
|
|
65
|
+
const width = /^\d+$/.test(labelOrWidth)
|
|
66
|
+
? Number.parseInt(labelOrWidth, 10)
|
|
67
|
+
: undefined;
|
|
68
|
+
const altText = width
|
|
69
|
+
? src.split('/').pop()
|
|
70
|
+
: labelOrWidth || src.split('/').pop();
|
|
71
|
+
if (!/\.(avif|gif|jpe?g|png|svg|webp)$/i.test(src)) {
|
|
72
|
+
return $createTextNode(match[0]);
|
|
73
|
+
}
|
|
74
|
+
textNode.replace($createImageNode({
|
|
75
|
+
altText: altText ?? '',
|
|
76
|
+
src,
|
|
77
|
+
width,
|
|
78
|
+
}));
|
|
79
|
+
},
|
|
80
|
+
trigger: ']',
|
|
81
|
+
type: 'text-match',
|
|
82
|
+
};
|
|
83
|
+
function mediaMarkdown(kind, src) {
|
|
84
|
+
return `<${kind} controls src="${src.replaceAll('"', '"')}"></${kind}>`;
|
|
85
|
+
}
|
|
86
|
+
export const MEDIA = {
|
|
87
|
+
dependencies: [MediaNode],
|
|
88
|
+
export: (node) => {
|
|
89
|
+
if (!$isMediaNode(node))
|
|
90
|
+
return null;
|
|
91
|
+
return mediaMarkdown(node.getKind(), node.getSrc());
|
|
92
|
+
},
|
|
93
|
+
importRegExp: /<(video|audio)\b[^>]*\bsrc=["']([^"']+)["'][^>]*>(?:<\/\1>)?/,
|
|
94
|
+
regExp: /<(video|audio)\b[^>]*\bsrc=["']([^"']+)["'][^>]*>(?:<\/\1>)?$/,
|
|
95
|
+
replace: (textNode, match) => {
|
|
96
|
+
const [, kind, src] = match;
|
|
97
|
+
if ((kind !== 'video' && kind !== 'audio') || !src) {
|
|
98
|
+
return $createTextNode(match[0]);
|
|
99
|
+
}
|
|
100
|
+
textNode.replace($createMediaNode({ kind, src }));
|
|
101
|
+
},
|
|
102
|
+
trigger: '>',
|
|
103
|
+
type: 'text-match',
|
|
104
|
+
};
|
|
28
105
|
/**
|
|
29
106
|
* All markdown transformers
|
|
30
107
|
*/
|
|
@@ -34,6 +111,9 @@ export const markdownTransformers = [
|
|
|
34
111
|
...ELEMENT_TRANSFORMERS,
|
|
35
112
|
...MULTILINE_ELEMENT_TRANSFORMERS,
|
|
36
113
|
...TEXT_FORMAT_TRANSFORMERS,
|
|
114
|
+
IMAGE,
|
|
115
|
+
OBSIDIAN_IMAGE_EMBED,
|
|
116
|
+
MEDIA,
|
|
37
117
|
...TEXT_MATCH_TRANSFORMERS,
|
|
38
118
|
];
|
|
39
119
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type MediaContextValue = {
|
|
2
|
+
resolveMediaUrl?: (src: string) => string;
|
|
3
|
+
};
|
|
4
|
+
export declare const MediaProvider: import("react").Provider<MediaContextValue>;
|
|
5
|
+
export declare function useMediaResolver(): ((src: string) => string) | undefined;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=media-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-context.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/media-context.tsx"],"names":[],"mappings":"AAIA,KAAK,iBAAiB,GAAG;IACvB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C,CAAA;AAID,eAAO,MAAM,aAAa,6CAAwB,CAAA;AAElD,wBAAgB,gBAAgB,WAPN,MAAM,KAAK,MAAM,cAS1C"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical';
|
|
3
|
+
import { DecoratorNode } from 'lexical';
|
|
4
|
+
export type MediaKind = 'audio' | 'video';
|
|
5
|
+
export type MediaPayload = {
|
|
6
|
+
kind: MediaKind;
|
|
7
|
+
key?: NodeKey;
|
|
8
|
+
src: string;
|
|
9
|
+
};
|
|
10
|
+
export type SerializedMediaNode = SerializedLexicalNode & {
|
|
11
|
+
kind: MediaKind;
|
|
12
|
+
src: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class MediaNode extends DecoratorNode<JSX.Element> {
|
|
15
|
+
__kind: MediaKind;
|
|
16
|
+
__src: string;
|
|
17
|
+
static getType(): string;
|
|
18
|
+
static clone(node: MediaNode): MediaNode;
|
|
19
|
+
static importJSON(serializedNode: SerializedMediaNode): MediaNode;
|
|
20
|
+
static importDOM(): DOMConversionMap | null;
|
|
21
|
+
constructor(kind: MediaKind, src: string, key?: NodeKey);
|
|
22
|
+
exportJSON(): SerializedMediaNode;
|
|
23
|
+
exportDOM(): DOMExportOutput;
|
|
24
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
25
|
+
updateDOM(): false;
|
|
26
|
+
getKind(): MediaKind;
|
|
27
|
+
getSrc(): string;
|
|
28
|
+
decorate(): JSX.Element;
|
|
29
|
+
}
|
|
30
|
+
export declare function $createMediaNode({ kind, key, src }: MediaPayload): MediaNode;
|
|
31
|
+
export declare function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode;
|
|
32
|
+
//# sourceMappingURL=media-node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-node.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/media-node.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAGhC,OAAO,KAAK,EACV,gBAAgB,EAEhB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,OAAO,EACP,qBAAqB,EACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAyB,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9D,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAEzC,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG;IACxD,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AA4BD,qBAAa,SAAU,SAAQ,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC;IACvD,MAAM,EAAE,SAAS,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IAEb,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS;IAIxC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,mBAAmB,GAAG,SAAS;IAOjE,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;gBAa/B,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO;IAMvD,UAAU,IAAI,mBAAmB;IAQjC,SAAS,IAAI,eAAe;IAO5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAO5C,SAAS,IAAI,KAAK;IAIlB,OAAO,IAAI,SAAS;IAIpB,MAAM,IAAI,MAAM;IAIhB,QAAQ,IAAI,GAAG,CAAC,OAAO;CAGxB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,YAAY,GAAG,SAAS,CAE5E;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GACnC,IAAI,IAAI,SAAS,CAEnB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { RichMediaPlayer } from '@moldable-ai/ui';
|
|
4
|
+
import { useMediaResolver } from './media-context';
|
|
5
|
+
import { $applyNodeReplacement, DecoratorNode } from 'lexical';
|
|
6
|
+
function mediaKindFromElement(element) {
|
|
7
|
+
if (element.tagName.toLowerCase() === 'audio')
|
|
8
|
+
return 'audio';
|
|
9
|
+
if (element.tagName.toLowerCase() === 'video')
|
|
10
|
+
return 'video';
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function $convertMediaElement(domNode) {
|
|
14
|
+
if (!(domNode instanceof HTMLElement))
|
|
15
|
+
return null;
|
|
16
|
+
const kind = mediaKindFromElement(domNode);
|
|
17
|
+
const src = domNode.getAttribute('src');
|
|
18
|
+
if (!kind || !src)
|
|
19
|
+
return null;
|
|
20
|
+
return { node: $createMediaNode({ kind, src }) };
|
|
21
|
+
}
|
|
22
|
+
function MediaComponent({ kind, src }) {
|
|
23
|
+
const resolveMediaUrl = useMediaResolver();
|
|
24
|
+
const resolvedSrc = resolveMediaUrl?.(src) ?? src;
|
|
25
|
+
return (_jsx("span", { className: "lexical-media-shell", children: _jsx(RichMediaPlayer, { kind: kind, src: resolvedSrc }) }));
|
|
26
|
+
}
|
|
27
|
+
export class MediaNode extends DecoratorNode {
|
|
28
|
+
__kind;
|
|
29
|
+
__src;
|
|
30
|
+
static getType() {
|
|
31
|
+
return 'media';
|
|
32
|
+
}
|
|
33
|
+
static clone(node) {
|
|
34
|
+
return new MediaNode(node.__kind, node.__src, node.__key);
|
|
35
|
+
}
|
|
36
|
+
static importJSON(serializedNode) {
|
|
37
|
+
return $createMediaNode({
|
|
38
|
+
kind: serializedNode.kind,
|
|
39
|
+
src: serializedNode.src,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
static importDOM() {
|
|
43
|
+
return {
|
|
44
|
+
audio: () => ({
|
|
45
|
+
conversion: $convertMediaElement,
|
|
46
|
+
priority: 0,
|
|
47
|
+
}),
|
|
48
|
+
video: () => ({
|
|
49
|
+
conversion: $convertMediaElement,
|
|
50
|
+
priority: 0,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
constructor(kind, src, key) {
|
|
55
|
+
super(key);
|
|
56
|
+
this.__kind = kind;
|
|
57
|
+
this.__src = src;
|
|
58
|
+
}
|
|
59
|
+
exportJSON() {
|
|
60
|
+
return {
|
|
61
|
+
...super.exportJSON(),
|
|
62
|
+
kind: this.__kind,
|
|
63
|
+
src: this.__src,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
exportDOM() {
|
|
67
|
+
const element = document.createElement(this.__kind);
|
|
68
|
+
element.setAttribute('controls', '');
|
|
69
|
+
element.setAttribute('src', this.__src);
|
|
70
|
+
return { element };
|
|
71
|
+
}
|
|
72
|
+
createDOM(config) {
|
|
73
|
+
const element = document.createElement('span');
|
|
74
|
+
const className = config.theme.media;
|
|
75
|
+
if (typeof className === 'string')
|
|
76
|
+
element.className = className;
|
|
77
|
+
return element;
|
|
78
|
+
}
|
|
79
|
+
updateDOM() {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
getKind() {
|
|
83
|
+
return this.getLatest().__kind;
|
|
84
|
+
}
|
|
85
|
+
getSrc() {
|
|
86
|
+
return this.getLatest().__src;
|
|
87
|
+
}
|
|
88
|
+
decorate() {
|
|
89
|
+
return _jsx(MediaComponent, { kind: this.__kind, src: this.__src });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function $createMediaNode({ kind, key, src }) {
|
|
93
|
+
return $applyNodeReplacement(new MediaNode(kind, src, key));
|
|
94
|
+
}
|
|
95
|
+
export function $isMediaNode(node) {
|
|
96
|
+
return node instanceof MediaNode;
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type MarkdownMediaKind = 'image' | 'video' | 'audio' | 'file';
|
|
2
|
+
export type MarkdownMediaUploadResult = {
|
|
3
|
+
altText?: string;
|
|
4
|
+
kind?: MarkdownMediaKind;
|
|
5
|
+
markdown?: string;
|
|
6
|
+
src: string;
|
|
7
|
+
};
|
|
8
|
+
export type MarkdownMediaUpload = (file: File) => Promise<MarkdownMediaUploadResult>;
|
|
9
|
+
export declare function MediaPasteDropPlugin({ onMediaUpload, }: {
|
|
10
|
+
onMediaUpload?: MarkdownMediaUpload;
|
|
11
|
+
}): null;
|
|
12
|
+
//# sourceMappingURL=media-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/media-plugin.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AAEpE,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,CAChC,IAAI,EAAE,IAAI,KACP,OAAO,CAAC,yBAAyB,CAAC,CAAA;AA4CvC,wBAAgB,oBAAoB,CAAC,EACnC,aAAa,GACd,EAAE;IACD,aAAa,CAAC,EAAE,mBAAmB,CAAA;CACpC,QA0GA"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { $createImageNode } from './image-node';
|
|
5
|
+
import { $createMediaNode } from './media-node';
|
|
6
|
+
import { $createParagraphNode, $createTextNode, $insertNodes, COMMAND_PRIORITY_HIGH, DROP_COMMAND, PASTE_COMMAND, } from 'lexical';
|
|
7
|
+
function dataUrlForFile(file) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const reader = new FileReader();
|
|
10
|
+
reader.onload = () => {
|
|
11
|
+
if (typeof reader.result === 'string')
|
|
12
|
+
resolve(reader.result);
|
|
13
|
+
else
|
|
14
|
+
reject(new Error('Failed to read file'));
|
|
15
|
+
};
|
|
16
|
+
reader.onerror = () => reject(reader.error ?? new Error('Failed to read file'));
|
|
17
|
+
reader.readAsDataURL(file);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function kindForFile(file) {
|
|
21
|
+
if (file.type.startsWith('image/'))
|
|
22
|
+
return 'image';
|
|
23
|
+
if (file.type.startsWith('video/'))
|
|
24
|
+
return 'video';
|
|
25
|
+
if (file.type.startsWith('audio/'))
|
|
26
|
+
return 'audio';
|
|
27
|
+
return 'file';
|
|
28
|
+
}
|
|
29
|
+
function escapeAttribute(value) {
|
|
30
|
+
return value
|
|
31
|
+
.replaceAll('&', '&')
|
|
32
|
+
.replaceAll('"', '"')
|
|
33
|
+
.replaceAll('<', '<')
|
|
34
|
+
.replaceAll('>', '>');
|
|
35
|
+
}
|
|
36
|
+
function markdownForMedia(result, file) {
|
|
37
|
+
const kind = result.kind ?? kindForFile(file);
|
|
38
|
+
const altText = result.altText ?? file.name;
|
|
39
|
+
if (result.markdown)
|
|
40
|
+
return result.markdown;
|
|
41
|
+
if (kind === 'video') {
|
|
42
|
+
return `<video controls src="${escapeAttribute(result.src)}"></video>`;
|
|
43
|
+
}
|
|
44
|
+
if (kind === 'audio') {
|
|
45
|
+
return `<audio controls src="${escapeAttribute(result.src)}"></audio>`;
|
|
46
|
+
}
|
|
47
|
+
return `[${altText}](${result.src})`;
|
|
48
|
+
}
|
|
49
|
+
export function MediaPasteDropPlugin({ onMediaUpload, }) {
|
|
50
|
+
const [editor] = useLexicalComposerContext();
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
async function uploadFile(file) {
|
|
53
|
+
if (onMediaUpload)
|
|
54
|
+
return onMediaUpload(file);
|
|
55
|
+
return {
|
|
56
|
+
altText: file.name,
|
|
57
|
+
kind: kindForFile(file),
|
|
58
|
+
src: await dataUrlForFile(file),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function insertFiles(files) {
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
const result = await uploadFile(file);
|
|
64
|
+
const kind = result.kind ?? kindForFile(file);
|
|
65
|
+
editor.update(() => {
|
|
66
|
+
if (kind === 'image') {
|
|
67
|
+
$insertNodes([
|
|
68
|
+
$createImageNode({
|
|
69
|
+
altText: result.altText ?? file.name,
|
|
70
|
+
src: result.src,
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (kind === 'video' || kind === 'audio') {
|
|
76
|
+
$insertNodes([$createMediaNode({ kind, src: result.src })]);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const paragraph = $createParagraphNode();
|
|
80
|
+
paragraph.append($createTextNode(markdownForMedia(result, file)));
|
|
81
|
+
$insertNodes([paragraph]);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function filesFromDataTransfer(dataTransfer) {
|
|
86
|
+
return Array.from(dataTransfer?.files ?? []).filter((file) => file.size > 0);
|
|
87
|
+
}
|
|
88
|
+
return editor.registerCommand(PASTE_COMMAND, (event) => {
|
|
89
|
+
if (!('clipboardData' in event))
|
|
90
|
+
return false;
|
|
91
|
+
const files = filesFromDataTransfer(event.clipboardData);
|
|
92
|
+
if (files.length === 0)
|
|
93
|
+
return false;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
void insertFiles(files);
|
|
96
|
+
return true;
|
|
97
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
98
|
+
}, [editor, onMediaUpload]);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
return editor.registerCommand(DROP_COMMAND, (event) => {
|
|
101
|
+
const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.size > 0);
|
|
102
|
+
if (files.length === 0)
|
|
103
|
+
return false;
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
void Promise.resolve().then(async () => {
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const result = onMediaUpload
|
|
108
|
+
? await onMediaUpload(file)
|
|
109
|
+
: {
|
|
110
|
+
altText: file.name,
|
|
111
|
+
kind: kindForFile(file),
|
|
112
|
+
src: await dataUrlForFile(file),
|
|
113
|
+
};
|
|
114
|
+
const kind = result.kind ?? kindForFile(file);
|
|
115
|
+
editor.update(() => {
|
|
116
|
+
if (kind === 'image') {
|
|
117
|
+
$insertNodes([
|
|
118
|
+
$createImageNode({
|
|
119
|
+
altText: result.altText ?? file.name,
|
|
120
|
+
src: result.src,
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
else if (kind === 'video' || kind === 'audio') {
|
|
125
|
+
$insertNodes([$createMediaNode({ kind, src: result.src })]);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const paragraph = $createParagraphNode();
|
|
129
|
+
paragraph.append($createTextNode(markdownForMedia(result, file)));
|
|
130
|
+
$insertNodes([paragraph]);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return true;
|
|
136
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
137
|
+
}, [editor, onMediaUpload]);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
@@ -2,6 +2,7 @@ interface SyncPluginProps {
|
|
|
2
2
|
value: string;
|
|
3
3
|
initialValueRef: React.MutableRefObject<string>;
|
|
4
4
|
}
|
|
5
|
+
export declare const EXTERNAL_MARKDOWN_SYNC_TAG = "moldable-external-markdown-sync";
|
|
5
6
|
/**
|
|
6
7
|
* Plugin that syncs external value changes with the editor.
|
|
7
8
|
* Handles cases where the value prop changes from outside the editor.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/sync-plugin.tsx"],"names":[],"mappings":"AAWA,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAA;CAChD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,eAAe,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"sync-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/sync-plugin.tsx"],"names":[],"mappings":"AAWA,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAA;CAChD;AAED,eAAO,MAAM,0BAA0B,oCAAoC,CAAA;AAE3E;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,eAAe,GAAG,IAAI,CAiD5E"}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
-
import {
|
|
3
|
+
import { useLayoutEffect } from 'react';
|
|
4
4
|
import { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, } from './markdown-transformers';
|
|
5
5
|
import { $createParagraphNode, $getRoot } from 'lexical';
|
|
6
|
+
export const EXTERNAL_MARKDOWN_SYNC_TAG = 'moldable-external-markdown-sync';
|
|
6
7
|
/**
|
|
7
8
|
* Plugin that syncs external value changes with the editor.
|
|
8
9
|
* Handles cases where the value prop changes from outside the editor.
|
|
9
10
|
*/
|
|
10
11
|
export function SyncPlugin({ value, initialValueRef }) {
|
|
11
12
|
const [editor] = useLexicalComposerContext();
|
|
12
|
-
|
|
13
|
+
useLayoutEffect(() => {
|
|
13
14
|
// Skip if this is the initial render or if value matches what we have
|
|
14
15
|
if (value === initialValueRef.current) {
|
|
15
16
|
return;
|
|
@@ -19,10 +20,12 @@ export function SyncPlugin({ value, initialValueRef }) {
|
|
|
19
20
|
editor.read(() => {
|
|
20
21
|
currentMarkdown = $convertToMarkdownString({
|
|
21
22
|
transformers: markdownTransformers,
|
|
23
|
+
shouldPreserveNewLines: false,
|
|
22
24
|
});
|
|
23
25
|
});
|
|
24
26
|
// Only update if the value is actually different
|
|
25
27
|
if (currentMarkdown !== value) {
|
|
28
|
+
initialValueRef.current = value;
|
|
26
29
|
editor.update(() => {
|
|
27
30
|
const root = $getRoot();
|
|
28
31
|
root.clear();
|
|
@@ -31,15 +34,20 @@ export function SyncPlugin({ value, initialValueRef }) {
|
|
|
31
34
|
markdown: value,
|
|
32
35
|
transformers: markdownTransformers,
|
|
33
36
|
node: root,
|
|
34
|
-
shouldPreserveNewLines:
|
|
37
|
+
shouldPreserveNewLines: false,
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
else {
|
|
38
41
|
root.append($createParagraphNode());
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
if (root.getChildrenSize() === 0) {
|
|
44
|
+
root.append($createParagraphNode());
|
|
45
|
+
}
|
|
46
|
+
root.selectEnd();
|
|
47
|
+
}, { discrete: true, tag: EXTERNAL_MARKDOWN_SYNC_TAG });
|
|
48
|
+
return;
|
|
42
49
|
}
|
|
50
|
+
initialValueRef.current = value;
|
|
43
51
|
}, [editor, value, initialValueRef]);
|
|
44
52
|
return null;
|
|
45
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moldable-ai/editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Rich text markdown editor for Moldable applications",
|
|
5
5
|
"author": "Desiderata LLC",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"vitest": "^4.0.16"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@moldable-ai/ui": ">=0.2.
|
|
78
|
+
"@moldable-ai/ui": ">=0.2.12",
|
|
79
79
|
"react": "^19.0.0",
|
|
80
80
|
"react-dom": "^19.0.0"
|
|
81
81
|
}
|
package/src/styles/index.css
CHANGED
|
@@ -101,3 +101,120 @@
|
|
|
101
101
|
padding: 0 4px;
|
|
102
102
|
border-radius: 4px;
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/* Rich media nodes */
|
|
106
|
+
.lexical-image-shell {
|
|
107
|
+
display: inline-block;
|
|
108
|
+
max-width: 100%;
|
|
109
|
+
position: relative;
|
|
110
|
+
vertical-align: top;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.lexical-image-shell-selected {
|
|
114
|
+
outline: 3px solid var(--primary);
|
|
115
|
+
outline-offset: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.lexical-image {
|
|
119
|
+
border-radius: 0;
|
|
120
|
+
display: block;
|
|
121
|
+
height: auto;
|
|
122
|
+
max-width: 100%;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.lexical-image-resize-handle {
|
|
126
|
+
background: var(--primary);
|
|
127
|
+
border: 2px solid var(--background);
|
|
128
|
+
border-radius: 9999px;
|
|
129
|
+
bottom: -8px;
|
|
130
|
+
cursor: nwse-resize;
|
|
131
|
+
height: 16px;
|
|
132
|
+
position: absolute;
|
|
133
|
+
right: -8px;
|
|
134
|
+
width: 16px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.lexical-media-shell {
|
|
138
|
+
display: block;
|
|
139
|
+
max-width: 100%;
|
|
140
|
+
width: 100%;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.lexical-media {
|
|
144
|
+
background: rgb(10 10 10);
|
|
145
|
+
border: 1px solid rgb(255 255 255 / 0.1);
|
|
146
|
+
border-radius: 0.5rem;
|
|
147
|
+
color-scheme: dark;
|
|
148
|
+
display: block;
|
|
149
|
+
margin: 0.75rem 0;
|
|
150
|
+
max-width: 100%;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.lexical-media-video {
|
|
154
|
+
max-height: min(520px, 62vh);
|
|
155
|
+
width: 100%;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.lexical-media-audio {
|
|
159
|
+
padding: 0.75rem;
|
|
160
|
+
width: min(100%, 40rem);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* File reference typeahead */
|
|
164
|
+
.moldable-editor-reference-menu {
|
|
165
|
+
background: hsl(var(--popover));
|
|
166
|
+
border: 1px solid hsl(var(--border));
|
|
167
|
+
border-radius: 0.5rem;
|
|
168
|
+
box-shadow:
|
|
169
|
+
0 16px 40px hsl(var(--foreground) / 0.14),
|
|
170
|
+
0 2px 8px hsl(var(--foreground) / 0.08);
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: column;
|
|
173
|
+
gap: 0.125rem;
|
|
174
|
+
min-width: min(22rem, calc(100vw - 2rem));
|
|
175
|
+
padding: 0.25rem;
|
|
176
|
+
z-index: 50;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.moldable-editor-reference-option {
|
|
180
|
+
align-items: center;
|
|
181
|
+
background: transparent;
|
|
182
|
+
border: 0;
|
|
183
|
+
border-radius: 0.375rem;
|
|
184
|
+
color: hsl(var(--popover-foreground));
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
display: grid;
|
|
187
|
+
gap: 0.125rem;
|
|
188
|
+
justify-items: stretch;
|
|
189
|
+
min-height: 2.75rem;
|
|
190
|
+
padding: 0.45rem 0.625rem;
|
|
191
|
+
text-align: left;
|
|
192
|
+
width: 100%;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.moldable-editor-reference-option:hover,
|
|
196
|
+
.moldable-editor-reference-option-selected {
|
|
197
|
+
background: hsl(var(--muted));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.moldable-editor-reference-title,
|
|
201
|
+
.moldable-editor-reference-path {
|
|
202
|
+
display: block;
|
|
203
|
+
min-width: 0;
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
text-overflow: ellipsis;
|
|
206
|
+
white-space: nowrap;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.moldable-editor-reference-title {
|
|
210
|
+
color: hsl(var(--foreground));
|
|
211
|
+
font-size: 0.8125rem;
|
|
212
|
+
font-weight: 500;
|
|
213
|
+
line-height: 1.15;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.moldable-editor-reference-path {
|
|
217
|
+
color: hsl(var(--muted-foreground));
|
|
218
|
+
font-size: 0.71875rem;
|
|
219
|
+
line-height: 1.15;
|
|
220
|
+
}
|