@meowdown/react 0.5.0 → 0.6.0
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/README.md +18 -7
- package/dist/index.d.ts +9 -6
- package/dist/index.js +55 -27
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ React components for Meowdown, a hybrid (live-preview) Markdown editor.
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
|
-
import {
|
|
8
|
+
import { MeowdownEditor, type EditorHandle } from '@meowdown/react'
|
|
9
9
|
import '@meowdown/react/style.css'
|
|
10
10
|
import { useRef, useCallback } from 'react'
|
|
11
11
|
|
|
@@ -15,13 +15,20 @@ export function App() {
|
|
|
15
15
|
console.log(ref.current?.getMarkdown())
|
|
16
16
|
}, [])
|
|
17
17
|
|
|
18
|
-
return
|
|
18
|
+
return (
|
|
19
|
+
<MeowdownEditor
|
|
20
|
+
handleRef={ref}
|
|
21
|
+
mode="focus"
|
|
22
|
+
initialMarkdown="# Hello"
|
|
23
|
+
onDocChange={handleDocChange}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
19
26
|
}
|
|
20
27
|
```
|
|
21
28
|
|
|
22
29
|
## API
|
|
23
30
|
|
|
24
|
-
### `<
|
|
31
|
+
### `<MeowdownEditor>`
|
|
25
32
|
|
|
26
33
|
The Markdown editor component. Renders inside a `div.meowdown` wrapper that fills a flex parent. In rich modes, typing `/` opens a slash menu for inserting blocks (headings, blockquote, lists, code block, table). Hovering a block shows a handle to its left: the plus button inserts an empty paragraph below the block, and the grip selects the block and can be dragged to move it, with a drop indicator line marking the target.
|
|
27
34
|
|
|
@@ -31,7 +38,7 @@ The Markdown editor component. Renders inside a `div.meowdown` wrapper that fill
|
|
|
31
38
|
- `'hide'`: Markdown syntax is always hidden.
|
|
32
39
|
- `'source'`: raw Markdown source with syntax highlighting.
|
|
33
40
|
- `initialMarkdown?: string`: first render only.
|
|
34
|
-
- `onDocChange?: VoidFunction`: called on every document change.
|
|
41
|
+
- `onDocChange?: VoidFunction`: called on every user-driven document change. Programmatic `setMarkdown` / `setState` on the handle do not fire it.
|
|
35
42
|
- `onTagSearch?: (query: string) => TagItem[] | Promise<TagItem[]>`: enables the tag menu, which opens when typing `#` followed by text in a rich mode. Returns ranked rows `{ tag, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `#tag ` then runs its `onSelect`. Omit to disable.
|
|
36
43
|
- `onWikilinkSearch?: (query: string) => WikilinkItem[] | Promise<WikilinkItem[]>`: enables the wikilink menu, which opens as soon as `[[` is typed in a rich mode. Returns ranked rows `{ target, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `[[target]]` then runs its `onSelect`. Omit to disable.
|
|
37
44
|
- `onWikilinkClick?: (payload: { target: string; event: MouseEvent }) => void`: called when a rendered wiki link is clicked. A plain click inside a link the caret already sits in just places the caret; `Mod`-click always fires. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
|
|
@@ -43,7 +50,7 @@ The Markdown editor component. Renders inside a `div.meowdown` wrapper that fill
|
|
|
43
50
|
- `spellCheck?: boolean`: toggles the browser's native spell checking in the rich modes. Defaults to the browser's behavior. Ignored in source mode.
|
|
44
51
|
- `editorClassName?: string`: class on the editable root (the contenteditable). Rich modes only.
|
|
45
52
|
- `wrapperClassName?: string`: class on the outer `div.meowdown` wrapper.
|
|
46
|
-
- `
|
|
53
|
+
- `handleRef?: Ref<EditorHandle>`
|
|
47
54
|
- `children?: ReactNode`: rendered inside the editor's ProseKit context, so children can call `useEditor()`. Only rendered in the rich modes; source mode ignores them.
|
|
48
55
|
|
|
49
56
|
### `useEditor`
|
|
@@ -54,12 +61,16 @@ Re-exported from `@prosekit/react`. Call it from a component passed as `children
|
|
|
54
61
|
|
|
55
62
|
Re-exported from `@meowdown/core`. `checkRoundTrip(markdown)` returns `'exact' | 'normalizing' | 'lossy'`, for hosts that gate saving markdown files on whether the editor reproduces them faithfully.
|
|
56
63
|
|
|
64
|
+
### `EDITOR_KEY_BINDINGS`
|
|
65
|
+
|
|
66
|
+
Re-exported from `@meowdown/core`. A literal (`as const`) object mapping each editor shortcut (e.g. `Mod-b`, `Mod-1`) to its description, for host settings UIs and keybinding-collision checks.
|
|
67
|
+
|
|
57
68
|
### `EditorHandle`
|
|
58
69
|
|
|
59
|
-
Imperative handle for the editor, attached via `
|
|
70
|
+
Imperative handle for the editor, attached via `handleRef`.
|
|
60
71
|
|
|
61
72
|
- `getMarkdown(): string`: serializes the current document to Markdown. Can be expensive on large documents; call it on demand (e.g. throttled) instead of on every change.
|
|
62
|
-
- `setMarkdown(markdown: string): void`: replaces the whole document as a single undoable edit.
|
|
73
|
+
- `setMarkdown(markdown: string): void`: replaces the whole document as a single undoable edit. Does not fire `onDocChange`.
|
|
63
74
|
- `getState(): EditorStateSnapshot`: returns `[markdown, selection]`, where `selection` is a `SelectionJSON` (`{ anchor: number, head: number, type: string }`).
|
|
64
75
|
- `setState(markdown?: string, selection?: SelectionJSON | 'start' | 'end'): void`: replaces the document (if `markdown` is given) and restores `selection`: exactly when valid, otherwise clamped to the nearest text selection; out-of-range positions never throw. `'start'` and `'end'` jump to the document edges. Without a selection, the current one is mapped through the change. Restore a snapshot with `handle.setState(...handle.getState())`.
|
|
65
76
|
- `getSelection(): SelectionJSON`: returns the current selection.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactNode, Ref } from "react";
|
|
2
|
-
import { ImageOptions, MarkMode, MarkMode as MarkMode$1, PlaceholderOptions, RoundTripFidelity, TypedEditor, TypedEditor as TypedEditor$1, WikilinkClickHandler, checkRoundTrip } from "@meowdown/core";
|
|
2
|
+
import { EDITOR_KEY_BINDINGS, ImageOptions, MarkMode, MarkMode as MarkMode$1, PlaceholderOptions, RoundTripFidelity, TypedEditor, TypedEditor as TypedEditor$1, WikilinkClickHandler, checkRoundTrip } from "@meowdown/core";
|
|
3
3
|
import { SelectionJSON, SelectionJSON as SelectionJSON$1 } from "@prosekit/core";
|
|
4
4
|
import { useEditor } from "@prosekit/react";
|
|
5
5
|
|
|
@@ -91,7 +91,10 @@ interface EditorProps {
|
|
|
91
91
|
* first render is used; later changes are ignored.
|
|
92
92
|
*/
|
|
93
93
|
initialMarkdown?: string;
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Called on every user-driven document change. Programmatic `setMarkdown` and
|
|
96
|
+
* `setState` on the handle do not fire it.
|
|
97
|
+
*/
|
|
95
98
|
onDocChange?: VoidFunction;
|
|
96
99
|
/**
|
|
97
100
|
* Searches tags for the tag menu, which opens when typing `#` followed by
|
|
@@ -144,11 +147,11 @@ interface EditorProps {
|
|
|
144
147
|
/** Class on the outer `.meowdown` wrapper div. */
|
|
145
148
|
wrapperClassName?: string;
|
|
146
149
|
/** Imperative handle for the editor. */
|
|
147
|
-
|
|
150
|
+
handleRef?: Ref<EditorHandle>;
|
|
148
151
|
/** Nodes rendered inside the editor's ProseKit context (rich modes only). */
|
|
149
152
|
children?: ReactNode;
|
|
150
153
|
}
|
|
151
|
-
declare function
|
|
154
|
+
declare function MeowdownEditor({
|
|
152
155
|
mode,
|
|
153
156
|
initialMarkdown,
|
|
154
157
|
onDocChange,
|
|
@@ -163,8 +166,8 @@ declare function Editor({
|
|
|
163
166
|
spellCheck,
|
|
164
167
|
editorClassName,
|
|
165
168
|
wrapperClassName,
|
|
166
|
-
|
|
169
|
+
handleRef,
|
|
167
170
|
children
|
|
168
171
|
}: EditorProps): import("react").JSX.Element;
|
|
169
172
|
//#endregion
|
|
170
|
-
export {
|
|
173
|
+
export { EDITOR_KEY_BINDINGS, type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, type MarkMode, MeowdownEditor, type RoundTripFidelity, type SelectionHint, type SelectionJSON, type TagItem, type TagSearchHandler, type TypedEditor, type WikilinkItem, type WikilinkSearchHandler, checkRoundTrip, useEditor };
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Compartment, EditorSelection, EditorState } from "@codemirror/state";
|
|
|
7
7
|
import { EditorView, keymap } from "@codemirror/view";
|
|
8
8
|
import { clamp } from "@ocavue/utils";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import { checkRoundTrip, codeBlockLanguages, defineEditorExtension, defineImages, defineMarkMode, definePlaceholder, defineReadonly, defineWikilinkClickHandler, docToMarkdown, markdownToDoc } from "@meowdown/core";
|
|
10
|
+
import { EDITOR_KEY_BINDINGS, checkRoundTrip, codeBlockLanguages, defineEditorExtension, defineImages, defineMarkMode, definePlaceholder, defineReadonly, defineWikilinkClickHandler, docToMarkdown, markdownToDoc } from "@meowdown/core";
|
|
11
11
|
import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, union } from "@prosekit/core";
|
|
12
12
|
import { Selection, TextSelection } from "@prosekit/pm/state";
|
|
13
13
|
import { ProseKit, defineReactNodeView, useEditor, useEditor as useEditor$1, useExtension } from "@prosekit/react";
|
|
@@ -26,6 +26,7 @@ function CodeMirrorEditor({ initialMarkdown, onDocChange, readOnly, ref }) {
|
|
|
26
26
|
const containerRef = useRef(null);
|
|
27
27
|
const viewRef = useRef(null);
|
|
28
28
|
const readOnlyCompartmentRef = useRef(new Compartment());
|
|
29
|
+
const suppressDocChangeRef = useRef(false);
|
|
29
30
|
const onDocChangeRef = useRef(onDocChange);
|
|
30
31
|
useLayoutEffect(() => {
|
|
31
32
|
onDocChangeRef.current = onDocChange;
|
|
@@ -55,15 +56,20 @@ function CodeMirrorEditor({ initialMarkdown, onDocChange, readOnly, ref }) {
|
|
|
55
56
|
}
|
|
56
57
|
if (markdown == null && !selection) return;
|
|
57
58
|
const docLength = markdown == null ? view.state.doc.length : markdown.length;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
suppressDocChangeRef.current = true;
|
|
60
|
+
try {
|
|
61
|
+
view.dispatch({
|
|
62
|
+
changes: markdown == null ? void 0 : {
|
|
63
|
+
from: 0,
|
|
64
|
+
to: view.state.doc.length,
|
|
65
|
+
insert: markdown
|
|
66
|
+
},
|
|
67
|
+
selection: selection ? resolveSelection$1(selection, docLength) : void 0,
|
|
68
|
+
scrollIntoView: true
|
|
69
|
+
});
|
|
70
|
+
} finally {
|
|
71
|
+
suppressDocChangeRef.current = false;
|
|
72
|
+
}
|
|
67
73
|
}
|
|
68
74
|
function setMarkdown(markdown) {
|
|
69
75
|
setState(markdown);
|
|
@@ -104,7 +110,7 @@ function CodeMirrorEditor({ initialMarkdown, onDocChange, readOnly, ref }) {
|
|
|
104
110
|
EditorView.lineWrapping,
|
|
105
111
|
readOnlyCompartmentRef.current.of(EditorState.readOnly.of(initialReadOnlyRef.current)),
|
|
106
112
|
EditorView.updateListener.of((update) => {
|
|
107
|
-
if (!update.docChanged) return;
|
|
113
|
+
if (!update.docChanged || suppressDocChangeRef.current) return;
|
|
108
114
|
onDocChangeRef.current?.();
|
|
109
115
|
})
|
|
110
116
|
]
|
|
@@ -429,22 +435,31 @@ function DropIndicator$1() {
|
|
|
429
435
|
//#endregion
|
|
430
436
|
//#region src/components/editor-extensions.tsx
|
|
431
437
|
function EditorExtensions({ markMode, onDocChange, onWikilinkClick, resolveImageUrl, onImagePaste, onImageSaveError, placeholder, readOnly }) {
|
|
432
|
-
useExtension(useMemo(() =>
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
useExtension(useMemo(() =>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
438
|
+
useExtension(useMemo(() => {
|
|
439
|
+
return defineMarkMode(markMode);
|
|
440
|
+
}, [markMode]));
|
|
441
|
+
useExtension(useMemo(() => {
|
|
442
|
+
return readOnly ? defineReadonly() : null;
|
|
443
|
+
}, [readOnly]));
|
|
444
|
+
useExtension(useMemo(() => {
|
|
445
|
+
return onDocChange ? defineDocChangeHandler(onDocChange) : null;
|
|
446
|
+
}, [onDocChange]));
|
|
447
|
+
useExtension(useMemo(() => {
|
|
448
|
+
return onWikilinkClick ? defineWikilinkClickHandler(onWikilinkClick) : null;
|
|
449
|
+
}, [onWikilinkClick]));
|
|
450
|
+
useExtension(useMemo(() => {
|
|
451
|
+
return resolveImageUrl ? defineImages({
|
|
452
|
+
resolveImageUrl,
|
|
453
|
+
onImagePaste,
|
|
454
|
+
onImageSaveError
|
|
455
|
+
}) : null;
|
|
456
|
+
}, [
|
|
441
457
|
resolveImageUrl,
|
|
442
458
|
onImagePaste,
|
|
443
459
|
onImageSaveError
|
|
444
460
|
]));
|
|
445
461
|
useExtension(useMemo(() => {
|
|
446
|
-
|
|
447
|
-
return definePlaceholder({ placeholder });
|
|
462
|
+
return placeholder ? definePlaceholder({ placeholder }) : null;
|
|
448
463
|
}, [placeholder]));
|
|
449
464
|
return null;
|
|
450
465
|
}
|
|
@@ -686,6 +701,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
|
|
|
686
701
|
if (initialMarkdown) editor.setContent(markdownToDoc(editor, initialMarkdown));
|
|
687
702
|
return editor;
|
|
688
703
|
});
|
|
704
|
+
const suppressDocChangeRef = useRef(false);
|
|
689
705
|
useImperativeHandle(ref, () => {
|
|
690
706
|
function getMarkdown() {
|
|
691
707
|
return docToMarkdown(editor.state.doc);
|
|
@@ -704,7 +720,12 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
|
|
|
704
720
|
transaction.replaceWith(0, transaction.doc.content.size, doc.content);
|
|
705
721
|
}
|
|
706
722
|
if (selection) transaction.setSelection(resolveSelection(transaction.doc, selection)).scrollIntoView();
|
|
707
|
-
|
|
723
|
+
suppressDocChangeRef.current = true;
|
|
724
|
+
try {
|
|
725
|
+
editor.view.dispatch(transaction);
|
|
726
|
+
} finally {
|
|
727
|
+
suppressDocChangeRef.current = false;
|
|
728
|
+
}
|
|
708
729
|
}
|
|
709
730
|
function setMarkdown(markdown) {
|
|
710
731
|
setState(markdown);
|
|
@@ -730,6 +751,13 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
|
|
|
730
751
|
editor
|
|
731
752
|
};
|
|
732
753
|
}, [editor]);
|
|
754
|
+
const handleDocChange = useMemo(() => {
|
|
755
|
+
if (!onDocChange) return void 0;
|
|
756
|
+
return () => {
|
|
757
|
+
if (suppressDocChangeRef.current) return;
|
|
758
|
+
onDocChange();
|
|
759
|
+
};
|
|
760
|
+
}, [onDocChange]);
|
|
733
761
|
return /* @__PURE__ */ jsxs(ProseKit, {
|
|
734
762
|
editor,
|
|
735
763
|
children: [
|
|
@@ -740,7 +768,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
|
|
|
740
768
|
}),
|
|
741
769
|
/* @__PURE__ */ jsx(EditorExtensions, {
|
|
742
770
|
markMode,
|
|
743
|
-
onDocChange,
|
|
771
|
+
onDocChange: handleDocChange,
|
|
744
772
|
onWikilinkClick,
|
|
745
773
|
resolveImageUrl,
|
|
746
774
|
onImagePaste,
|
|
@@ -760,9 +788,9 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
|
|
|
760
788
|
|
|
761
789
|
//#endregion
|
|
762
790
|
//#region src/components/editor.tsx
|
|
763
|
-
function
|
|
791
|
+
function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, resolveImageUrl, onImagePaste, onImageSaveError, placeholder, readOnly, spellCheck, editorClassName, wrapperClassName, handleRef, children }) {
|
|
764
792
|
const childRef = useRef(null);
|
|
765
|
-
useImperativeHandle(
|
|
793
|
+
useImperativeHandle(handleRef, () => {
|
|
766
794
|
function getMarkdown() {
|
|
767
795
|
return childRef.current?.getMarkdown() ?? "";
|
|
768
796
|
}
|
|
@@ -838,4 +866,4 @@ function Editor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onW
|
|
|
838
866
|
}
|
|
839
867
|
|
|
840
868
|
//#endregion
|
|
841
|
-
export {
|
|
869
|
+
export { EDITOR_KEY_BINDINGS, MeowdownEditor, checkRoundTrip, useEditor };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meowdown/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@prosekit/pm": "^0.1.18",
|
|
29
29
|
"@prosekit/react": "^0.8.0-beta.0",
|
|
30
30
|
"clsx": "^2.1.1",
|
|
31
|
-
"@meowdown/core": "0.
|
|
31
|
+
"@meowdown/core": "0.5.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^19.0.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@css-modules-kit/codegen": "^1.
|
|
46
|
+
"@css-modules-kit/codegen": "^1.3.0",
|
|
47
47
|
"@ocavue/tsconfig": "^0.7.1",
|
|
48
48
|
"@tsdown/css": "^0.22.2",
|
|
49
49
|
"@types/react": "^19.2.17",
|