@cyberyozh/text-editor 0.1.17 → 0.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/dist/editor.css +1 -0
- package/dist/editor.css.map +1 -1
- package/dist/editor.d.mts +9 -1
- package/dist/editor.d.ts +9 -1
- package/dist/editor.js +204 -66
- package/dist/editor.js.map +1 -1
- package/dist/editor.mjs +216 -78
- package/dist/editor.mjs.map +1 -1
- package/dist/render.js +29 -38
- package/dist/render.js.map +1 -1
- package/dist/render.mjs +34 -43
- package/dist/render.mjs.map +1 -1
- package/dist/styles/index.css +1 -0
- package/package.json +40 -39
- package/src/editor/controls/bubble-menu-content.tsx +12 -3
- package/src/editor/controls/character-count.tsx +7 -4
- package/src/editor/dialogs/image-dialog/image-dialog.css +1 -0
- package/src/editor/dialogs/image-dialog/image-dialog.tsx +9 -1
- package/src/editor/editor.tsx +52 -9
- package/src/editor/handlers/index.ts +4 -0
- package/src/editor/handlers/insertion-handlers.ts +113 -0
- package/src/editor/index.ts +1 -1
- package/src/types/image-handler.ts +5 -0
- package/src/utils/prosemirror-iterator.ts +38 -0
package/dist/styles/index.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyberyozh/text-editor",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Rich text editor library based on Tiptap for CyberYozh",
|
|
5
5
|
"author": "CyberYozh",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "rm -rf dist && tsup",
|
|
36
|
-
"dev": "tsup --watch",
|
|
36
|
+
"dev": "cross-env NODE_ENV=development tsup --watch",
|
|
37
37
|
"dev:link": "npm run build && npm link && npm run dev",
|
|
38
38
|
"type-check": "tsc --noEmit",
|
|
39
39
|
"lint": "eslint src --ext .ts,.tsx",
|
|
@@ -47,47 +47,48 @@
|
|
|
47
47
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@floating-ui/dom": "
|
|
51
|
-
"@tiptap/core": "
|
|
52
|
-
"@tiptap/extension-blockquote": "
|
|
53
|
-
"@tiptap/extension-bubble-menu": "
|
|
54
|
-
"@tiptap/extension-code": "
|
|
55
|
-
"@tiptap/extension-code-block-lowlight": "
|
|
56
|
-
"@tiptap/extension-collaboration": "
|
|
57
|
-
"@tiptap/extension-details": "
|
|
58
|
-
"@tiptap/extension-drag-handle": "
|
|
59
|
-
"@tiptap/extension-drag-handle-react": "
|
|
60
|
-
"@tiptap/extension-floating-menu": "
|
|
61
|
-
"@tiptap/extension-hard-break": "
|
|
62
|
-
"@tiptap/extension-highlight": "
|
|
63
|
-
"@tiptap/extension-horizontal-rule": "
|
|
64
|
-
"@tiptap/extension-image": "
|
|
65
|
-
"@tiptap/extension-link": "
|
|
66
|
-
"@tiptap/extension-list": "
|
|
67
|
-
"@tiptap/extension-mathematics": "
|
|
68
|
-
"@tiptap/extension-node-range": "
|
|
69
|
-
"@tiptap/extension-subscript": "
|
|
70
|
-
"@tiptap/extension-superscript": "
|
|
71
|
-
"@tiptap/extension-table": "
|
|
72
|
-
"@tiptap/extension-text-align": "
|
|
73
|
-
"@tiptap/extensions": "
|
|
74
|
-
"@tiptap/html": "
|
|
75
|
-
"@tiptap/pm": "
|
|
76
|
-
"@tiptap/react": "
|
|
77
|
-
"@tiptap/starter-kit": "
|
|
78
|
-
"@tiptap/static-renderer": "
|
|
79
|
-
"@tiptap/suggestion": "
|
|
80
|
-
"@tiptap/y-tiptap": "
|
|
81
|
-
"emoji-picker-react": "
|
|
82
|
-
"highlight.js": "
|
|
83
|
-
"katex": "
|
|
84
|
-
"lowlight": "
|
|
85
|
-
"y-protocols": "
|
|
86
|
-
"yjs": "
|
|
50
|
+
"@floating-ui/dom": "1.7.6",
|
|
51
|
+
"@tiptap/core": "3.23.6",
|
|
52
|
+
"@tiptap/extension-blockquote": "3.23.6",
|
|
53
|
+
"@tiptap/extension-bubble-menu": "3.23.6",
|
|
54
|
+
"@tiptap/extension-code": "3.23.6",
|
|
55
|
+
"@tiptap/extension-code-block-lowlight": "3.23.6",
|
|
56
|
+
"@tiptap/extension-collaboration": "3.23.6",
|
|
57
|
+
"@tiptap/extension-details": "3.23.6",
|
|
58
|
+
"@tiptap/extension-drag-handle": "3.23.6",
|
|
59
|
+
"@tiptap/extension-drag-handle-react": "3.23.6",
|
|
60
|
+
"@tiptap/extension-floating-menu": "3.23.6",
|
|
61
|
+
"@tiptap/extension-hard-break": "3.23.6",
|
|
62
|
+
"@tiptap/extension-highlight": "3.23.6",
|
|
63
|
+
"@tiptap/extension-horizontal-rule": "3.23.6",
|
|
64
|
+
"@tiptap/extension-image": "3.23.6",
|
|
65
|
+
"@tiptap/extension-link": "3.23.6",
|
|
66
|
+
"@tiptap/extension-list": "3.23.6",
|
|
67
|
+
"@tiptap/extension-mathematics": "3.23.6",
|
|
68
|
+
"@tiptap/extension-node-range": "3.23.6",
|
|
69
|
+
"@tiptap/extension-subscript": "3.23.6",
|
|
70
|
+
"@tiptap/extension-superscript": "3.23.6",
|
|
71
|
+
"@tiptap/extension-table": "3.23.6",
|
|
72
|
+
"@tiptap/extension-text-align": "3.23.6",
|
|
73
|
+
"@tiptap/extensions": "3.23.6",
|
|
74
|
+
"@tiptap/html": "3.23.6",
|
|
75
|
+
"@tiptap/pm": "3.23.6",
|
|
76
|
+
"@tiptap/react": "3.23.6",
|
|
77
|
+
"@tiptap/starter-kit": "3.23.6",
|
|
78
|
+
"@tiptap/static-renderer": "3.23.6",
|
|
79
|
+
"@tiptap/suggestion": "3.23.6",
|
|
80
|
+
"@tiptap/y-tiptap": "3.0.3",
|
|
81
|
+
"emoji-picker-react": "4.19.1",
|
|
82
|
+
"highlight.js": "11.11.1",
|
|
83
|
+
"katex": "0.16.35",
|
|
84
|
+
"lowlight": "3.3.0",
|
|
85
|
+
"y-protocols": "1.0.7",
|
|
86
|
+
"yjs": "13.6.30"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@changesets/cli": "^2.30.0",
|
|
90
90
|
"@types/node": "^25.3.3",
|
|
91
|
+
"cross-env": "^10.1.0",
|
|
91
92
|
"eslint": "^10.0.2",
|
|
92
93
|
"tsup": "^8.5.1",
|
|
93
94
|
"typescript": "^5.9.3"
|
|
@@ -69,10 +69,19 @@ export const BubbleMenuContent = ({ editor, onLinkClick }: Props) => {
|
|
|
69
69
|
} = useEditorState({
|
|
70
70
|
editor,
|
|
71
71
|
selector: (ctx) => {
|
|
72
|
+
let canAvalable = false;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
ctx.editor.can().undo();
|
|
76
|
+
canAvalable = true;
|
|
77
|
+
} catch {
|
|
78
|
+
// When `lang` changes, the editor reloads and there is a brief period where parts of its internal state are inaccessible, causing certain methods to throw exceptions
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
return {
|
|
73
|
-
canUndo: ctx.editor.can().undo(),
|
|
74
|
-
canRedo: ctx.editor.can().redo(),
|
|
75
|
-
canClearMarks: ctx.editor.can().unsetAllMarks(),
|
|
82
|
+
canUndo: canAvalable ? ctx.editor.can().undo() : false,
|
|
83
|
+
canRedo: canAvalable ? ctx.editor.can().redo() : false,
|
|
84
|
+
canClearMarks: canAvalable ? ctx.editor.can().unsetAllMarks() : false,
|
|
76
85
|
isHeading2: ctx.editor.isActive("heading", { level: 2 }),
|
|
77
86
|
isHeading3: ctx.editor.isActive("heading", { level: 3 }),
|
|
78
87
|
isHeading4: ctx.editor.isActive("heading", { level: 4 }),
|
|
@@ -11,10 +11,13 @@ export const CharacterCount = ({ editor }: Props) => {
|
|
|
11
11
|
const translations = useTiptapTranslations();
|
|
12
12
|
const { charactersCount, wordsCount } = useEditorState({
|
|
13
13
|
editor,
|
|
14
|
-
selector: (context) =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
selector: (context) => {
|
|
15
|
+
return {
|
|
16
|
+
charactersCount:
|
|
17
|
+
context.editor.storage.characterCount?.characters() || 0,
|
|
18
|
+
wordsCount: context.editor.storage.characterCount?.words() || 0,
|
|
19
|
+
};
|
|
20
|
+
},
|
|
18
21
|
});
|
|
19
22
|
|
|
20
23
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useRef, useState } from "react";
|
|
3
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import { TiptapX } from "../../../icons";
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ import { useTiptapTranslations } from "../../context";
|
|
|
12
12
|
interface ImageDialogProps {
|
|
13
13
|
open: boolean;
|
|
14
14
|
onClose: () => void;
|
|
15
|
+
defaultFile?: File | null;
|
|
15
16
|
onImageAdd?: (attrs: TiptapImageAttrs) => void;
|
|
16
17
|
onImageEdit?: (attrs: Partial<FigureAttrs>) => void;
|
|
17
18
|
uploadHandler?: ImageUploadHandler;
|
|
@@ -33,6 +34,7 @@ interface ImageFormState {
|
|
|
33
34
|
export const ImageDialog = ({
|
|
34
35
|
open,
|
|
35
36
|
onClose,
|
|
37
|
+
defaultFile,
|
|
36
38
|
onImageAdd,
|
|
37
39
|
onImageEdit,
|
|
38
40
|
uploadHandler,
|
|
@@ -131,6 +133,12 @@ export const ImageDialog = ({
|
|
|
131
133
|
};
|
|
132
134
|
};
|
|
133
135
|
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (defaultFile) {
|
|
138
|
+
handleSelectFile(defaultFile)
|
|
139
|
+
}
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
134
142
|
const handleAreaClick = () => {
|
|
135
143
|
inputRef.current?.click();
|
|
136
144
|
};
|
package/src/editor/editor.tsx
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
createHandleKatexAdd,
|
|
32
32
|
createHandleLinkAdd,
|
|
33
33
|
createHandleVideoAdd,
|
|
34
|
+
useInsertionHandlers,
|
|
34
35
|
} from "./handlers";
|
|
35
36
|
|
|
36
37
|
import "../styles/styles.css";
|
|
@@ -38,7 +39,7 @@ import { createSuggestions } from "./commands/suggestion";
|
|
|
38
39
|
import { getTranslations, type SupportedLanguage } from "../utils/get-translations";
|
|
39
40
|
import { LinkDialogContextProps } from "@/types/link";
|
|
40
41
|
|
|
41
|
-
interface TiptapProps {
|
|
42
|
+
export interface TiptapProps {
|
|
42
43
|
isEditable?: boolean;
|
|
43
44
|
/**
|
|
44
45
|
* When true, makes the editor completely disabled (non-editable with disabled styling)
|
|
@@ -61,7 +62,7 @@ interface TiptapProps {
|
|
|
61
62
|
error?: string;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
interface KatexDialogContextProps {
|
|
65
|
+
export interface KatexDialogContextProps {
|
|
65
66
|
latex?: string;
|
|
66
67
|
pos?: number;
|
|
67
68
|
}
|
|
@@ -87,6 +88,9 @@ function TiptapContent({
|
|
|
87
88
|
const { activeDialog, closeDialogCommand, openDialogCommand } = useDialogCommand();
|
|
88
89
|
const translations = useTiptapTranslations();
|
|
89
90
|
|
|
91
|
+
const [isDisabledInternal, setIsDisabledInternal] = useState(isDisabled);
|
|
92
|
+
const disabled = isDisabled || isDisabledInternal;
|
|
93
|
+
|
|
90
94
|
let safeContent: JSONContent;
|
|
91
95
|
|
|
92
96
|
try {
|
|
@@ -95,13 +99,20 @@ function TiptapContent({
|
|
|
95
99
|
safeContent = { type: "doc", content: [{ type: "paragraph" }] };
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
const {
|
|
103
|
+
insertedImage,
|
|
104
|
+
onPaste,
|
|
105
|
+
onDrop,
|
|
106
|
+
onMarkupInsertion,
|
|
107
|
+
} = useInsertionHandlers();
|
|
108
|
+
|
|
98
109
|
const editor = useEditor(
|
|
99
110
|
{
|
|
100
|
-
editable: isEditable && !
|
|
111
|
+
editable: isEditable && !disabled,
|
|
101
112
|
extensions: getEditorExtensions({
|
|
102
113
|
translations,
|
|
103
114
|
suggestion: createSuggestions(translations, !!imageHandlers),
|
|
104
|
-
isEditable: isEditable && !
|
|
115
|
+
isEditable: isEditable && !disabled,
|
|
105
116
|
getImageById: imageHandlers?.getImage,
|
|
106
117
|
onKatexClick: (latex, pos) => {
|
|
107
118
|
setKatexDialogContext({ latex, pos });
|
|
@@ -115,7 +126,38 @@ function TiptapContent({
|
|
|
115
126
|
content: safeContent,
|
|
116
127
|
immediatelyRender: false,
|
|
117
128
|
editorProps: {
|
|
118
|
-
|
|
129
|
+
handleDOMEvents: {
|
|
130
|
+
paste: onPaste,
|
|
131
|
+
drop: onDrop,
|
|
132
|
+
},
|
|
133
|
+
handleDrop: (view, event, slice) => {
|
|
134
|
+
// Ignoring events from dragging within the editor itself
|
|
135
|
+
if (view.dragging) return false;
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
const toReturn = onMarkupInsertion(
|
|
139
|
+
view,
|
|
140
|
+
event,
|
|
141
|
+
slice,
|
|
142
|
+
setIsDisabledInternal,
|
|
143
|
+
imageHandlers?.uploadMultipleByURL
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (typeof toReturn === 'boolean') return toReturn;
|
|
147
|
+
|
|
148
|
+
return false;
|
|
149
|
+
},
|
|
150
|
+
handlePaste: (view, event, slice) => {
|
|
151
|
+
const toReturn = onMarkupInsertion(
|
|
152
|
+
view,
|
|
153
|
+
event,
|
|
154
|
+
slice,
|
|
155
|
+
setIsDisabledInternal,
|
|
156
|
+
imageHandlers?.uploadMultipleByURL
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (typeof toReturn === 'boolean') return toReturn;
|
|
160
|
+
|
|
119
161
|
try {
|
|
120
162
|
const text = event.clipboardData?.getData("text/plain");
|
|
121
163
|
if (!text) return false;
|
|
@@ -203,8 +245,8 @@ function TiptapContent({
|
|
|
203
245
|
return (
|
|
204
246
|
<>
|
|
205
247
|
{<DragHandle editor={editor} />}
|
|
206
|
-
{isEditable && !
|
|
207
|
-
{isEditable && !
|
|
248
|
+
{isEditable && !disabled && <TableMenu editor={editor} />}
|
|
249
|
+
{isEditable && !disabled && (
|
|
208
250
|
<BubbleMenu
|
|
209
251
|
shouldShow={shouldBubbleMenuShow}
|
|
210
252
|
editor={editor}
|
|
@@ -219,11 +261,11 @@ function TiptapContent({
|
|
|
219
261
|
|
|
220
262
|
<div
|
|
221
263
|
className={`tiptap-content-wrapper ${!!error ? "has-error" : ""} ${
|
|
222
|
-
|
|
264
|
+
disabled ? "is-disabled" : ""
|
|
223
265
|
}`}
|
|
224
266
|
>
|
|
225
267
|
<EditorContent editor={editor} />
|
|
226
|
-
{isEditable && !
|
|
268
|
+
{isEditable && !disabled && <CharacterCountComponent editor={editor} />}
|
|
227
269
|
</div>
|
|
228
270
|
{!!error && <p className="tiptap-error-message">{error}</p>}
|
|
229
271
|
|
|
@@ -233,6 +275,7 @@ function TiptapContent({
|
|
|
233
275
|
<ImageDialog
|
|
234
276
|
open={true}
|
|
235
277
|
onClose={handleCloseDialog}
|
|
278
|
+
defaultFile={insertedImage}
|
|
236
279
|
onImageAdd={handleImageAdd}
|
|
237
280
|
onImageEdit={handleImageEdit}
|
|
238
281
|
uploadHandler={imageHandlers?.upload}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
|
2
|
+
import { useDialogCommand } from "../context";
|
|
3
|
+
import { EditorView } from "@tiptap/pm/view";
|
|
4
|
+
import { Node, Slice } from "@tiptap/pm/model";
|
|
5
|
+
import { ImageUploadMultipleByURLHandler } from "@/types";
|
|
6
|
+
import { prosemirrorIterator } from "@/utils/prosemirror-iterator";
|
|
7
|
+
|
|
8
|
+
export function useInsertionHandlers() {
|
|
9
|
+
const [insertedImage, setInsertedImage] = useState<File | null>(null);
|
|
10
|
+
|
|
11
|
+
const { activeDialog, openDialogCommand } = useDialogCommand();
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (insertedImage && !activeDialog) {
|
|
15
|
+
setInsertedImage(null);
|
|
16
|
+
}
|
|
17
|
+
}, [insertedImage, activeDialog]);
|
|
18
|
+
|
|
19
|
+
function genericInsertionHandler(file: File | undefined, event: Event) {
|
|
20
|
+
if (!file || !file.type.startsWith("image/")) return;
|
|
21
|
+
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
event.stopPropagation();
|
|
24
|
+
|
|
25
|
+
setInsertedImage(file);
|
|
26
|
+
openDialogCommand("image");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function onPaste(_: unknown, event: ClipboardEvent) {
|
|
30
|
+
const file = event.clipboardData?.files?.[0];
|
|
31
|
+
genericInsertionHandler(file, event);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function onDrop(_: unknown, event: DragEvent) {
|
|
35
|
+
const file = event.dataTransfer?.files?.[0];
|
|
36
|
+
genericInsertionHandler(file, event);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function onMarkupInsertion(
|
|
40
|
+
view: EditorView,
|
|
41
|
+
event: ClipboardEvent | DragEvent,
|
|
42
|
+
slice: Slice,
|
|
43
|
+
setDisabled: Dispatch<SetStateAction<boolean>>,
|
|
44
|
+
uploadMultipleByURL?: ImageUploadMultipleByURLHandler
|
|
45
|
+
): boolean | void {
|
|
46
|
+
if (!uploadMultipleByURL) return;
|
|
47
|
+
|
|
48
|
+
let hasMarkupInsertion = false;
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
"clipboardData" in event &&
|
|
52
|
+
Array.from(event.clipboardData?.items ?? []).some(
|
|
53
|
+
(item) => item.type === "text/html"
|
|
54
|
+
)
|
|
55
|
+
) {
|
|
56
|
+
hasMarkupInsertion = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
"dataTransfer" in event &&
|
|
61
|
+
Array.from(event.dataTransfer?.items ?? []).some(
|
|
62
|
+
(item) => item.type === "text/html"
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
hasMarkupInsertion = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (hasMarkupInsertion) {
|
|
69
|
+
const imagesToNodes: [string, Node][] = [];
|
|
70
|
+
|
|
71
|
+
setDisabled(true);
|
|
72
|
+
|
|
73
|
+
prosemirrorIterator(slice, (node) => {
|
|
74
|
+
if (node.type.name === "image" && !node.attrs.fileId) {
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(node.attrs.src).toString();
|
|
77
|
+
imagesToNodes.push([url, node]);
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
uploadMultipleByURL(imagesToNodes.map(([url]) => url))
|
|
83
|
+
.then((results) => {
|
|
84
|
+
results.forEach((result, index) => {
|
|
85
|
+
const node = imagesToNodes[index][1];
|
|
86
|
+
|
|
87
|
+
// Ignoring `readonly` should be safe in this place because we are inserting slice to the Prosemirror tree only after manipilation.
|
|
88
|
+
|
|
89
|
+
// @ts-expect-error ignore `readonly`
|
|
90
|
+
node.attrs.src = null;
|
|
91
|
+
// @ts-expect-error ignore `readonly`
|
|
92
|
+
node.attrs.fileId = result?.fileId;
|
|
93
|
+
// @ts-expect-error ignore `readonly`
|
|
94
|
+
node.attrs.storageKey = result?.storageKey;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
view.dispatch(view.state.tr.replaceSelection(slice));
|
|
98
|
+
})
|
|
99
|
+
.finally(() => {
|
|
100
|
+
setDisabled(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
insertedImage,
|
|
109
|
+
onPaste,
|
|
110
|
+
onDrop,
|
|
111
|
+
onMarkupInsertion,
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/editor/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Tiptap } from "./editor";
|
|
1
|
+
export { Tiptap, type TiptapProps, type KatexDialogContextProps } from "./editor";
|
|
2
2
|
export type { SupportedLanguage as TiptapSupportedLanguages } from "../utils/get-translations";
|
|
3
3
|
export { type JSONContent } from "@tiptap/react";
|
|
4
4
|
export { htmlToTiptapContent } from "../utils/html-to-tiptap-content";
|
|
@@ -8,12 +8,17 @@ export interface ImageUploadHandler {
|
|
|
8
8
|
(file: File): Promise<ImageUploadResult>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export interface ImageUploadMultipleByURLHandler {
|
|
12
|
+
(files: string[]): Promise<(ImageUploadResult | null)[]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
export interface ImageGetHandler {
|
|
12
16
|
(fileId: string): string | null;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export interface ImageHandlers {
|
|
16
20
|
upload: ImageUploadHandler;
|
|
21
|
+
uploadMultipleByURL?: ImageUploadMultipleByURLHandler;
|
|
17
22
|
getImage?: ImageGetHandler;
|
|
18
23
|
}
|
|
19
24
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Fragment, Node, Slice } from "@tiptap/pm/model";
|
|
2
|
+
|
|
3
|
+
export type ProsemirrorIteratorCallback = (node: Node) => boolean | void;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Traverses a ProseMirror document tree depth-first, invoking `cb` for each node.
|
|
7
|
+
*
|
|
8
|
+
* Iteration stops early when `cb` returns `true`.
|
|
9
|
+
*/
|
|
10
|
+
export function prosemirrorIterator(
|
|
11
|
+
slice: Slice | Fragment | Node,
|
|
12
|
+
cb: ProsemirrorIteratorCallback,
|
|
13
|
+
) {
|
|
14
|
+
const stack: Node[] = [];
|
|
15
|
+
|
|
16
|
+
if (slice instanceof Slice) {
|
|
17
|
+
collectFragmentChildren(slice.content, stack);
|
|
18
|
+
} else if (slice instanceof Fragment) {
|
|
19
|
+
collectFragmentChildren(slice, stack);
|
|
20
|
+
} else {
|
|
21
|
+
stack.push(slice);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while (stack.length > 0) {
|
|
25
|
+
const node = stack.pop()!;
|
|
26
|
+
const shouldStop = cb(node);
|
|
27
|
+
if (shouldStop === true) break;
|
|
28
|
+
if (node.content.size > 0) {
|
|
29
|
+
collectFragmentChildren(node.content, stack);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function collectFragmentChildren(fragment: Fragment, stack: Node[]) {
|
|
35
|
+
for (let i = fragment.childCount - 1; i >= 0; i--) {
|
|
36
|
+
stack.push(fragment.child(i));
|
|
37
|
+
}
|
|
38
|
+
}
|