@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.
Files changed (142) hide show
  1. package/README.md +43 -0
  2. package/dist/composites/AiCommandDock.d.ts +13 -0
  3. package/dist/composites/AiCommandDock.d.ts.map +1 -0
  4. package/dist/composites/AiCommandDock.js +63 -0
  5. package/dist/composites/AiCommandMenu.d.ts +16 -0
  6. package/dist/composites/AiCommandMenu.d.ts.map +1 -0
  7. package/dist/composites/AiCommandMenu.js +51 -0
  8. package/dist/composites/AiCommandPanel.d.ts +12 -0
  9. package/dist/composites/AiCommandPanel.d.ts.map +1 -0
  10. package/dist/composites/AiCommandPanel.js +12 -0
  11. package/dist/composites/AiOverlay.d.ts +7 -0
  12. package/dist/composites/AiOverlay.d.ts.map +1 -0
  13. package/dist/composites/AiOverlay.js +8 -0
  14. package/dist/composites/EditorBubbleMenu.d.ts +10 -0
  15. package/dist/composites/EditorBubbleMenu.d.ts.map +1 -0
  16. package/dist/composites/EditorBubbleMenu.js +16 -0
  17. package/dist/composites/EditorToolbar.d.ts +20 -0
  18. package/dist/composites/EditorToolbar.d.ts.map +1 -0
  19. package/dist/composites/EditorToolbar.js +45 -0
  20. package/dist/composites/ImageBubbleMenu.d.ts +7 -0
  21. package/dist/composites/ImageBubbleMenu.d.ts.map +1 -0
  22. package/dist/composites/ImageBubbleMenu.js +15 -0
  23. package/dist/composites/MentionMenu.d.ts +11 -0
  24. package/dist/composites/MentionMenu.d.ts.map +1 -0
  25. package/dist/composites/MentionMenu.js +10 -0
  26. package/dist/composites/SlashCommandMenu.d.ts +13 -0
  27. package/dist/composites/SlashCommandMenu.d.ts.map +1 -0
  28. package/dist/composites/SlashCommandMenu.js +10 -0
  29. package/dist/composites/SpatikaEditor.d.ts +14 -0
  30. package/dist/composites/SpatikaEditor.d.ts.map +1 -0
  31. package/dist/composites/SpatikaEditor.js +159 -0
  32. package/dist/composites/TableGridPicker.d.ts +9 -0
  33. package/dist/composites/TableGridPicker.d.ts.map +1 -0
  34. package/dist/composites/TableGridPicker.js +54 -0
  35. package/dist/composites/ToolbarButton.d.ts +7 -0
  36. package/dist/composites/ToolbarButton.d.ts.map +1 -0
  37. package/dist/composites/ToolbarButton.js +7 -0
  38. package/dist/composites/ToolbarFormatPanels.d.ts +9 -0
  39. package/dist/composites/ToolbarFormatPanels.d.ts.map +1 -0
  40. package/dist/composites/ToolbarFormatPanels.js +35 -0
  41. package/dist/composites/ToolbarInsertPanels.d.ts +14 -0
  42. package/dist/composites/ToolbarInsertPanels.d.ts.map +1 -0
  43. package/dist/composites/ToolbarInsertPanels.js +96 -0
  44. package/dist/composites/ToolbarPopover.d.ts +14 -0
  45. package/dist/composites/ToolbarPopover.d.ts.map +1 -0
  46. package/dist/composites/ToolbarPopover.js +48 -0
  47. package/dist/extensions/ai-action.d.ts +24 -0
  48. package/dist/extensions/ai-action.d.ts.map +1 -0
  49. package/dist/extensions/ai-action.js +53 -0
  50. package/dist/extensions/mention.d.ts +17 -0
  51. package/dist/extensions/mention.d.ts.map +1 -0
  52. package/dist/extensions/mention.js +129 -0
  53. package/dist/extensions/resizable-image-view.d.ts +18 -0
  54. package/dist/extensions/resizable-image-view.d.ts.map +1 -0
  55. package/dist/extensions/resizable-image-view.js +70 -0
  56. package/dist/extensions/resizable-image.d.ts +4 -0
  57. package/dist/extensions/resizable-image.d.ts.map +1 -0
  58. package/dist/extensions/resizable-image.js +68 -0
  59. package/dist/extensions/slash-command.d.ts +18 -0
  60. package/dist/extensions/slash-command.d.ts.map +1 -0
  61. package/dist/extensions/slash-command.js +178 -0
  62. package/dist/index.d.ts +24 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +18 -0
  65. package/dist/lib/ai-command-menu.d.ts +9 -0
  66. package/dist/lib/ai-command-menu.d.ts.map +1 -0
  67. package/dist/lib/ai-command-menu.js +43 -0
  68. package/dist/lib/cn.d.ts +4 -0
  69. package/dist/lib/cn.d.ts.map +1 -0
  70. package/dist/lib/cn.js +6 -0
  71. package/dist/lib/editor-format-colors.d.ts +10 -0
  72. package/dist/lib/editor-format-colors.d.ts.map +1 -0
  73. package/dist/lib/editor-format-colors.js +24 -0
  74. package/dist/lib/image-upload.d.ts +14 -0
  75. package/dist/lib/image-upload.d.ts.map +1 -0
  76. package/dist/lib/image-upload.js +29 -0
  77. package/dist/lib/insert-editor-images.d.ts +5 -0
  78. package/dist/lib/insert-editor-images.d.ts.map +1 -0
  79. package/dist/lib/insert-editor-images.js +32 -0
  80. package/dist/lib/types.d.ts +131 -0
  81. package/dist/lib/types.d.ts.map +1 -0
  82. package/dist/lib/types.js +12 -0
  83. package/dist/lib/use-spatika-editor.d.ts +13 -0
  84. package/dist/lib/use-spatika-editor.d.ts.map +1 -0
  85. package/dist/lib/use-spatika-editor.js +171 -0
  86. package/dist/styles.css +1056 -0
  87. package/package.json +101 -0
  88. package/src/composites/AiCommandDock.test.tsx +48 -0
  89. package/src/composites/AiCommandDock.tsx +137 -0
  90. package/src/composites/AiCommandMenu.test.tsx +67 -0
  91. package/src/composites/AiCommandMenu.tsx +120 -0
  92. package/src/composites/AiCommandPanel.test.tsx +42 -0
  93. package/src/composites/AiCommandPanel.tsx +56 -0
  94. package/src/composites/AiOverlay.test.tsx +18 -0
  95. package/src/composites/AiOverlay.tsx +18 -0
  96. package/src/composites/EditorBubbleMenu.test.tsx +49 -0
  97. package/src/composites/EditorBubbleMenu.tsx +73 -0
  98. package/src/composites/EditorToolbar.test.tsx +146 -0
  99. package/src/composites/EditorToolbar.tsx +362 -0
  100. package/src/composites/ImageBubbleMenu.test.tsx +43 -0
  101. package/src/composites/ImageBubbleMenu.tsx +51 -0
  102. package/src/composites/MentionMenu.test.tsx +25 -0
  103. package/src/composites/MentionMenu.tsx +46 -0
  104. package/src/composites/SlashCommandMenu.test.tsx +33 -0
  105. package/src/composites/SlashCommandMenu.tsx +54 -0
  106. package/src/composites/SpatikaEditor.test.tsx +167 -0
  107. package/src/composites/SpatikaEditor.tsx +302 -0
  108. package/src/composites/TableGridPicker.test.tsx +26 -0
  109. package/src/composites/TableGridPicker.tsx +138 -0
  110. package/src/composites/ToolbarButton.test.tsx +21 -0
  111. package/src/composites/ToolbarButton.tsx +24 -0
  112. package/src/composites/ToolbarFormatPanels.test.tsx +46 -0
  113. package/src/composites/ToolbarFormatPanels.tsx +78 -0
  114. package/src/composites/ToolbarInsertPanels.test.tsx +60 -0
  115. package/src/composites/ToolbarInsertPanels.tsx +200 -0
  116. package/src/composites/ToolbarPopover.test.tsx +24 -0
  117. package/src/composites/ToolbarPopover.tsx +95 -0
  118. package/src/extensions/ai-action.test.ts +26 -0
  119. package/src/extensions/ai-action.ts +73 -0
  120. package/src/extensions/mention.test.ts +31 -0
  121. package/src/extensions/mention.ts +158 -0
  122. package/src/extensions/resizable-image-view.test.ts +26 -0
  123. package/src/extensions/resizable-image-view.ts +96 -0
  124. package/src/extensions/resizable-image.test.ts +28 -0
  125. package/src/extensions/resizable-image.ts +75 -0
  126. package/src/extensions/slash-command.test.ts +45 -0
  127. package/src/extensions/slash-command.ts +197 -0
  128. package/src/index.ts +49 -0
  129. package/src/lib/ai-command-menu.test.ts +48 -0
  130. package/src/lib/ai-command-menu.ts +57 -0
  131. package/src/lib/cn.ts +7 -0
  132. package/src/lib/editor-format-colors.test.ts +15 -0
  133. package/src/lib/editor-format-colors.ts +31 -0
  134. package/src/lib/image-upload.test.ts +19 -0
  135. package/src/lib/image-upload.ts +49 -0
  136. package/src/lib/insert-editor-images.test.ts +76 -0
  137. package/src/lib/insert-editor-images.ts +44 -0
  138. package/src/lib/types.ts +149 -0
  139. package/src/lib/use-spatika-editor.test.tsx +56 -0
  140. package/src/lib/use-spatika-editor.ts +191 -0
  141. package/src/styles/editor.css +1056 -0
  142. package/src/test/setup.ts +46 -0
