@spatika/editor 1.5.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 +43 -0
- package/dist/composites/AiCommandDock.d.ts +13 -0
- package/dist/composites/AiCommandDock.d.ts.map +1 -0
- package/dist/composites/AiCommandDock.js +63 -0
- package/dist/composites/AiCommandMenu.d.ts +16 -0
- package/dist/composites/AiCommandMenu.d.ts.map +1 -0
- package/dist/composites/AiCommandMenu.js +51 -0
- package/dist/composites/AiCommandPanel.d.ts +12 -0
- package/dist/composites/AiCommandPanel.d.ts.map +1 -0
- package/dist/composites/AiCommandPanel.js +12 -0
- package/dist/composites/AiOverlay.d.ts +7 -0
- package/dist/composites/AiOverlay.d.ts.map +1 -0
- package/dist/composites/AiOverlay.js +8 -0
- package/dist/composites/EditorBubbleMenu.d.ts +10 -0
- package/dist/composites/EditorBubbleMenu.d.ts.map +1 -0
- package/dist/composites/EditorBubbleMenu.js +16 -0
- package/dist/composites/EditorToolbar.d.ts +20 -0
- package/dist/composites/EditorToolbar.d.ts.map +1 -0
- package/dist/composites/EditorToolbar.js +45 -0
- package/dist/composites/ImageBubbleMenu.d.ts +7 -0
- package/dist/composites/ImageBubbleMenu.d.ts.map +1 -0
- package/dist/composites/ImageBubbleMenu.js +15 -0
- package/dist/composites/MentionMenu.d.ts +11 -0
- package/dist/composites/MentionMenu.d.ts.map +1 -0
- package/dist/composites/MentionMenu.js +10 -0
- package/dist/composites/SlashCommandMenu.d.ts +13 -0
- package/dist/composites/SlashCommandMenu.d.ts.map +1 -0
- package/dist/composites/SlashCommandMenu.js +10 -0
- package/dist/composites/SpatikaEditor.d.ts +14 -0
- package/dist/composites/SpatikaEditor.d.ts.map +1 -0
- package/dist/composites/SpatikaEditor.js +159 -0
- package/dist/composites/TableGridPicker.d.ts +9 -0
- package/dist/composites/TableGridPicker.d.ts.map +1 -0
- package/dist/composites/TableGridPicker.js +54 -0
- package/dist/composites/ToolbarButton.d.ts +7 -0
- package/dist/composites/ToolbarButton.d.ts.map +1 -0
- package/dist/composites/ToolbarButton.js +7 -0
- package/dist/composites/ToolbarFormatPanels.d.ts +9 -0
- package/dist/composites/ToolbarFormatPanels.d.ts.map +1 -0
- package/dist/composites/ToolbarFormatPanels.js +35 -0
- package/dist/composites/ToolbarInsertPanels.d.ts +14 -0
- package/dist/composites/ToolbarInsertPanels.d.ts.map +1 -0
- package/dist/composites/ToolbarInsertPanels.js +96 -0
- package/dist/composites/ToolbarPopover.d.ts +14 -0
- package/dist/composites/ToolbarPopover.d.ts.map +1 -0
- package/dist/composites/ToolbarPopover.js +48 -0
- package/dist/extensions/ai-action.d.ts +24 -0
- package/dist/extensions/ai-action.d.ts.map +1 -0
- package/dist/extensions/ai-action.js +53 -0
- package/dist/extensions/mention.d.ts +17 -0
- package/dist/extensions/mention.d.ts.map +1 -0
- package/dist/extensions/mention.js +129 -0
- package/dist/extensions/resizable-image-view.d.ts +18 -0
- package/dist/extensions/resizable-image-view.d.ts.map +1 -0
- package/dist/extensions/resizable-image-view.js +70 -0
- package/dist/extensions/resizable-image.d.ts +4 -0
- package/dist/extensions/resizable-image.d.ts.map +1 -0
- package/dist/extensions/resizable-image.js +68 -0
- package/dist/extensions/slash-command.d.ts +18 -0
- package/dist/extensions/slash-command.d.ts.map +1 -0
- package/dist/extensions/slash-command.js +178 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/lib/ai-command-menu.d.ts +9 -0
- package/dist/lib/ai-command-menu.d.ts.map +1 -0
- package/dist/lib/ai-command-menu.js +43 -0
- package/dist/lib/cn.d.ts +4 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +6 -0
- package/dist/lib/editor-format-colors.d.ts +10 -0
- package/dist/lib/editor-format-colors.d.ts.map +1 -0
- package/dist/lib/editor-format-colors.js +24 -0
- package/dist/lib/image-upload.d.ts +14 -0
- package/dist/lib/image-upload.d.ts.map +1 -0
- package/dist/lib/image-upload.js +29 -0
- package/dist/lib/insert-editor-images.d.ts +5 -0
- package/dist/lib/insert-editor-images.d.ts.map +1 -0
- package/dist/lib/insert-editor-images.js +32 -0
- package/dist/lib/types.d.ts +131 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +12 -0
- package/dist/lib/use-spatika-editor.d.ts +13 -0
- package/dist/lib/use-spatika-editor.d.ts.map +1 -0
- package/dist/lib/use-spatika-editor.js +171 -0
- package/dist/styles.css +1056 -0
- package/package.json +101 -0
- package/src/composites/AiCommandDock.test.tsx +48 -0
- package/src/composites/AiCommandDock.tsx +137 -0
- package/src/composites/AiCommandMenu.test.tsx +67 -0
- package/src/composites/AiCommandMenu.tsx +120 -0
- package/src/composites/AiCommandPanel.test.tsx +42 -0
- package/src/composites/AiCommandPanel.tsx +56 -0
- package/src/composites/AiOverlay.test.tsx +18 -0
- package/src/composites/AiOverlay.tsx +18 -0
- package/src/composites/EditorBubbleMenu.test.tsx +49 -0
- package/src/composites/EditorBubbleMenu.tsx +73 -0
- package/src/composites/EditorToolbar.test.tsx +146 -0
- package/src/composites/EditorToolbar.tsx +362 -0
- package/src/composites/ImageBubbleMenu.test.tsx +43 -0
- package/src/composites/ImageBubbleMenu.tsx +51 -0
- package/src/composites/MentionMenu.test.tsx +25 -0
- package/src/composites/MentionMenu.tsx +46 -0
- package/src/composites/SlashCommandMenu.test.tsx +33 -0
- package/src/composites/SlashCommandMenu.tsx +54 -0
- package/src/composites/SpatikaEditor.test.tsx +167 -0
- package/src/composites/SpatikaEditor.tsx +302 -0
- package/src/composites/TableGridPicker.test.tsx +26 -0
- package/src/composites/TableGridPicker.tsx +138 -0
- package/src/composites/ToolbarButton.test.tsx +21 -0
- package/src/composites/ToolbarButton.tsx +24 -0
- package/src/composites/ToolbarFormatPanels.test.tsx +46 -0
- package/src/composites/ToolbarFormatPanels.tsx +78 -0
- package/src/composites/ToolbarInsertPanels.test.tsx +60 -0
- package/src/composites/ToolbarInsertPanels.tsx +200 -0
- package/src/composites/ToolbarPopover.test.tsx +24 -0
- package/src/composites/ToolbarPopover.tsx +95 -0
- package/src/extensions/ai-action.test.ts +26 -0
- package/src/extensions/ai-action.ts +73 -0
- package/src/extensions/mention.test.ts +31 -0
- package/src/extensions/mention.ts +158 -0
- package/src/extensions/resizable-image-view.test.ts +26 -0
- package/src/extensions/resizable-image-view.ts +96 -0
- package/src/extensions/resizable-image.test.ts +28 -0
- package/src/extensions/resizable-image.ts +75 -0
- package/src/extensions/slash-command.test.ts +45 -0
- package/src/extensions/slash-command.ts +197 -0
- package/src/index.ts +49 -0
- package/src/lib/ai-command-menu.test.ts +48 -0
- package/src/lib/ai-command-menu.ts +57 -0
- package/src/lib/cn.ts +7 -0
- package/src/lib/editor-format-colors.test.ts +15 -0
- package/src/lib/editor-format-colors.ts +31 -0
- package/src/lib/image-upload.test.ts +19 -0
- package/src/lib/image-upload.ts +49 -0
- package/src/lib/insert-editor-images.test.ts +76 -0
- package/src/lib/insert-editor-images.ts +44 -0
- package/src/lib/types.ts +149 -0
- package/src/lib/use-spatika-editor.test.tsx +56 -0
- package/src/lib/use-spatika-editor.ts +191 -0
- package/src/styles/editor.css +1056 -0
- package/src/test/setup.ts +46 -0
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Editor, Extensions } from "@tiptap/core";
|
|
2
|
+
import type { StarterKitOptions } from "@tiptap/starter-kit";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export type SpatikaEditorToolbarConfig = {
|
|
6
|
+
/** Show text style group (paragraph, headings). Default true. */
|
|
7
|
+
textStyle?: boolean;
|
|
8
|
+
/** Show inline formatting (bold, italic, underline, strike, code, sub/sup, colors, blockquote). Default true. */
|
|
9
|
+
inlineFormat?: boolean;
|
|
10
|
+
/** Show list controls (bullet, numbered, task). Default true. */
|
|
11
|
+
lists?: boolean;
|
|
12
|
+
/** Show text alignment (left, center, right, justify). Default true. */
|
|
13
|
+
alignment?: boolean;
|
|
14
|
+
/** Show insert controls (link, divider, table, image). Default true. */
|
|
15
|
+
insert?: boolean;
|
|
16
|
+
/** Show AI actions. Default true. */
|
|
17
|
+
ai?: boolean;
|
|
18
|
+
/** Show undo/redo controls. Default true. */
|
|
19
|
+
history?: boolean;
|
|
20
|
+
/** Toolbar layout. Default "ribbon". */
|
|
21
|
+
layout?: "ribbon" | "compact";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ImageInsertConfig = {
|
|
25
|
+
/** Maximum files per drop or picker action. Default 3. */
|
|
26
|
+
maxFiles?: number;
|
|
27
|
+
/** Maximum bytes per file. Default 5MB. */
|
|
28
|
+
maxSizeBytes?: number;
|
|
29
|
+
/** Accepted MIME types for uploads. Default `image/*`. */
|
|
30
|
+
accept?: string;
|
|
31
|
+
/** Host-owned upload handler. Defaults to inline base64 data URLs. */
|
|
32
|
+
onUpload?: (file: File) => Promise<string>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type SlashCommandItem = {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
icon?: ReactNode;
|
|
40
|
+
keywords?: string[];
|
|
41
|
+
command: (editor: Editor) => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type MentionContact = {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
avatarUrl?: string;
|
|
48
|
+
subtitle?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type AiActionDefinition = {
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
icon?: ReactNode;
|
|
56
|
+
/** When true the action stays disabled until the user selects text. */
|
|
57
|
+
requiresSelection?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type AiCommandPromptConfig = {
|
|
61
|
+
enabled?: boolean;
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
submitLabel?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type AiCommandMenuConfig = {
|
|
67
|
+
/** Panel heading. Default "AI commands". */
|
|
68
|
+
title?: string;
|
|
69
|
+
/** Preset actions shown in the menu. Falls back to `aiActions` then defaults. */
|
|
70
|
+
actions?: AiActionDefinition[];
|
|
71
|
+
/** Free-form prompt field. Default enabled. Pass `false` to hide it. */
|
|
72
|
+
prompt?: boolean | AiCommandPromptConfig;
|
|
73
|
+
/** Mirror preset actions in the selection bubble menu. Default false. */
|
|
74
|
+
bubbleActions?: boolean;
|
|
75
|
+
/** Where the AI command UI renders. Default `dock` (Tiptap-style prompt bar). */
|
|
76
|
+
placement?: "toolbar" | "dock";
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Sent to `onAiCommand` when the user runs a preset action or submits a prompt. */
|
|
80
|
+
export type AiCommandContext = {
|
|
81
|
+
/** Preset action id, or `"prompt"` for a free-form request. */
|
|
82
|
+
actionId: string;
|
|
83
|
+
/** User text from the prompt field when `actionId` is `"prompt"`. */
|
|
84
|
+
prompt?: string;
|
|
85
|
+
selectedText: string;
|
|
86
|
+
documentText: string;
|
|
87
|
+
html: string;
|
|
88
|
+
selection: { from: number; to: number } | null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const AI_PROMPT_ACTION_ID = "prompt";
|
|
92
|
+
|
|
93
|
+
export type SpatikaEditorHandle = {
|
|
94
|
+
focus: () => void;
|
|
95
|
+
setContent: (html: string) => void;
|
|
96
|
+
getHTML: () => string;
|
|
97
|
+
getJSON: () => unknown;
|
|
98
|
+
expandFullscreen: () => void;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type PlaceholderConfig =
|
|
102
|
+
| string
|
|
103
|
+
| ((node: { type: string; level?: number; pos: number }) => string);
|
|
104
|
+
|
|
105
|
+
export type UseSpatikaEditorOptions = {
|
|
106
|
+
value: string;
|
|
107
|
+
onChange: (html: string) => void;
|
|
108
|
+
placeholder?: PlaceholderConfig;
|
|
109
|
+
disabled?: boolean;
|
|
110
|
+
extensions?: Extensions;
|
|
111
|
+
starterKit?: Partial<StarterKitOptions>;
|
|
112
|
+
slashCommands?: SlashCommandItem[];
|
|
113
|
+
mentions?: MentionContact[];
|
|
114
|
+
/** @deprecated Prefer `onAiCommand` for preset actions and free-form prompts. */
|
|
115
|
+
onAiSelectionAction?: (action: string, selectedText: string) => Promise<string | null>;
|
|
116
|
+
onAiCommand?: (context: AiCommandContext) => Promise<string | null>;
|
|
117
|
+
resizableImages?: boolean;
|
|
118
|
+
onSlashUpdate?: (state: import("../extensions/slash-command").SlashCommandStorage) => void;
|
|
119
|
+
onMentionUpdate?: (state: import("../extensions/mention").MentionStorage) => void;
|
|
120
|
+
imageInsert?: ImageInsertConfig;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type SpatikaEditorProps = UseSpatikaEditorOptions & {
|
|
124
|
+
className?: string;
|
|
125
|
+
heightClassName?: string;
|
|
126
|
+
toolbar?: SpatikaEditorToolbarConfig;
|
|
127
|
+
toolbarActions?: ReactNode;
|
|
128
|
+
isAiProcessing?: boolean;
|
|
129
|
+
/** Preset AI actions for legacy toolbar/bubble buttons when `aiCommandMenu` is false. */
|
|
130
|
+
aiActions?: AiActionDefinition[];
|
|
131
|
+
/** Tiptap-style AI command menu. Default on when an AI handler is provided. Pass `false` for legacy buttons. */
|
|
132
|
+
aiCommandMenu?: AiCommandMenuConfig | false;
|
|
133
|
+
imageInsert?: ImageInsertConfig;
|
|
134
|
+
onInsertImage?: () => void;
|
|
135
|
+
mobileFullscreen?: boolean;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const DEFAULT_TOOLBAR: Required<SpatikaEditorToolbarConfig> = {
|
|
139
|
+
textStyle: true,
|
|
140
|
+
inlineFormat: true,
|
|
141
|
+
lists: true,
|
|
142
|
+
alignment: true,
|
|
143
|
+
insert: true,
|
|
144
|
+
ai: true,
|
|
145
|
+
history: true,
|
|
146
|
+
layout: "ribbon",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const DEFAULT_SLASH_COMMANDS: SlashCommandItem[] = [];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { buildSpatikaExtensions, useSpatikaEditor } from "./use-spatika-editor";
|
|
4
|
+
|
|
5
|
+
describe("buildSpatikaExtensions", () => {
|
|
6
|
+
it("adds mention and AI extensions when configured", () => {
|
|
7
|
+
const extensions = buildSpatikaExtensions({
|
|
8
|
+
value: "",
|
|
9
|
+
onChange: () => {},
|
|
10
|
+
mentions: [{ id: "1", label: "Asha" }],
|
|
11
|
+
onAiCommand: async () => null,
|
|
12
|
+
});
|
|
13
|
+
const names = extensions.map((ext) => ext.name);
|
|
14
|
+
expect(names).toContain("mention");
|
|
15
|
+
expect(names).toContain("aiAction");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("includes extended formatting extensions by default", () => {
|
|
19
|
+
const extensions = buildSpatikaExtensions({
|
|
20
|
+
value: "",
|
|
21
|
+
onChange: () => {},
|
|
22
|
+
});
|
|
23
|
+
const names = extensions.map((ext) => ext.name);
|
|
24
|
+
expect(names).toContain("taskList");
|
|
25
|
+
expect(names).toContain("taskItem");
|
|
26
|
+
expect(names).toContain("highlight");
|
|
27
|
+
expect(names).toContain("textStyle");
|
|
28
|
+
expect(names).toContain("subscript");
|
|
29
|
+
expect(names).toContain("superscript");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("useSpatikaEditor", () => {
|
|
34
|
+
it("returns a ready editor and emits onChange updates", async () => {
|
|
35
|
+
const onChange = vi.fn();
|
|
36
|
+
const { result } = renderHook(() =>
|
|
37
|
+
useSpatikaEditor({
|
|
38
|
+
value: "<p>Hello</p>",
|
|
39
|
+
onChange,
|
|
40
|
+
placeholder: "Write here…",
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await waitFor(() => {
|
|
45
|
+
expect(result.current.isReady).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
result.current.editor?.commands.setContent("<p>Updated</p>");
|
|
49
|
+
await waitFor(() => {
|
|
50
|
+
expect(onChange).toHaveBeenCalled();
|
|
51
|
+
expect(onChange.mock.calls.at(-1)?.[0]).toContain("Updated");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
result.current.editor?.destroy();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import type { Extensions } from "@tiptap/core";
|
|
3
|
+
import Color from "@tiptap/extension-color";
|
|
4
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
5
|
+
import Image from "@tiptap/extension-image";
|
|
6
|
+
import Link from "@tiptap/extension-link";
|
|
7
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
8
|
+
import Subscript from "@tiptap/extension-subscript";
|
|
9
|
+
import Superscript from "@tiptap/extension-superscript";
|
|
10
|
+
import { Table } from "@tiptap/extension-table";
|
|
11
|
+
import TableCell from "@tiptap/extension-table-cell";
|
|
12
|
+
import TableHeader from "@tiptap/extension-table-header";
|
|
13
|
+
import TableRow from "@tiptap/extension-table-row";
|
|
14
|
+
import TaskItem from "@tiptap/extension-task-item";
|
|
15
|
+
import TaskList from "@tiptap/extension-task-list";
|
|
16
|
+
import TextAlign from "@tiptap/extension-text-align";
|
|
17
|
+
import { TextStyle } from "@tiptap/extension-text-style";
|
|
18
|
+
import Underline from "@tiptap/extension-underline";
|
|
19
|
+
import { useEditor } from "@tiptap/react";
|
|
20
|
+
import { TextSelection } from "@tiptap/pm/state";
|
|
21
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
22
|
+
import { createAiActionExtension } from "../extensions/ai-action";
|
|
23
|
+
import { createMentionExtension } from "../extensions/mention";
|
|
24
|
+
import { ResizableImage } from "../extensions/resizable-image";
|
|
25
|
+
import {
|
|
26
|
+
createSlashCommandExtension,
|
|
27
|
+
defaultSlashCommands,
|
|
28
|
+
} from "../extensions/slash-command";
|
|
29
|
+
import type { PlaceholderConfig, UseSpatikaEditorOptions } from "./types";
|
|
30
|
+
import { extractImageFiles, insertImageFilesIntoEditor } from "./insert-editor-images";
|
|
31
|
+
|
|
32
|
+
function resolvePlaceholder(placeholder?: PlaceholderConfig) {
|
|
33
|
+
if (!placeholder) return "Start writing…";
|
|
34
|
+
if (typeof placeholder === "string") return placeholder;
|
|
35
|
+
return (props: { node: { type: { name: string }; attrs: { level?: number } }; pos: number }) =>
|
|
36
|
+
placeholder({
|
|
37
|
+
type: props.node.type.name,
|
|
38
|
+
level: props.node.attrs.level,
|
|
39
|
+
pos: props.pos,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildSpatikaExtensions(
|
|
44
|
+
options: UseSpatikaEditorOptions & {
|
|
45
|
+
onSlashUpdate?: Parameters<typeof createSlashCommandExtension>[1];
|
|
46
|
+
onMentionUpdate?: Parameters<typeof createMentionExtension>[1];
|
|
47
|
+
},
|
|
48
|
+
): Extensions {
|
|
49
|
+
const imageExtension = options.resizableImages ? ResizableImage : Image;
|
|
50
|
+
const slashItems = [
|
|
51
|
+
...defaultSlashCommands(),
|
|
52
|
+
...(options.slashCommands ?? []),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const extensions: Extensions = [
|
|
56
|
+
StarterKit.configure({
|
|
57
|
+
heading: { levels: [1, 2, 3] },
|
|
58
|
+
link: false,
|
|
59
|
+
underline: false,
|
|
60
|
+
...options.starterKit,
|
|
61
|
+
}),
|
|
62
|
+
TextStyle,
|
|
63
|
+
Color,
|
|
64
|
+
Highlight.configure({ multicolor: true }),
|
|
65
|
+
Subscript,
|
|
66
|
+
Superscript,
|
|
67
|
+
Underline,
|
|
68
|
+
Link.configure({
|
|
69
|
+
openOnClick: false,
|
|
70
|
+
HTMLAttributes: { class: "spk-editor-link" },
|
|
71
|
+
}),
|
|
72
|
+
Placeholder.configure({
|
|
73
|
+
placeholder: resolvePlaceholder(options.placeholder),
|
|
74
|
+
}),
|
|
75
|
+
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
76
|
+
TaskList,
|
|
77
|
+
TaskItem.configure({ nested: true }),
|
|
78
|
+
Table.configure({ resizable: true }),
|
|
79
|
+
TableRow,
|
|
80
|
+
TableHeader,
|
|
81
|
+
TableCell,
|
|
82
|
+
imageExtension.configure({ inline: false, allowBase64: true }),
|
|
83
|
+
createSlashCommandExtension(slashItems, options.onSlashUpdate),
|
|
84
|
+
...(options.onAiSelectionAction || options.onAiCommand ? [createAiActionExtension()] : []),
|
|
85
|
+
...(options.extensions ?? []),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
if (options.mentions?.length) {
|
|
89
|
+
extensions.push(...createMentionExtension(options.mentions, options.onMentionUpdate));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return extensions;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function useSpatikaEditor(options: UseSpatikaEditorOptions) {
|
|
96
|
+
const onChangeRef = useRef(options.onChange);
|
|
97
|
+
onChangeRef.current = options.onChange;
|
|
98
|
+
const slashUpdateRef = useRef(options.onSlashUpdate);
|
|
99
|
+
slashUpdateRef.current = options.onSlashUpdate;
|
|
100
|
+
const mentionUpdateRef = useRef(options.onMentionUpdate);
|
|
101
|
+
mentionUpdateRef.current = options.onMentionUpdate;
|
|
102
|
+
const imageInsertRef = useRef(options.imageInsert);
|
|
103
|
+
imageInsertRef.current = options.imageInsert;
|
|
104
|
+
const editorRef = useRef<ReturnType<typeof useEditor>>(null);
|
|
105
|
+
|
|
106
|
+
const extensions = useMemo(
|
|
107
|
+
() =>
|
|
108
|
+
buildSpatikaExtensions({
|
|
109
|
+
...options,
|
|
110
|
+
onSlashUpdate: (state) => slashUpdateRef.current?.(state),
|
|
111
|
+
onMentionUpdate: (state) => mentionUpdateRef.current?.(state),
|
|
112
|
+
}),
|
|
113
|
+
[
|
|
114
|
+
options.placeholder,
|
|
115
|
+
options.resizableImages,
|
|
116
|
+
options.slashCommands,
|
|
117
|
+
options.mentions,
|
|
118
|
+
options.extensions,
|
|
119
|
+
options.starterKit,
|
|
120
|
+
options.onAiSelectionAction,
|
|
121
|
+
options.onAiCommand,
|
|
122
|
+
],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const editor = useEditor({
|
|
126
|
+
immediatelyRender: true,
|
|
127
|
+
extensions,
|
|
128
|
+
content: options.value,
|
|
129
|
+
editable: !options.disabled,
|
|
130
|
+
onUpdate: ({ editor: ed }) => {
|
|
131
|
+
onChangeRef.current(ed.getHTML());
|
|
132
|
+
},
|
|
133
|
+
editorProps: {
|
|
134
|
+
attributes: {
|
|
135
|
+
class: "spk-editor-content ProseMirror",
|
|
136
|
+
role: "textbox",
|
|
137
|
+
"aria-multiline": "true",
|
|
138
|
+
"aria-label": "Rich text editor",
|
|
139
|
+
},
|
|
140
|
+
handleDrop(view, event) {
|
|
141
|
+
const files = extractImageFiles(event.dataTransfer);
|
|
142
|
+
if (!files.length) return false;
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
const coords = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
145
|
+
if (coords) {
|
|
146
|
+
view.dispatch(
|
|
147
|
+
view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(coords.pos))),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const ed = editorRef.current;
|
|
151
|
+
if (ed) void insertImageFilesIntoEditor(ed, files, imageInsertRef.current);
|
|
152
|
+
return true;
|
|
153
|
+
},
|
|
154
|
+
handlePaste(_view, event) {
|
|
155
|
+
const items = event.clipboardData?.items;
|
|
156
|
+
if (!items) return false;
|
|
157
|
+
const files = Array.from(items)
|
|
158
|
+
.filter((item) => item.type.startsWith("image/"))
|
|
159
|
+
.map((item) => item.getAsFile())
|
|
160
|
+
.filter((file): file is File => Boolean(file));
|
|
161
|
+
if (!files.length) return false;
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
const ed = editorRef.current;
|
|
164
|
+
if (ed) void insertImageFilesIntoEditor(ed, files, imageInsertRef.current);
|
|
165
|
+
return true;
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
editorRef.current = editor;
|
|
172
|
+
}, [editor]);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!editor) return;
|
|
176
|
+
editor.setEditable(!options.disabled);
|
|
177
|
+
}, [editor, options.disabled]);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!editor) return;
|
|
181
|
+
const current = editor.getHTML();
|
|
182
|
+
if (options.value !== current) {
|
|
183
|
+
editor.commands.setContent(options.value, { emitUpdate: false });
|
|
184
|
+
}
|
|
185
|
+
}, [editor, options.value]);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
editor,
|
|
189
|
+
isReady: Boolean(editor),
|
|
190
|
+
};
|
|
191
|
+
}
|