@mp-lb/mdkit 0.0.1-main.2.1
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 +132 -0
- package/dist/collaboration/useMdKitCollaboration.d.ts +10 -0
- package/dist/collaboration/useMdKitCollaboration.js +90 -0
- package/dist/core/documentEngine.d.ts +38 -0
- package/dist/core/documentEngine.js +95 -0
- package/dist/core/documentEngine.test.d.ts +1 -0
- package/dist/core/documentEngine.test.js +119 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +1 -0
- package/dist/document/MdKitConflictPanel.d.ts +7 -0
- package/dist/document/MdKitConflictPanel.js +41 -0
- package/dist/document/MdKitDocumentToolbar.d.ts +13 -0
- package/dist/document/MdKitDocumentToolbar.js +48 -0
- package/dist/document/documentTypes.d.ts +57 -0
- package/dist/document/documentTypes.js +1 -0
- package/dist/document/useMdKitDocument.d.ts +33 -0
- package/dist/document/useMdKitDocument.js +396 -0
- package/dist/document/useMdKitDocument.test.d.ts +1 -0
- package/dist/document/useMdKitDocument.test.js +151 -0
- package/dist/fastify.d.ts +3 -0
- package/dist/fastify.js +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +11 -0
- package/dist/markdown/MarkdownBubbleMenu.d.ts +6 -0
- package/dist/markdown/MarkdownBubbleMenu.js +29 -0
- package/dist/markdown/MdKitEditor.d.ts +25 -0
- package/dist/markdown/MdKitEditor.js +7 -0
- package/dist/markdown/MdKitEditor.test.d.ts +1 -0
- package/dist/markdown/MdKitEditor.test.js +126 -0
- package/dist/markdown/TiptapMarkdownSurface.d.ts +23 -0
- package/dist/markdown/TiptapMarkdownSurface.js +430 -0
- package/dist/markdown/editorDebug.d.ts +5 -0
- package/dist/markdown/editorDebug.js +1 -0
- package/dist/markdown/markdownFenceRanges.d.ts +6 -0
- package/dist/markdown/markdownFenceRanges.js +41 -0
- package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
- package/dist/markdown/normalizeMarkdownSerialization.js +34 -0
- package/dist/markdown/normalizeMarkdownSerialization.test.d.ts +1 -0
- package/dist/markdown/normalizeMarkdownSerialization.test.js +16 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.js +12 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.test.d.ts +1 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.test.js +13 -0
- package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
- package/dist/markdown/preserveMarkdownWhitespace.js +86 -0
- package/dist/markdown/preserveMarkdownWhitespace.test.d.ts +1 -0
- package/dist/markdown/preserveMarkdownWhitespace.test.js +25 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.js +13 -0
- package/dist/theme/MdKitThemeEditor.d.ts +8 -0
- package/dist/theme/MdKitThemeEditor.js +13 -0
- package/dist/theme/editorTheme.d.ts +20 -0
- package/dist/theme/editorTheme.js +47 -0
- package/dist/transport/fastify.d.ts +7 -0
- package/dist/transport/fastify.js +19 -0
- package/dist/transport/http.d.ts +43 -0
- package/dist/transport/http.js +80 -0
- package/dist/transport/index.d.ts +5 -0
- package/dist/transport/index.js +2 -0
- package/dist/transport/rest.d.ts +6 -0
- package/dist/transport/rest.js +34 -0
- package/dist/transport/store.d.ts +21 -0
- package/dist/transport/store.js +1 -0
- package/dist/transport/trpcClient.d.ts +81 -0
- package/dist/transport/trpcClient.js +21 -0
- package/dist/transport/trpcServer.d.ts +72 -0
- package/dist/transport/trpcServer.js +45 -0
- package/dist/trpc/client.d.ts +3 -0
- package/dist/trpc/client.js +1 -0
- package/dist/trpc/server.d.ts +3 -0
- package/dist/trpc/server.js +1 -0
- package/dist/trpc.d.ts +3 -0
- package/dist/trpc.js +1 -0
- package/dist/ui/joinClassNames.d.ts +1 -0
- package/dist/ui/joinClassNames.js +1 -0
- package/dist/versioning/VersionHistoryPanel.d.ts +9 -0
- package/dist/versioning/VersionHistoryPanel.js +29 -0
- package/dist/versioning/useMdKitDocumentVersions.d.ts +16 -0
- package/dist/versioning/useMdKitDocumentVersions.js +88 -0
- package/dist/versioning/useMdKitDocumentVersions.test.d.ts +1 -0
- package/dist/versioning/useMdKitDocumentVersions.test.js +41 -0
- package/docs/.vitepress/config.ts +34 -0
- package/docs/.vitepress/dist/404.html +22 -0
- package/docs/.vitepress/dist/api.html +120 -0
- package/docs/.vitepress/dist/architecture.html +25 -0
- package/docs/.vitepress/dist/assets/api.md.asncK3PQ.js +96 -0
- package/docs/.vitepress/dist/assets/api.md.asncK3PQ.lean.js +1 -0
- package/docs/.vitepress/dist/assets/app.BQvrHyG0.js +1 -0
- package/docs/.vitepress/dist/assets/architecture.md.BHQLarmZ.js +1 -0
- package/docs/.vitepress/dist/assets/architecture.md.BHQLarmZ.lean.js +1 -0
- package/docs/.vitepress/dist/assets/chunks/framework.RRduUuAx.js +19 -0
- package/docs/.vitepress/dist/assets/chunks/theme.CkCo6Nk1.js +1 -0
- package/docs/.vitepress/dist/assets/index.md.CITl-897.js +137 -0
- package/docs/.vitepress/dist/assets/index.md.CITl-897.lean.js +1 -0
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/.vitepress/dist/assets/shadcn.md.C3idOo2N.js +57 -0
- package/docs/.vitepress/dist/assets/shadcn.md.C3idOo2N.lean.js +1 -0
- package/docs/.vitepress/dist/assets/style.BtrGaL3i.css +1 -0
- package/docs/.vitepress/dist/assets/styling.md.B2C6kVFa.js +91 -0
- package/docs/.vitepress/dist/assets/styling.md.B2C6kVFa.lean.js +1 -0
- package/docs/.vitepress/dist/hashmap.json +1 -0
- package/docs/.vitepress/dist/index.html +161 -0
- package/docs/.vitepress/dist/shadcn.html +81 -0
- package/docs/.vitepress/dist/styling.html +115 -0
- package/docs/.vitepress/dist/vp-icons.css +1 -0
- package/docs/api.md +343 -0
- package/docs/architecture.md +67 -0
- package/docs/index.md +244 -0
- package/docs/shadcn.md +118 -0
- package/docs/styling.md +247 -0
- package/package.json +105 -0
- package/src/styles.css +676 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { MdKitCollaborationSession } from "../document/documentTypes";
|
|
3
|
+
import type { MdKitEditorDebugEvent } from "./editorDebug";
|
|
4
|
+
type MdKitEditorBaseProps = {
|
|
5
|
+
className?: string;
|
|
6
|
+
fillHeight?: boolean;
|
|
7
|
+
instanceKey?: string | number;
|
|
8
|
+
onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
|
|
9
|
+
onFocusChange?: (focused: boolean) => void;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
};
|
|
13
|
+
type LocalMdKitEditorProps = MdKitEditorBaseProps & {
|
|
14
|
+
collaboration?: null;
|
|
15
|
+
onChange?: (markdown: string) => void;
|
|
16
|
+
value: string;
|
|
17
|
+
};
|
|
18
|
+
type CollaborativeMdKitEditorProps = MdKitEditorBaseProps & {
|
|
19
|
+
collaboration: MdKitCollaborationSession;
|
|
20
|
+
onChange?: (markdown: string) => void;
|
|
21
|
+
value?: string;
|
|
22
|
+
};
|
|
23
|
+
export type MdKitEditorProps = CollaborativeMdKitEditorProps | LocalMdKitEditorProps;
|
|
24
|
+
export declare const MdKitEditor: (props: MdKitEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { joinClassNames } from "../ui/joinClassNames";
|
|
3
|
+
import { TiptapMarkdownSurface } from "./TiptapMarkdownSurface";
|
|
4
|
+
export const MdKitEditor = (props) => {
|
|
5
|
+
const { className, fillHeight = false, readOnly = false, style, ...surfaceProps } = props;
|
|
6
|
+
return (_jsx("div", { className: joinClassNames("mdkit-markdown-editor", fillHeight && "mdkit-markdown-editor-fill-height", className), "data-read-only": readOnly ? "true" : undefined, style: style, children: _jsx(TiptapMarkdownSurface, { readOnly: readOnly, ...surfaceProps }, props.instanceKey) }));
|
|
7
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { act, fireEvent, render, screen, waitFor, } from "@testing-library/react";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { MdKitEditor } from "./MdKitEditor";
|
|
8
|
+
const firstMarkdown = "# Stored document\n\nOriginal paragraph.";
|
|
9
|
+
const restoredMarkdown = "# Restored document\n\n- first\n- second";
|
|
10
|
+
const TestHarness = () => {
|
|
11
|
+
const [value, setValue] = useState(firstMarkdown);
|
|
12
|
+
const [revision, setRevision] = useState(0);
|
|
13
|
+
const coldRestore = () => {
|
|
14
|
+
setValue(restoredMarkdown);
|
|
15
|
+
setRevision((current) => current + 1);
|
|
16
|
+
};
|
|
17
|
+
return (_jsxs("div", { children: [_jsx("button", { type: "button", onClick: () => setValue(restoredMarkdown), children: "Replace from storage" }), _jsx("button", { type: "button", onClick: coldRestore, children: "Cold restore" }), _jsx(MdKitEditor, { instanceKey: revision, value: value, onChange: setValue })] }));
|
|
18
|
+
};
|
|
19
|
+
const editorText = (container) => {
|
|
20
|
+
const editor = container.querySelector(".ProseMirror");
|
|
21
|
+
if (!(editor instanceof HTMLElement)) {
|
|
22
|
+
throw new Error("Expected TipTap to render a ProseMirror editor.");
|
|
23
|
+
}
|
|
24
|
+
return editor.textContent ?? "";
|
|
25
|
+
};
|
|
26
|
+
describe("MdKitEditor", () => {
|
|
27
|
+
it("marks the editor as full-height when requested", async () => {
|
|
28
|
+
const { container } = render(_jsx(MdKitEditor, { fillHeight: true, value: firstMarkdown, onChange: () => { } }));
|
|
29
|
+
await waitFor(() => {
|
|
30
|
+
expect(container.querySelector(".mdkit-markdown-editor-fill-height")).toBeTruthy();
|
|
31
|
+
expect(editorText(container)).toContain("Stored document");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it("keeps the editor root full-width without fill-height", async () => {
|
|
35
|
+
const css = readFileSync(resolve(__dirname, "../styles.css"), "utf8");
|
|
36
|
+
expect(css).toMatch(/\.mdkit-markdown-editor\s*{[^}]*width:\s*100%;/);
|
|
37
|
+
});
|
|
38
|
+
it("can render the editor as read-only", async () => {
|
|
39
|
+
const { container } = render(_jsx(MdKitEditor, { readOnly: true, value: "Read-only document", onChange: () => { } }));
|
|
40
|
+
const editor = await waitFor(() => {
|
|
41
|
+
const element = container.querySelector(".ProseMirror");
|
|
42
|
+
if (!(element instanceof HTMLElement)) {
|
|
43
|
+
throw new Error("Expected TipTap to render a ProseMirror editor.");
|
|
44
|
+
}
|
|
45
|
+
return element;
|
|
46
|
+
});
|
|
47
|
+
expect(editor.getAttribute("contenteditable")).toBe("false");
|
|
48
|
+
expect(container
|
|
49
|
+
.querySelector(".mdkit-markdown-editor")
|
|
50
|
+
?.getAttribute("data-read-only")).toBe("true");
|
|
51
|
+
});
|
|
52
|
+
it("hydrates and restores from serialized markdown values", async () => {
|
|
53
|
+
const { container } = render(_jsx(TestHarness, {}));
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
expect(editorText(container)).toContain("Stored document");
|
|
56
|
+
expect(editorText(container)).toContain("Original paragraph.");
|
|
57
|
+
});
|
|
58
|
+
await act(async () => {
|
|
59
|
+
screen.getByRole("button", { name: "Replace from storage" }).click();
|
|
60
|
+
});
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(editorText(container)).toContain("Restored document");
|
|
63
|
+
expect(editorText(container)).toContain("first");
|
|
64
|
+
expect(editorText(container)).toContain("second");
|
|
65
|
+
});
|
|
66
|
+
await act(async () => {
|
|
67
|
+
screen.getByRole("button", { name: "Cold restore" }).click();
|
|
68
|
+
});
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(editorText(container)).toContain("Restored document");
|
|
71
|
+
expect(editorText(container)).toContain("first");
|
|
72
|
+
expect(editorText(container)).toContain("second");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("hydrates middle blank-line runs as visible empty editor paragraphs", async () => {
|
|
76
|
+
const markdown = "Before paragraph.\n\nWhitespace probe:\n\n\n\n\nAfter paragraph.";
|
|
77
|
+
const { container } = render(_jsx(MdKitEditor, { value: markdown, onChange: () => { } }));
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(editorText(container)).toContain("Whitespace probe:");
|
|
80
|
+
expect(editorText(container)).toContain("After paragraph.");
|
|
81
|
+
});
|
|
82
|
+
const paragraphs = Array.from(container.querySelectorAll(".ProseMirror p"));
|
|
83
|
+
const whitespaceParagraphIndex = paragraphs.findIndex((paragraph) => paragraph.textContent === "Whitespace probe:");
|
|
84
|
+
const afterParagraphIndex = paragraphs.findIndex((paragraph) => paragraph.textContent === "After paragraph.");
|
|
85
|
+
expect(whitespaceParagraphIndex).toBeGreaterThanOrEqual(0);
|
|
86
|
+
expect(afterParagraphIndex).toBeGreaterThan(whitespaceParagraphIndex);
|
|
87
|
+
expect(afterParagraphIndex - whitespaceParagraphIndex).toBeGreaterThan(1);
|
|
88
|
+
});
|
|
89
|
+
it("keeps the same editor instance when controlled value changes", async () => {
|
|
90
|
+
const { container, rerender } = render(_jsx(MdKitEditor, { value: "one", onChange: () => { } }));
|
|
91
|
+
const editor = await waitFor(() => {
|
|
92
|
+
const element = container.querySelector(".ProseMirror");
|
|
93
|
+
if (!(element instanceof HTMLElement)) {
|
|
94
|
+
throw new Error("Expected TipTap to render a ProseMirror editor.");
|
|
95
|
+
}
|
|
96
|
+
return element;
|
|
97
|
+
});
|
|
98
|
+
rerender(_jsx(MdKitEditor, { value: "one two", onChange: () => { } }));
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(container.querySelector(".ProseMirror")).toBe(editor);
|
|
101
|
+
expect(editorText(container)).toContain("one two");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it("focuses the editor when the fill-height background is clicked", async () => {
|
|
105
|
+
const onFocusChange = vi.fn();
|
|
106
|
+
const { container } = render(_jsx(MdKitEditor, { fillHeight: true, value: "", onChange: () => { }, onFocusChange: onFocusChange }));
|
|
107
|
+
const surface = await waitFor(() => {
|
|
108
|
+
const element = container.querySelector(".hsk-editor-surface");
|
|
109
|
+
if (!(element instanceof HTMLElement)) {
|
|
110
|
+
throw new Error("Expected editor surface to render.");
|
|
111
|
+
}
|
|
112
|
+
return element;
|
|
113
|
+
});
|
|
114
|
+
const editor = container.querySelector(".ProseMirror");
|
|
115
|
+
if (!(editor instanceof HTMLElement)) {
|
|
116
|
+
throw new Error("Expected TipTap to render a ProseMirror editor.");
|
|
117
|
+
}
|
|
118
|
+
fireEvent.pointerDown(surface);
|
|
119
|
+
fireEvent.pointerUp(surface);
|
|
120
|
+
fireEvent.click(surface);
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(document.activeElement).toBe(editor);
|
|
123
|
+
expect(onFocusChange).toHaveBeenCalledWith(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MdKitCollaborationSession } from "../document/documentTypes";
|
|
2
|
+
import type { MdKitEditorDebugEvent } from "./editorDebug";
|
|
3
|
+
type LocalTiptapMarkdownSurfaceProps = {
|
|
4
|
+
collaboration?: null;
|
|
5
|
+
onChange?: (markdown: string) => void;
|
|
6
|
+
onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
|
|
7
|
+
onFocusChange?: (focused: boolean) => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
readOnly?: boolean;
|
|
10
|
+
value: string;
|
|
11
|
+
};
|
|
12
|
+
type CollaborativeTiptapMarkdownSurfaceProps = {
|
|
13
|
+
collaboration: MdKitCollaborationSession;
|
|
14
|
+
onChange?: (markdown: string) => void;
|
|
15
|
+
onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
|
|
16
|
+
onFocusChange?: (focused: boolean) => void;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
readOnly?: boolean;
|
|
19
|
+
value?: string;
|
|
20
|
+
};
|
|
21
|
+
type TiptapMarkdownSurfaceProps = CollaborativeTiptapMarkdownSurfaceProps | LocalTiptapMarkdownSurfaceProps;
|
|
22
|
+
export declare const TiptapMarkdownSurface: (props: TiptapMarkdownSurfaceProps) => import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import Collaboration from "@tiptap/extension-collaboration";
|
|
4
|
+
import CollaborationCaret from "@tiptap/extension-collaboration-caret";
|
|
5
|
+
import { Markdown } from "@tiptap/markdown";
|
|
6
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
7
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
8
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
9
|
+
import { MarkdownBubbleMenu } from "./MarkdownBubbleMenu";
|
|
10
|
+
import { normalizeMarkdownSerialization } from "./normalizeMarkdownSerialization";
|
|
11
|
+
import { prepareMarkdownForEditorHydration } from "./prepareMarkdownForEditorHydration";
|
|
12
|
+
const describeElement = (element) => {
|
|
13
|
+
const classes = element instanceof HTMLElement && element.className
|
|
14
|
+
? `.${String(element.className).trim().replace(/\s+/g, ".")}`
|
|
15
|
+
: "";
|
|
16
|
+
return `${element.tagName.toLowerCase()}${classes}`;
|
|
17
|
+
};
|
|
18
|
+
const describeEventTarget = (target) => target instanceof Element ? describeElement(target) : String(target);
|
|
19
|
+
const isInteractiveElement = (target) => !!target.closest("a,button,input,select,textarea,[contenteditable='false']");
|
|
20
|
+
const createEditorDebugSnapshot = (editor, phase) => {
|
|
21
|
+
const activeElement = typeof document === "undefined" || !document.activeElement
|
|
22
|
+
? null
|
|
23
|
+
: describeElement(document.activeElement);
|
|
24
|
+
const browserSelection = typeof window === "undefined" ? null : window.getSelection();
|
|
25
|
+
let editorIsFocused = null;
|
|
26
|
+
let selectionAnchor = null;
|
|
27
|
+
let selectionEmpty = null;
|
|
28
|
+
let selectionFrom = null;
|
|
29
|
+
let selectionHead = null;
|
|
30
|
+
let selectionTo = null;
|
|
31
|
+
let viewHasFocus = null;
|
|
32
|
+
let viewUnavailable = false;
|
|
33
|
+
try {
|
|
34
|
+
editorIsFocused = editor.isFocused;
|
|
35
|
+
selectionAnchor = editor.state.selection.anchor;
|
|
36
|
+
selectionEmpty = editor.state.selection.empty;
|
|
37
|
+
selectionFrom = editor.state.selection.from;
|
|
38
|
+
selectionHead = editor.state.selection.head;
|
|
39
|
+
selectionTo = editor.state.selection.to;
|
|
40
|
+
viewHasFocus = editor.view.hasFocus();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
viewUnavailable = true;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
activeElement,
|
|
47
|
+
browserSelectionAnchorNode: browserSelection?.anchorNode instanceof Element
|
|
48
|
+
? describeElement(browserSelection.anchorNode)
|
|
49
|
+
: (browserSelection?.anchorNode?.nodeName ?? null),
|
|
50
|
+
browserSelectionAnchorOffset: browserSelection?.anchorOffset ?? null,
|
|
51
|
+
editorIsFocused,
|
|
52
|
+
phase,
|
|
53
|
+
selectionAnchor,
|
|
54
|
+
selectionEmpty,
|
|
55
|
+
selectionFrom,
|
|
56
|
+
selectionHead,
|
|
57
|
+
selectionTo,
|
|
58
|
+
viewHasFocus,
|
|
59
|
+
viewUnavailable,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export const TiptapMarkdownSurface = (props) => {
|
|
63
|
+
const { collaboration = null, onDebugEvent, onFocusChange, placeholder = "Start writing...", readOnly = false, } = props;
|
|
64
|
+
const markdownValue = "value" in props && typeof props.value === "string" ? props.value : "";
|
|
65
|
+
const editorSurfaceRef = useRef(null);
|
|
66
|
+
const onDebugEventRef = useRef(onDebugEvent);
|
|
67
|
+
const onFocusChangeRef = useRef(onFocusChange);
|
|
68
|
+
const onChangeRef = useRef(props.onChange);
|
|
69
|
+
const currentMarkdownRef = useRef(markdownValue);
|
|
70
|
+
const isApplyingExternalValueRef = useRef(false);
|
|
71
|
+
const pendingControlledEchoesRef = useRef(new Set());
|
|
72
|
+
const pendingContentFocusRef = useRef(null);
|
|
73
|
+
const shouldFocusAfterPointerRef = useRef(false);
|
|
74
|
+
const collaborationDocument = collaboration?.document ?? null;
|
|
75
|
+
const collaborationProvider = collaboration?.provider ?? null;
|
|
76
|
+
const collaborationUserColor = collaboration?.collaborator.color ?? "";
|
|
77
|
+
const collaborationUserName = collaboration?.collaborator.name ?? "";
|
|
78
|
+
const hasCollaboration = !!collaborationDocument;
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
onDebugEventRef.current = onDebugEvent;
|
|
81
|
+
}, [onDebugEvent]);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
onFocusChangeRef.current = onFocusChange;
|
|
84
|
+
}, [onFocusChange]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
onChangeRef.current = props.onChange;
|
|
87
|
+
}, [props.onChange]);
|
|
88
|
+
const collaborationCaretExtensions = useMemo(() => collaborationProvider
|
|
89
|
+
? [
|
|
90
|
+
CollaborationCaret.configure({
|
|
91
|
+
provider: collaborationProvider,
|
|
92
|
+
render: (user) => {
|
|
93
|
+
const cursor = document.createElement("span");
|
|
94
|
+
cursor.classList.add("hsk-collaboration-caret");
|
|
95
|
+
cursor.style.borderColor = user.color;
|
|
96
|
+
const label = document.createElement("div");
|
|
97
|
+
label.classList.add("hsk-collaboration-caret-label");
|
|
98
|
+
label.style.backgroundColor = user.color;
|
|
99
|
+
label.textContent = user.name;
|
|
100
|
+
cursor.appendChild(label);
|
|
101
|
+
return cursor;
|
|
102
|
+
},
|
|
103
|
+
selectionRender: (user) => ({
|
|
104
|
+
style: `background-color: ${user.color}20`,
|
|
105
|
+
}),
|
|
106
|
+
user: {
|
|
107
|
+
color: collaborationUserColor,
|
|
108
|
+
name: collaborationUserName,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
]
|
|
112
|
+
: [], [collaborationProvider, collaborationUserColor, collaborationUserName]);
|
|
113
|
+
const editor = useEditor({
|
|
114
|
+
content: hasCollaboration
|
|
115
|
+
? undefined
|
|
116
|
+
: prepareMarkdownForEditorHydration(markdownValue),
|
|
117
|
+
contentType: "markdown",
|
|
118
|
+
editable: !readOnly,
|
|
119
|
+
editorProps: {
|
|
120
|
+
attributes: {
|
|
121
|
+
class: "hsk-tiptap",
|
|
122
|
+
spellcheck: "false",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
extensions: [
|
|
126
|
+
StarterKit.configure({
|
|
127
|
+
heading: { levels: [1, 2] },
|
|
128
|
+
link: {
|
|
129
|
+
HTMLAttributes: {
|
|
130
|
+
rel: "noopener noreferrer",
|
|
131
|
+
target: "_blank",
|
|
132
|
+
},
|
|
133
|
+
autolink: true,
|
|
134
|
+
linkOnPaste: true,
|
|
135
|
+
openOnClick: true,
|
|
136
|
+
},
|
|
137
|
+
undoRedo: hasCollaboration ? false : undefined,
|
|
138
|
+
}),
|
|
139
|
+
Placeholder.configure({
|
|
140
|
+
placeholder,
|
|
141
|
+
}),
|
|
142
|
+
Markdown.configure({
|
|
143
|
+
markedOptions: {
|
|
144
|
+
gfm: true,
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
...(collaborationDocument
|
|
148
|
+
? [
|
|
149
|
+
Collaboration.configure({
|
|
150
|
+
document: collaborationDocument,
|
|
151
|
+
}),
|
|
152
|
+
...collaborationCaretExtensions,
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
155
|
+
],
|
|
156
|
+
onBlur: ({ editor: blurredEditor }) => {
|
|
157
|
+
onDebugEventRef.current?.({
|
|
158
|
+
detail: createEditorDebugSnapshot(blurredEditor, "blur"),
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
type: "editor-blur",
|
|
161
|
+
});
|
|
162
|
+
onFocusChangeRef.current?.(false);
|
|
163
|
+
},
|
|
164
|
+
onFocus: ({ editor: focusedEditor }) => {
|
|
165
|
+
onDebugEventRef.current?.({
|
|
166
|
+
detail: createEditorDebugSnapshot(focusedEditor, "focus"),
|
|
167
|
+
timestamp: Date.now(),
|
|
168
|
+
type: "editor-focus",
|
|
169
|
+
});
|
|
170
|
+
onFocusChangeRef.current?.(true);
|
|
171
|
+
},
|
|
172
|
+
onUpdate: ({ editor: updatedEditor }) => {
|
|
173
|
+
if (isApplyingExternalValueRef.current) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const nextSerializedMarkdown = normalizeMarkdownSerialization(updatedEditor.getMarkdown());
|
|
177
|
+
const previousMarkdown = currentMarkdownRef.current;
|
|
178
|
+
const nextMarkdown = nextSerializedMarkdown;
|
|
179
|
+
currentMarkdownRef.current = nextMarkdown;
|
|
180
|
+
if (nextMarkdown !== previousMarkdown) {
|
|
181
|
+
pendingControlledEchoesRef.current.add(nextMarkdown);
|
|
182
|
+
onChangeRef.current?.(nextMarkdown);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
}, [
|
|
186
|
+
collaborationCaretExtensions,
|
|
187
|
+
collaborationDocument,
|
|
188
|
+
hasCollaboration,
|
|
189
|
+
placeholder,
|
|
190
|
+
]);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
editor?.setEditable(!readOnly);
|
|
193
|
+
}, [editor, readOnly]);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!editor) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (markdownValue === currentMarkdownRef.current) {
|
|
199
|
+
pendingControlledEchoesRef.current.clear();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (pendingControlledEchoesRef.current.has(markdownValue)) {
|
|
203
|
+
pendingControlledEchoesRef.current.delete(markdownValue);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
pendingControlledEchoesRef.current.clear();
|
|
207
|
+
isApplyingExternalValueRef.current = true;
|
|
208
|
+
editor.commands.setContent(prepareMarkdownForEditorHydration(markdownValue), {
|
|
209
|
+
contentType: "markdown",
|
|
210
|
+
emitUpdate: false,
|
|
211
|
+
});
|
|
212
|
+
currentMarkdownRef.current = markdownValue;
|
|
213
|
+
window.queueMicrotask(() => {
|
|
214
|
+
isApplyingExternalValueRef.current = false;
|
|
215
|
+
});
|
|
216
|
+
}, [editor, markdownValue]);
|
|
217
|
+
if (!editor) {
|
|
218
|
+
return (_jsx("div", { className: "hsk-editor-shell", children: _jsx("div", { className: "hsk-editor-empty", children: collaboration
|
|
219
|
+
? "Connecting collaboration session..."
|
|
220
|
+
: "Loading editor..." }) }));
|
|
221
|
+
}
|
|
222
|
+
const getProseMirrorElement = () => editorSurfaceRef.current?.querySelector(".ProseMirror");
|
|
223
|
+
const clampEditorPosition = (position) => Math.max(0, Math.min(position, editor.state.doc.content.size));
|
|
224
|
+
const getEditorBackgroundPositionAtClientPoint = (proseMirror, clientY) => {
|
|
225
|
+
const blockElements = Array.from(proseMirror.children).filter((child) => child instanceof HTMLElement);
|
|
226
|
+
if (blockElements.length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
for (let index = 0; index < blockElements.length; index += 1) {
|
|
230
|
+
const block = blockElements[index];
|
|
231
|
+
const rect = block.getBoundingClientRect();
|
|
232
|
+
if (clientY <= rect.bottom) {
|
|
233
|
+
if (clientY >= rect.top) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const previousBlock = blockElements[index - 1];
|
|
237
|
+
if (previousBlock) {
|
|
238
|
+
const previousRect = previousBlock.getBoundingClientRect();
|
|
239
|
+
const distanceFromPrevious = Math.abs(clientY - previousRect.bottom);
|
|
240
|
+
const distanceFromNext = Math.abs(rect.top - clientY);
|
|
241
|
+
if (distanceFromPrevious <= distanceFromNext) {
|
|
242
|
+
return editor.view.posAtDOM(previousBlock, previousBlock.childNodes.length);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return editor.view.posAtDOM(block, 0);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const lastBlock = blockElements[blockElements.length - 1];
|
|
249
|
+
return editor.view.posAtDOM(lastBlock, lastBlock.childNodes.length);
|
|
250
|
+
};
|
|
251
|
+
const getEditorPositionAtClientPoint = (clientX, clientY, target) => {
|
|
252
|
+
const proseMirror = getProseMirrorElement();
|
|
253
|
+
const targetIsEditorBackground = proseMirror &&
|
|
254
|
+
target instanceof Element &&
|
|
255
|
+
(target === proseMirror || !proseMirror.contains(target));
|
|
256
|
+
if (proseMirror && targetIsEditorBackground) {
|
|
257
|
+
const backgroundPosition = getEditorBackgroundPositionAtClientPoint(proseMirror, clientY);
|
|
258
|
+
if (typeof backgroundPosition === "number") {
|
|
259
|
+
return clampEditorPosition(backgroundPosition);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let coordinatePosition;
|
|
263
|
+
try {
|
|
264
|
+
coordinatePosition = editor.view.posAtCoords({
|
|
265
|
+
left: clientX,
|
|
266
|
+
top: clientY,
|
|
267
|
+
})?.pos;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
coordinatePosition = undefined;
|
|
271
|
+
}
|
|
272
|
+
if (typeof coordinatePosition !== "number") {
|
|
273
|
+
return editor.state.doc.content.size;
|
|
274
|
+
}
|
|
275
|
+
return clampEditorPosition(coordinatePosition);
|
|
276
|
+
};
|
|
277
|
+
const focusEditorAtPosition = (position) => {
|
|
278
|
+
if (editor.isDestroyed || readOnly) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const { state, view } = editor;
|
|
282
|
+
const nextPosition = Math.max(0, Math.min(position, state.doc.content.size));
|
|
283
|
+
try {
|
|
284
|
+
emitDebugEvent("focus-at-position-before", {
|
|
285
|
+
requestedPosition: nextPosition,
|
|
286
|
+
});
|
|
287
|
+
view.focus();
|
|
288
|
+
editor.commands.setTextSelection(nextPosition);
|
|
289
|
+
view.focus();
|
|
290
|
+
emitDebugEvent("focus-at-position-after", {
|
|
291
|
+
requestedPosition: nextPosition,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
emitDebugEvent("focus-at-position-aborted", {
|
|
296
|
+
requestedPosition: nextPosition,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const queueEditorFocusAtPosition = (position) => {
|
|
301
|
+
emitDebugEvent("focus-queued", {
|
|
302
|
+
pendingFromPointer: shouldFocusAfterPointerRef.current,
|
|
303
|
+
requestedPosition: position,
|
|
304
|
+
});
|
|
305
|
+
window.setTimeout(() => {
|
|
306
|
+
focusEditorAtPosition(position);
|
|
307
|
+
window.requestAnimationFrame(() => {
|
|
308
|
+
emitDebugEvent("focus-raf-run", {
|
|
309
|
+
requestedPosition: position,
|
|
310
|
+
});
|
|
311
|
+
focusEditorAtPosition(position);
|
|
312
|
+
});
|
|
313
|
+
}, 0);
|
|
314
|
+
};
|
|
315
|
+
const emitDebugEvent = (type, detail) => {
|
|
316
|
+
const event = {
|
|
317
|
+
detail: {
|
|
318
|
+
...detail,
|
|
319
|
+
...createEditorDebugSnapshot(editor, type),
|
|
320
|
+
},
|
|
321
|
+
timestamp: Date.now(),
|
|
322
|
+
type,
|
|
323
|
+
};
|
|
324
|
+
onDebugEventRef.current?.(event);
|
|
325
|
+
};
|
|
326
|
+
const shouldFocusEditorBackground = (target) => {
|
|
327
|
+
if (!(target instanceof Element)) {
|
|
328
|
+
emitDebugEvent("hitbox-target-not-element", {
|
|
329
|
+
targetType: typeof target,
|
|
330
|
+
});
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const proseMirror = getProseMirrorElement();
|
|
334
|
+
if (!proseMirror) {
|
|
335
|
+
emitDebugEvent("hitbox-missing-prosemirror", {
|
|
336
|
+
target: describeElement(target),
|
|
337
|
+
});
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
const targetIsInsideEditor = proseMirror.contains(target);
|
|
341
|
+
const targetIsEmptyPlaceholder = !!target.closest(".ProseMirror p.is-editor-empty");
|
|
342
|
+
const editorIsEmpty = editor.isEmpty;
|
|
343
|
+
const shouldFocus = !isInteractiveElement(target) &&
|
|
344
|
+
(target === proseMirror ||
|
|
345
|
+
!targetIsInsideEditor ||
|
|
346
|
+
targetIsEmptyPlaceholder ||
|
|
347
|
+
(editorIsEmpty && targetIsInsideEditor));
|
|
348
|
+
emitDebugEvent("hitbox-check", {
|
|
349
|
+
editorIsEmpty,
|
|
350
|
+
proseMirrorContainsTarget: targetIsInsideEditor,
|
|
351
|
+
shouldFocus,
|
|
352
|
+
target: describeElement(target),
|
|
353
|
+
targetIsEmptyPlaceholder,
|
|
354
|
+
targetIsProseMirror: target === proseMirror,
|
|
355
|
+
});
|
|
356
|
+
return shouldFocus;
|
|
357
|
+
};
|
|
358
|
+
const focusEditorBackgroundOnPointerDown = (event) => {
|
|
359
|
+
const proseMirror = editorSurfaceRef.current?.querySelector(".ProseMirror");
|
|
360
|
+
const target = event.target;
|
|
361
|
+
if (readOnly) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (proseMirror &&
|
|
365
|
+
target instanceof Element &&
|
|
366
|
+
proseMirror.contains(target) &&
|
|
367
|
+
target !== proseMirror &&
|
|
368
|
+
!editor.isFocused &&
|
|
369
|
+
!editor.isEmpty &&
|
|
370
|
+
!target.closest(".ProseMirror p.is-editor-empty") &&
|
|
371
|
+
!isInteractiveElement(target)) {
|
|
372
|
+
pendingContentFocusRef.current = {
|
|
373
|
+
pointerId: event.pointerId,
|
|
374
|
+
x: event.clientX,
|
|
375
|
+
y: event.clientY,
|
|
376
|
+
};
|
|
377
|
+
emitDebugEvent("content-pointer-focus-pending", {
|
|
378
|
+
pointerType: event.pointerType,
|
|
379
|
+
target: describeElement(target),
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!shouldFocusEditorBackground(event.target)) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const requestedPosition = getEditorPositionAtClientPoint(event.clientX, event.clientY, event.target);
|
|
387
|
+
event.preventDefault();
|
|
388
|
+
shouldFocusAfterPointerRef.current = false;
|
|
389
|
+
emitDebugEvent("hitbox-pointer-down", {
|
|
390
|
+
defaultPrevented: event.defaultPrevented,
|
|
391
|
+
pointerType: event.pointerType,
|
|
392
|
+
requestedPosition,
|
|
393
|
+
target: describeEventTarget(event.target),
|
|
394
|
+
});
|
|
395
|
+
queueEditorFocusAtPosition(requestedPosition);
|
|
396
|
+
};
|
|
397
|
+
const focusEditorBackgroundOnPointerUp = (event) => {
|
|
398
|
+
const pendingContentFocus = pendingContentFocusRef.current;
|
|
399
|
+
if (readOnly) {
|
|
400
|
+
pendingContentFocusRef.current = null;
|
|
401
|
+
shouldFocusAfterPointerRef.current = false;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (pendingContentFocus &&
|
|
405
|
+
pendingContentFocus.pointerId === event.pointerId) {
|
|
406
|
+
pendingContentFocusRef.current = null;
|
|
407
|
+
const moved = Math.hypot(event.clientX - pendingContentFocus.x, event.clientY - pendingContentFocus.y);
|
|
408
|
+
emitDebugEvent("content-pointer-focus-resolve", {
|
|
409
|
+
moved,
|
|
410
|
+
pointerType: event.pointerType,
|
|
411
|
+
});
|
|
412
|
+
if (moved < 4 && !editor.isFocused) {
|
|
413
|
+
editor.view.focus();
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (!shouldFocusAfterPointerRef.current) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
shouldFocusAfterPointerRef.current = false;
|
|
422
|
+
emitDebugEvent("hitbox-pointer-up", {
|
|
423
|
+
defaultPrevented: event.defaultPrevented,
|
|
424
|
+
pointerType: event.pointerType,
|
|
425
|
+
target: describeEventTarget(event.target),
|
|
426
|
+
});
|
|
427
|
+
queueEditorFocusAtPosition(getEditorPositionAtClientPoint(event.clientX, event.clientY, event.target));
|
|
428
|
+
};
|
|
429
|
+
return (_jsx("div", { className: "hsk-editor-shell", children: _jsxs("div", { ref: editorSurfaceRef, className: "hsk-editor-surface", onPointerDownCapture: focusEditorBackgroundOnPointerDown, onPointerUpCapture: focusEditorBackgroundOnPointerUp, children: [_jsx(MarkdownBubbleMenu, { editor: editor }), _jsx(EditorContent, { editor: editor })] }) }));
|
|
430
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fenceStartPattern = /^ {0,3}(`{3,}|~{3,})/;
|
|
2
|
+
export const getMarkdownFenceRanges = (markdown) => {
|
|
3
|
+
const ranges = [];
|
|
4
|
+
let activeFence = null;
|
|
5
|
+
let lineStart = 0;
|
|
6
|
+
while (lineStart < markdown.length) {
|
|
7
|
+
const newlineIndex = markdown.indexOf("\n", lineStart);
|
|
8
|
+
const lineEnd = newlineIndex === -1 ? markdown.length : newlineIndex;
|
|
9
|
+
const nextLineStart = newlineIndex === -1 ? markdown.length : newlineIndex + 1;
|
|
10
|
+
const line = markdown.slice(lineStart, lineEnd);
|
|
11
|
+
const fenceMatch = line.match(fenceStartPattern);
|
|
12
|
+
if (fenceMatch) {
|
|
13
|
+
const marker = fenceMatch[1] ?? "";
|
|
14
|
+
const markerChar = marker[0];
|
|
15
|
+
if (!activeFence) {
|
|
16
|
+
activeFence = {
|
|
17
|
+
char: markerChar,
|
|
18
|
+
length: marker.length,
|
|
19
|
+
start: lineStart,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
else if (markerChar === activeFence.char &&
|
|
23
|
+
marker.length >= activeFence.length) {
|
|
24
|
+
ranges.push({
|
|
25
|
+
start: activeFence.start,
|
|
26
|
+
end: nextLineStart,
|
|
27
|
+
});
|
|
28
|
+
activeFence = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
lineStart = nextLineStart;
|
|
32
|
+
}
|
|
33
|
+
if (activeFence) {
|
|
34
|
+
ranges.push({
|
|
35
|
+
start: activeFence.start,
|
|
36
|
+
end: markdown.length,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return ranges;
|
|
40
|
+
};
|
|
41
|
+
export const isInsideMarkdownFence = (index, ranges) => ranges.some((range) => index >= range.start && index < range.end - 1);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const normalizeMarkdownSerialization: (markdown: string) => string;
|