@@ -0,0 +1,24 @@
1
+ /** Preset text colors for the formatting toolbar. */
2
+ export const EDITOR_TEXT_COLORS = [
3
+ { id: "default", label: "Default text color", value: null },
4
+ { id: "red", label: "Red text", value: "#dc2626" },
5
+ { id: "orange", label: "Orange text", value: "#ea580c" },
6
+ { id: "amber", label: "Amber text", value: "#d97706" },
7
+ { id: "green", label: "Green text", value: "#16a34a" },
8
+ { id: "teal", label: "Teal text", value: "#0d9488" },
9
+ { id: "blue", label: "Blue text", value: "#2563eb" },
10
+ { id: "violet", label: "Violet text", value: "#7c3aed" },
11
+ { id: "pink", label: "Pink text", value: "#db2777" },
12
+ { id: "gray", label: "Gray text", value: "#64748b" },
13
+ ];
14
+ /** Preset highlight colors for the formatting toolbar. */
15
+ export const EDITOR_HIGHLIGHT_COLORS = [
16
+ { id: "none", label: "Remove highlight", value: null },
17
+ { id: "yellow", label: "Yellow highlight", value: "#fef08a" },
18
+ { id: "lime", label: "Lime highlight", value: "#d9f99d" },
19
+ { id: "cyan", label: "Cyan highlight", value: "#a5f3fc" },
20
+ { id: "blue", label: "Blue highlight", value: "#bfdbfe" },
21
+ { id: "violet", label: "Violet highlight", value: "#ddd6fe" },
22
+ { id: "pink", label: "Pink highlight", value: "#fbcfe8" },
23
+ { id: "orange", label: "Orange highlight", value: "#fed7aa" },
24
+ ];
@@ -0,0 +1,14 @@
1
+ export type ImageUploadOptions = {
2
+ maxSizeBytes?: number;
3
+ accept?: string;
4
+ };
5
+ export type ImageUploadResult = {
6
+ ok: true;
7
+ src: string;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ export declare function readImageFileAsDataUrl(file: File, options?: ImageUploadOptions): Promise<ImageUploadResult>;
13
+ export declare function insertImagesFromFiles(files: FileList | File[], upload: (file: File) => Promise<ImageUploadResult>, limit?: number): Promise<ImageUploadResult[]>;
14
+ //# sourceMappingURL=image-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-upload.d.ts","sourceRoot":"","sources":["../../src/lib/image-upload.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAIjC,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAyB5B;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,EACxB,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,iBAAiB,CAAC,EAClD,KAAK,SAAI,GACR,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAG9B"}
@@ -0,0 +1,29 @@
1
+ const DEFAULT_MAX_SIZE = 5 * 1024 * 1024;
2
+ export function readImageFileAsDataUrl(file, options = {}) {
3
+ const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE;
4
+ const accept = options.accept ?? "image/*";
5
+ if (!file.type.startsWith("image/") && !accept.includes("*")) {
6
+ return Promise.resolve({ ok: false, error: "Only image files are supported." });
7
+ }
8
+ if (file.size > maxSizeBytes) {
9
+ const limitMb = Math.round(maxSizeBytes / (1024 * 1024));
10
+ return Promise.resolve({ ok: false, error: `Maximum file size is ${limitMb}MB.` });
11
+ }
12
+ return new Promise((resolve) => {
13
+ const reader = new FileReader();
14
+ reader.onload = () => {
15
+ if (typeof reader.result === "string") {
16
+ resolve({ ok: true, src: reader.result });
17
+ }
18
+ else {
19
+ resolve({ ok: false, error: "Could not read the image file." });
20
+ }
21
+ };
22
+ reader.onerror = () => resolve({ ok: false, error: "Could not read the image file." });
23
+ reader.readAsDataURL(file);
24
+ });
25
+ }
26
+ export function insertImagesFromFiles(files, upload, limit = 3) {
27
+ const list = Array.from(files).slice(0, limit);
28
+ return Promise.all(list.map((file) => upload(file)));
29
+ }
@@ -0,0 +1,5 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ import type { ImageInsertConfig } from "./types";
3
+ export declare function insertImageFilesIntoEditor(editor: Editor, files: FileList | File[], config?: ImageInsertConfig): Promise<boolean>;
4
+ export declare function extractImageFiles(dataTransfer: DataTransfer | null | undefined): File[];
5
+ //# sourceMappingURL=insert-editor-images.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insert-editor-images.d.ts","sourceRoot":"","sources":["../../src/lib/insert-editor-images.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAkBjD,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,EACxB,MAAM,CAAC,EAAE,iBAAiB,oBAgB3B;AAED,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,UAG9E"}
@@ -0,0 +1,32 @@
1
+ import { insertImagesFromFiles, readImageFileAsDataUrl } from "./image-upload";
2
+ async function uploadImageFile(file, config) {
3
+ if (config?.onUpload) {
4
+ try {
5
+ const src = await config.onUpload(file);
6
+ return src ? { ok: true, src } : { ok: false, error: "Upload failed." };
7
+ }
8
+ catch {
9
+ return { ok: false, error: "Upload failed." };
10
+ }
11
+ }
12
+ return readImageFileAsDataUrl(file, {
13
+ maxSizeBytes: config?.maxSizeBytes,
14
+ accept: config?.accept,
15
+ });
16
+ }
17
+ export async function insertImageFilesIntoEditor(editor, files, config) {
18
+ const maxFiles = config?.maxFiles ?? 3;
19
+ const results = await insertImagesFromFiles(files, (file) => uploadImageFile(file, config), maxFiles);
20
+ const sources = results.flatMap((result) => (result.ok ? [result.src] : []));
21
+ if (!sources.length)
22
+ return false;
23
+ sources.forEach((src) => {
24
+ editor.chain().focus().setImage({ src }).run();
25
+ });
26
+ return true;
27
+ }
28
+ export function extractImageFiles(dataTransfer) {
29
+ if (!dataTransfer?.files?.length)
30
+ return [];
31
+ return Array.from(dataTransfer.files).filter((file) => file.type.startsWith("image/"));
32
+ }
@@ -0,0 +1,131 @@
1
+ import type { Editor, Extensions } from "@tiptap/core";
2
+ import type { StarterKitOptions } from "@tiptap/starter-kit";
3
+ import type { ReactNode } from "react";
4
+ export type SpatikaEditorToolbarConfig = {
5
+ /** Show text style group (paragraph, headings). Default true. */
6
+ textStyle?: boolean;
7
+ /** Show inline formatting (bold, italic, underline, strike, code, sub/sup, colors, blockquote). Default true. */
8
+ inlineFormat?: boolean;
9
+ /** Show list controls (bullet, numbered, task). Default true. */
10
+ lists?: boolean;
11
+ /** Show text alignment (left, center, right, justify). Default true. */
12
+ alignment?: boolean;
13
+ /** Show insert controls (link, divider, table, image). Default true. */
14
+ insert?: boolean;
15
+ /** Show AI actions. Default true. */
16
+ ai?: boolean;
17
+ /** Show undo/redo controls. Default true. */
18
+ history?: boolean;
19
+ /** Toolbar layout. Default "ribbon". */
20
+ layout?: "ribbon" | "compact";
21
+ };
22
+ export type ImageInsertConfig = {
23
+ /** Maximum files per drop or picker action. Default 3. */
24
+ maxFiles?: number;
25
+ /** Maximum bytes per file. Default 5MB. */
26
+ maxSizeBytes?: number;
27
+ /** Accepted MIME types for uploads. Default `image/*`. */
28
+ accept?: string;
29
+ /** Host-owned upload handler. Defaults to inline base64 data URLs. */
30
+ onUpload?: (file: File) => Promise<string>;
31
+ };
32
+ export type SlashCommandItem = {
33
+ id: string;
34
+ title: string;
35
+ description?: string;
36
+ icon?: ReactNode;
37
+ keywords?: string[];
38
+ command: (editor: Editor) => void;
39
+ };
40
+ export type MentionContact = {
41
+ id: string;
42
+ label: string;
43
+ avatarUrl?: string;
44
+ subtitle?: string;
45
+ };
46
+ export type AiActionDefinition = {
47
+ id: string;
48
+ label: string;
49
+ description?: string;
50
+ icon?: ReactNode;
51
+ /** When true the action stays disabled until the user selects text. */
52
+ requiresSelection?: boolean;
53
+ };
54
+ export type AiCommandPromptConfig = {
55
+ enabled?: boolean;
56
+ placeholder?: string;
57
+ submitLabel?: string;
58
+ };
59
+ export type AiCommandMenuConfig = {
60
+ /** Panel heading. Default "AI commands". */
61
+ title?: string;
62
+ /** Preset actions shown in the menu. Falls back to `aiActions` then defaults. */
63
+ actions?: AiActionDefinition[];
64
+ /** Free-form prompt field. Default enabled. Pass `false` to hide it. */
65
+ prompt?: boolean | AiCommandPromptConfig;
66
+ /** Mirror preset actions in the selection bubble menu. Default false. */
67
+ bubbleActions?: boolean;
68
+ /** Where the AI command UI renders. Default `dock` (Tiptap-style prompt bar). */
69
+ placement?: "toolbar" | "dock";
70
+ };
71
+ /** Sent to `onAiCommand` when the user runs a preset action or submits a prompt. */
72
+ export type AiCommandContext = {
73
+ /** Preset action id, or `"prompt"` for a free-form request. */
74
+ actionId: string;
75
+ /** User text from the prompt field when `actionId` is `"prompt"`. */
76
+ prompt?: string;
77
+ selectedText: string;
78
+ documentText: string;
79
+ html: string;
80
+ selection: {
81
+ from: number;
82
+ to: number;
83
+ } | null;
84
+ };
85
+ export declare const AI_PROMPT_ACTION_ID = "prompt";
86
+ export type SpatikaEditorHandle = {
87
+ focus: () => void;
88
+ setContent: (html: string) => void;
89
+ getHTML: () => string;
90
+ getJSON: () => unknown;
91
+ expandFullscreen: () => void;
92
+ };
93
+ export type PlaceholderConfig = string | ((node: {
94
+ type: string;
95
+ level?: number;
96
+ pos: number;
97
+ }) => string);
98
+ export type UseSpatikaEditorOptions = {
99
+ value: string;
100
+ onChange: (html: string) => void;
101
+ placeholder?: PlaceholderConfig;
102
+ disabled?: boolean;
103
+ extensions?: Extensions;
104
+ starterKit?: Partial<StarterKitOptions>;
105
+ slashCommands?: SlashCommandItem[];
106
+ mentions?: MentionContact[];
107
+ /** @deprecated Prefer `onAiCommand` for preset actions and free-form prompts. */
108
+ onAiSelectionAction?: (action: string, selectedText: string) => Promise<string | null>;
109
+ onAiCommand?: (context: AiCommandContext) => Promise<string | null>;
110
+ resizableImages?: boolean;
111
+ onSlashUpdate?: (state: import("../extensions/slash-command").SlashCommandStorage) => void;
112
+ onMentionUpdate?: (state: import("../extensions/mention").MentionStorage) => void;
113
+ imageInsert?: ImageInsertConfig;
114
+ };
115
+ export type SpatikaEditorProps = UseSpatikaEditorOptions & {
116
+ className?: string;
117
+ heightClassName?: string;
118
+ toolbar?: SpatikaEditorToolbarConfig;
119
+ toolbarActions?: ReactNode;
120
+ isAiProcessing?: boolean;
121
+ /** Preset AI actions for legacy toolbar/bubble buttons when `aiCommandMenu` is false. */
122
+ aiActions?: AiActionDefinition[];
123
+ /** Tiptap-style AI command menu. Default on when an AI handler is provided. Pass `false` for legacy buttons. */
124
+ aiCommandMenu?: AiCommandMenuConfig | false;
125
+ imageInsert?: ImageInsertConfig;
126
+ onInsertImage?: () => void;
127
+ mobileFullscreen?: boolean;
128
+ };
129
+ export declare const DEFAULT_TOOLBAR: Required<SpatikaEditorToolbarConfig>;
130
+ export declare const DEFAULT_SLASH_COMMANDS: SlashCommandItem[];
131
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export const AI_PROMPT_ACTION_ID = "prompt";
2
+ export const DEFAULT_TOOLBAR = {
3
+ textStyle: true,
4
+ inlineFormat: true,
5
+ lists: true,
6
+ alignment: true,
7
+ insert: true,
8
+ ai: true,
9
+ history: true,
10
+ layout: "ribbon",
11
+ };
12
+ export const DEFAULT_SLASH_COMMANDS = [];
@@ -0,0 +1,13 @@
1
+ import type { Extensions } from "@tiptap/core";
2
+ import { createMentionExtension } from "../extensions/mention";
3
+ import { createSlashCommandExtension } from "../extensions/slash-command";
4
+ import type { UseSpatikaEditorOptions } from "./types";
5
+ export declare function buildSpatikaExtensions(options: UseSpatikaEditorOptions & {
6
+ onSlashUpdate?: Parameters<typeof createSlashCommandExtension>[1];
7
+ onMentionUpdate?: Parameters<typeof createMentionExtension>[1];
8
+ }): Extensions;
9
+ export declare function useSpatikaEditor(options: UseSpatikaEditorOptions): {
10
+ editor: import("@tiptap/core").Editor;
11
+ isReady: boolean;
12
+ };
13
+ //# sourceMappingURL=use-spatika-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-spatika-editor.d.ts","sourceRoot":"","sources":["../../src/lib/use-spatika-editor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAqB/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EACL,2BAA2B,EAE5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAqB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAc1E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAAG;IACjC,aAAa,CAAC,EAAE,UAAU,CAAC,OAAO,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC;IAClE,eAAe,CAAC,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;CAChE,GACA,UAAU,CA6CZ;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB;;;EAgGhE"}
@@ -0,0 +1,171 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import Color from "@tiptap/extension-color";
3
+ import Highlight from "@tiptap/extension-highlight";
4
+ import Image from "@tiptap/extension-image";
5
+ import Link from "@tiptap/extension-link";
6
+ import Placeholder from "@tiptap/extension-placeholder";
7
+ import Subscript from "@tiptap/extension-subscript";
8
+ import Superscript from "@tiptap/extension-superscript";
9
+ import { Table } from "@tiptap/extension-table";
10
+ import TableCell from "@tiptap/extension-table-cell";
11
+ import TableHeader from "@tiptap/extension-table-header";
12
+ import TableRow from "@tiptap/extension-table-row";
13
+ import TaskItem from "@tiptap/extension-task-item";
14
+ import TaskList from "@tiptap/extension-task-list";
15
+ import TextAlign from "@tiptap/extension-text-align";
16
+ import { TextStyle } from "@tiptap/extension-text-style";
17
+ import Underline from "@tiptap/extension-underline";
18
+ import { useEditor } from "@tiptap/react";
19
+ import { TextSelection } from "@tiptap/pm/state";
20
+ import StarterKit from "@tiptap/starter-kit";
21
+ import { createAiActionExtension } from "../extensions/ai-action";
22
+ import { createMentionExtension } from "../extensions/mention";
23
+ import { ResizableImage } from "../extensions/resizable-image";
24
+ import { createSlashCommandExtension, defaultSlashCommands, } from "../extensions/slash-command";
25
+ import { extractImageFiles, insertImageFilesIntoEditor } from "./insert-editor-images";
26
+ function resolvePlaceholder(placeholder) {
27
+ if (!placeholder)
28
+ return "Start writing…";
29
+ if (typeof placeholder === "string")
30
+ return placeholder;
31
+ return (props) => placeholder({
32
+ type: props.node.type.name,
33
+ level: props.node.attrs.level,
34
+ pos: props.pos,
35
+ });
36
+ }
37
+ export function buildSpatikaExtensions(options) {
38
+ const imageExtension = options.resizableImages ? ResizableImage : Image;
39
+ const slashItems = [
40
+ ...defaultSlashCommands(),
41
+ ...(options.slashCommands ?? []),
42
+ ];
43
+ const extensions = [
44
+ StarterKit.configure({
45
+ heading: { levels: [1, 2, 3] },
46
+ link: false,
47
+ underline: false,
48
+ ...options.starterKit,
49
+ }),
50
+ TextStyle,
51
+ Color,
52
+ Highlight.configure({ multicolor: true }),
53
+ Subscript,
54
+ Superscript,
55
+ Underline,
56
+ Link.configure({
57
+ openOnClick: false,
58
+ HTMLAttributes: { class: "spk-editor-link" },
59
+ }),
60
+ Placeholder.configure({
61
+ placeholder: resolvePlaceholder(options.placeholder),
62
+ }),
63
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
64
+ TaskList,
65
+ TaskItem.configure({ nested: true }),
66
+ Table.configure({ resizable: true }),
67
+ TableRow,
68
+ TableHeader,
69
+ TableCell,
70
+ imageExtension.configure({ inline: false, allowBase64: true }),
71
+ createSlashCommandExtension(slashItems, options.onSlashUpdate),
72
+ ...(options.onAiSelectionAction || options.onAiCommand ? [createAiActionExtension()] : []),
73
+ ...(options.extensions ?? []),
74
+ ];
75
+ if (options.mentions?.length) {
76
+ extensions.push(...createMentionExtension(options.mentions, options.onMentionUpdate));
77
+ }
78
+ return extensions;
79
+ }
80
+ export function useSpatikaEditor(options) {
81
+ const onChangeRef = useRef(options.onChange);
82
+ onChangeRef.current = options.onChange;
83
+ const slashUpdateRef = useRef(options.onSlashUpdate);
84
+ slashUpdateRef.current = options.onSlashUpdate;
85
+ const mentionUpdateRef = useRef(options.onMentionUpdate);
86
+ mentionUpdateRef.current = options.onMentionUpdate;
87
+ const imageInsertRef = useRef(options.imageInsert);
88
+ imageInsertRef.current = options.imageInsert;
89
+ const editorRef = useRef(null);
90
+ const extensions = useMemo(() => buildSpatikaExtensions({
91
+ ...options,
92
+ onSlashUpdate: (state) => slashUpdateRef.current?.(state),
93
+ onMentionUpdate: (state) => mentionUpdateRef.current?.(state),
94
+ }), [
95
+ options.placeholder,
96
+ options.resizableImages,
97
+ options.slashCommands,
98
+ options.mentions,
99
+ options.extensions,
100
+ options.starterKit,
101
+ options.onAiSelectionAction,
102
+ options.onAiCommand,
103
+ ]);
104
+ const editor = useEditor({
105
+ immediatelyRender: true,
106
+ extensions,
107
+ content: options.value,
108
+ editable: !options.disabled,
109
+ onUpdate: ({ editor: ed }) => {
110
+ onChangeRef.current(ed.getHTML());
111
+ },
112
+ editorProps: {
113
+ attributes: {
114
+ class: "spk-editor-content ProseMirror",
115
+ role: "textbox",
116
+ "aria-multiline": "true",
117
+ "aria-label": "Rich text editor",
118
+ },
119
+ handleDrop(view, event) {
120
+ const files = extractImageFiles(event.dataTransfer);
121
+ if (!files.length)
122
+ return false;
123
+ event.preventDefault();
124
+ const coords = view.posAtCoords({ left: event.clientX, top: event.clientY });
125
+ if (coords) {
126
+ view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(coords.pos))));
127
+ }
128
+ const ed = editorRef.current;
129
+ if (ed)
130
+ void insertImageFilesIntoEditor(ed, files, imageInsertRef.current);
131
+ return true;
132
+ },
133
+ handlePaste(_view, event) {
134
+ const items = event.clipboardData?.items;
135
+ if (!items)
136
+ return false;
137
+ const files = Array.from(items)
138
+ .filter((item) => item.type.startsWith("image/"))
139
+ .map((item) => item.getAsFile())
140
+ .filter((file) => Boolean(file));
141
+ if (!files.length)
142
+ return false;
143
+ event.preventDefault();
144
+ const ed = editorRef.current;
145
+ if (ed)
146
+ void insertImageFilesIntoEditor(ed, files, imageInsertRef.current);
147
+ return true;
148
+ },
149
+ },
150
+ });
151
+ useEffect(() => {
152
+ editorRef.current = editor;
153
+ }, [editor]);
154
+ useEffect(() => {
155
+ if (!editor)
156
+ return;
157
+ editor.setEditable(!options.disabled);
158
+ }, [editor, options.disabled]);
159
+ useEffect(() => {
160
+ if (!editor)
161
+ return;
162
+ const current = editor.getHTML();
163
+ if (options.value !== current) {
164
+ editor.commands.setContent(options.value, { emitUpdate: false });
165
+ }
166
+ }, [editor, options.value]);
167
+ return {
168
+ editor,
169
+ isReady: Boolean(editor),
170
+ };
171
+ }