@cyberyozh/text-editor 0.1.8 → 0.1.10
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 +12 -2
- package/dist/editor.css.map +1 -1
- package/dist/editor.d.mts +1 -0
- package/dist/editor.d.ts +1 -0
- package/dist/editor.js +231 -131
- package/dist/editor.js.map +1 -1
- package/dist/editor.mjs +175 -75
- package/dist/editor.mjs.map +1 -1
- package/dist/render.js +79 -6
- package/dist/render.js.map +1 -1
- package/dist/render.mjs +77 -4
- package/dist/render.mjs.map +1 -1
- package/dist/styles/index.css +12 -2
- package/package.json +2 -2
- package/src/editor/controls/bubble-menu-content.tsx +4 -8
- package/src/editor/controls/callout-menu.tsx +2 -0
- package/src/editor/controls/drag-handle.tsx +6 -1
- package/src/editor/dialogs/add-link-dialog/add-link-dialog.css +12 -4
- package/src/editor/dialogs/add-link-dialog/add-link-dialog.tsx +65 -28
- package/src/editor/dialogs/image-dialog/image-dialog.tsx +1 -0
- package/src/editor/editor.tsx +6 -10
- package/src/editor/handlers/dialog-handlers.ts +33 -32
- package/src/extensions/base/compatibility.ts +52 -0
- package/src/extensions/base/figure.ts +7 -0
- package/src/extensions/base/link.ts +3 -3
- package/src/extensions/views/components/table.tsx +34 -8
- package/src/extensions/views/node-views/figure.tsx +1 -1
- package/src/styles/editor/base.css +1 -1
- package/src/styles/extensions/callout.css +1 -1
- package/src/translations/en.json +5 -1
- package/src/translations/ru.json +5 -1
- package/src/types/image-handler.ts +2 -0
- package/src/types/link.ts +9 -0
package/dist/styles/index.css
CHANGED
|
@@ -343,6 +343,16 @@
|
|
|
343
343
|
.link-dialog-button-submit:active {
|
|
344
344
|
transform: scale(0.98);
|
|
345
345
|
}
|
|
346
|
+
.link-dialog-checkbox-label {
|
|
347
|
+
display: flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
gap: 4px;
|
|
350
|
+
}
|
|
351
|
+
.link-dialog-help-text {
|
|
352
|
+
margin: 0;
|
|
353
|
+
color: var(--ui-text-secondary);
|
|
354
|
+
font-size: 14px;
|
|
355
|
+
}
|
|
346
356
|
|
|
347
357
|
/* src/editor/dialogs/add-video-dialog/add-video-dialog.css */
|
|
348
358
|
.video-dialog-title {
|
|
@@ -1141,7 +1151,7 @@
|
|
|
1141
1151
|
flex: 1;
|
|
1142
1152
|
min-width: 0;
|
|
1143
1153
|
font-size: var(--tiptap-font-size);
|
|
1144
|
-
line-height:
|
|
1154
|
+
line-height: 140%;
|
|
1145
1155
|
}
|
|
1146
1156
|
.callout-content {
|
|
1147
1157
|
color: inherit;
|
|
@@ -1568,7 +1578,7 @@
|
|
|
1568
1578
|
font-size: var(--tiptap-font-size);
|
|
1569
1579
|
font-family: var(--tiptap-font-family);
|
|
1570
1580
|
}
|
|
1571
|
-
.tiptap-content-wrapper:focus {
|
|
1581
|
+
.tiptap-content-wrapper:focus-within {
|
|
1572
1582
|
border-color: var(--tiptap-color-primary-light);
|
|
1573
1583
|
outline: none;
|
|
1574
1584
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyberyozh/text-editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Rich text editor library based on Tiptap for CyberYozh",
|
|
5
5
|
"author": "CyberYozh",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"changeset": "changeset",
|
|
41
41
|
"version": "changeset version",
|
|
42
42
|
"release": "npm run build && changeset publish",
|
|
43
|
-
"release:complete": "git add . && git commit -m \"chore: release v$(node -p \"require('./package.json').version\")\" && npm run release && git push --follow-tags"
|
|
43
|
+
"release:complete": "changeset version && git add . && git commit -m \"chore: release v$(node -p \"require('./package.json').version\")\" && npm run release && git push --follow-tags"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -31,10 +31,11 @@ import {
|
|
|
31
31
|
|
|
32
32
|
import { useDialogCommand } from "../context/dialog-command";
|
|
33
33
|
import { useTiptapTranslations } from "../context";
|
|
34
|
+
import { LinkAttrsType, LinkDialogContextProps } from "@/types/link";
|
|
34
35
|
|
|
35
36
|
interface Props {
|
|
36
37
|
editor: Editor;
|
|
37
|
-
onLinkClick?: (
|
|
38
|
+
onLinkClick?: (params: LinkDialogContextProps) => void;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export const BubbleMenuContent = ({ editor, onLinkClick }: Props) => {
|
|
@@ -101,13 +102,8 @@ export const BubbleMenuContent = ({ editor, onLinkClick }: Props) => {
|
|
|
101
102
|
editor.state.selection.$to.pos
|
|
102
103
|
);
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const link = editor.getAttributes("link");
|
|
107
|
-
linkUrl = link.href;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
onLinkClick?.(selectedText, linkUrl);
|
|
105
|
+
const linkAttrs = editor.getAttributes("link");
|
|
106
|
+
onLinkClick?.({ title: selectedText, ...(linkAttrs as LinkAttrsType) });
|
|
111
107
|
openDialogCommand("link");
|
|
112
108
|
};
|
|
113
109
|
|
|
@@ -148,6 +148,8 @@ export const CalloutMenu = ({ isOpen, onClose, onColorSelect, editor, nodePos }:
|
|
|
148
148
|
.setColor(textColor)
|
|
149
149
|
.setNodeSelection(nodePos)
|
|
150
150
|
.updateAttributes("callout", { bgColor })
|
|
151
|
+
.updateAttributes("calloutTip", { bgColor })
|
|
152
|
+
.updateAttributes("calloutAlert", { bgColor })
|
|
151
153
|
.run();
|
|
152
154
|
}
|
|
153
155
|
}
|
|
@@ -22,7 +22,10 @@ export const DragHandle = ({ editor }: Props) => {
|
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
24
|
const node = editor.view.state.doc.nodeAt(pos);
|
|
25
|
-
const isCallout =
|
|
25
|
+
const isCallout =
|
|
26
|
+
node?.type.name === "callout" ||
|
|
27
|
+
node?.type.name === "calloutAlert" ||
|
|
28
|
+
node?.type.name === "calloutTip";
|
|
26
29
|
setIsCalloutNode(isCallout);
|
|
27
30
|
|
|
28
31
|
if (prevPos !== null && prevPos !== pos) {
|
|
@@ -61,6 +64,8 @@ export const DragHandle = ({ editor }: Props) => {
|
|
|
61
64
|
.setColor(textColor)
|
|
62
65
|
.setNodeSelection(pos)
|
|
63
66
|
.updateAttributes("callout", { bgColor })
|
|
67
|
+
.updateAttributes("calloutTip", { bgColor })
|
|
68
|
+
.updateAttributes("calloutAlert", { bgColor })
|
|
64
69
|
.run();
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/* ═══════════════════════════════════════════════════════════ */
|
|
2
|
-
/* ADD LINK DIALOG */
|
|
3
|
-
/* ═══════════════════════════════════════════════════════════ */
|
|
4
|
-
|
|
5
1
|
.link-dialog-title {
|
|
6
2
|
margin: 0 0 16px 0;
|
|
7
3
|
font-weight: 600;
|
|
@@ -103,3 +99,15 @@
|
|
|
103
99
|
.link-dialog-button-submit:active {
|
|
104
100
|
transform: scale(0.98);
|
|
105
101
|
}
|
|
102
|
+
|
|
103
|
+
.link-dialog-checkbox-label {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 4px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.link-dialog-help-text {
|
|
110
|
+
margin: 0;
|
|
111
|
+
color: var(--ui-text-secondary);
|
|
112
|
+
font-size: 14px;
|
|
113
|
+
}
|
|
@@ -4,39 +4,55 @@ import { ChangeEvent, useEffect, useRef, useState } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { CommonModal } from "../../components";
|
|
6
6
|
import { useTiptapTranslations } from "../../context";
|
|
7
|
+
import { LinkDialogContextProps } from "@/types/link";
|
|
7
8
|
|
|
8
9
|
interface AddLinkDialogProps {
|
|
9
10
|
open: boolean;
|
|
10
11
|
onClose: () => void;
|
|
11
|
-
onLinkAdd: (
|
|
12
|
-
|
|
13
|
-
initialUrl?: string;
|
|
12
|
+
onLinkAdd: (params: LinkDialogContextProps) => void;
|
|
13
|
+
linkDialogContext: LinkDialogContextProps | null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export const AddLinkDialog = ({
|
|
17
17
|
open,
|
|
18
18
|
onClose,
|
|
19
19
|
onLinkAdd,
|
|
20
|
-
|
|
21
|
-
initialUrl = "",
|
|
20
|
+
linkDialogContext,
|
|
22
21
|
}: AddLinkDialogProps) => {
|
|
23
22
|
const translations = useTiptapTranslations();
|
|
24
|
-
|
|
25
|
-
const
|
|
23
|
+
|
|
24
|
+
const initialLinkTarget = linkDialogContext?.target
|
|
25
|
+
? linkDialogContext.target === "_blank"
|
|
26
|
+
: true;
|
|
27
|
+
|
|
28
|
+
const initialLinkRel = linkDialogContext?.rel
|
|
29
|
+
? linkDialogContext.rel?.includes("noopener")
|
|
30
|
+
: true;
|
|
31
|
+
|
|
32
|
+
const [title, setTitle] = useState(linkDialogContext?.title);
|
|
33
|
+
const [url, setUrl] = useState(linkDialogContext?.href);
|
|
34
|
+
const [openInNewTab, setOpenInNewTab] = useState(initialLinkTarget);
|
|
35
|
+
const [addNoopener, setAddNoopener] = useState(initialLinkRel);
|
|
26
36
|
const [error, setError] = useState<string>("");
|
|
27
37
|
const titleInputRef = useRef<HTMLInputElement>(null);
|
|
28
38
|
|
|
39
|
+
const clearDialogState = () => {
|
|
40
|
+
setTitle(linkDialogContext?.title);
|
|
41
|
+
setUrl(linkDialogContext?.href);
|
|
42
|
+
setOpenInNewTab(initialLinkTarget);
|
|
43
|
+
setAddNoopener(initialLinkRel);
|
|
44
|
+
setError("");
|
|
45
|
+
};
|
|
46
|
+
|
|
29
47
|
useEffect(() => {
|
|
30
48
|
if (open) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
setError("");
|
|
34
|
-
// Focus the first input after modal opens
|
|
49
|
+
clearDialogState();
|
|
50
|
+
|
|
35
51
|
setTimeout(() => {
|
|
36
52
|
titleInputRef.current?.focus();
|
|
37
53
|
}, 0);
|
|
38
54
|
}
|
|
39
|
-
}, [open,
|
|
55
|
+
}, [open, linkDialogContext]);
|
|
40
56
|
|
|
41
57
|
const validateUrl = (urlStr: string): boolean => {
|
|
42
58
|
if (!urlStr.trim()) {
|
|
@@ -44,17 +60,14 @@ export const AddLinkDialog = ({
|
|
|
44
60
|
return false;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
// Check if URL has a protocol (mailto:, tel:, http:, https:, etc)
|
|
48
63
|
const hasProtocol =
|
|
49
64
|
urlStr.includes("://") || urlStr.startsWith("mailto:") || urlStr.startsWith("tel:");
|
|
50
65
|
|
|
51
|
-
// Only validate hostname for web URLs (http/https)
|
|
52
66
|
if (!hasProtocol || urlStr.startsWith("http")) {
|
|
53
67
|
try {
|
|
54
68
|
const urlWithProtocol = urlStr.startsWith("http") ? urlStr : `https://${urlStr}`;
|
|
55
69
|
const url = new URL(urlWithProtocol);
|
|
56
70
|
|
|
57
|
-
// Validate hostname has proper structure (contains . or is localhost)
|
|
58
71
|
const hostname = url.hostname;
|
|
59
72
|
if (!hostname.includes(".") && hostname !== "localhost") {
|
|
60
73
|
setError(translations.link.invalidUrl);
|
|
@@ -67,34 +80,34 @@ export const AddLinkDialog = ({
|
|
|
67
80
|
return false;
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
|
-
|
|
71
|
-
// For non-web protocols (mailto, tel, etc), just validate basic format
|
|
72
83
|
return true;
|
|
73
84
|
};
|
|
74
85
|
|
|
75
86
|
const handleSubmit = () => {
|
|
76
87
|
setError("");
|
|
77
88
|
|
|
78
|
-
if (!title
|
|
89
|
+
if (!title?.trim()) {
|
|
79
90
|
setError(translations.link.titleError);
|
|
80
91
|
return;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
if (!validateUrl(url)) {
|
|
94
|
+
if (!validateUrl(url ?? "")) {
|
|
84
95
|
return;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
const hasProtocol =
|
|
99
|
+
url?.includes("://") || url?.startsWith("mailto:") || url?.startsWith("tel:");
|
|
89
100
|
const finalUrl = hasProtocol ? url : `https://${url}`;
|
|
90
|
-
|
|
101
|
+
|
|
102
|
+
const target = openInNewTab ? "_blank" : "_self";
|
|
103
|
+
const rel = addNoopener ? "noopener noreferrer nofollow" : null;
|
|
104
|
+
|
|
105
|
+
onLinkAdd({ title, href: finalUrl ?? "", target, rel });
|
|
91
106
|
handleClose();
|
|
92
107
|
};
|
|
93
108
|
|
|
94
109
|
const handleClose = () => {
|
|
95
|
-
|
|
96
|
-
setUrl(initialUrl);
|
|
97
|
-
setError("");
|
|
110
|
+
clearDialogState();
|
|
98
111
|
onClose();
|
|
99
112
|
};
|
|
100
113
|
|
|
@@ -111,7 +124,7 @@ export const AddLinkDialog = ({
|
|
|
111
124
|
handleClose={handleClose}
|
|
112
125
|
>
|
|
113
126
|
<p className="link-dialog-title">
|
|
114
|
-
{
|
|
127
|
+
{linkDialogContext?.href ? translations.link.editLink : translations.link.addLink}
|
|
115
128
|
</p>
|
|
116
129
|
<hr className="link-dialog-divider" />
|
|
117
130
|
|
|
@@ -147,12 +160,36 @@ export const AddLinkDialog = ({
|
|
|
147
160
|
type="text"
|
|
148
161
|
className="link-dialog-input"
|
|
149
162
|
placeholder={translations.link.urlPlaceholder}
|
|
150
|
-
value={url}
|
|
163
|
+
value={url ?? ""}
|
|
151
164
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setUrl(e.target.value)}
|
|
152
165
|
onKeyDown={handleKeyDown}
|
|
153
166
|
/>
|
|
154
167
|
</div>
|
|
155
168
|
|
|
169
|
+
<div className="link-dialog-form-group">
|
|
170
|
+
<label className="link-dialog-checkbox-label">
|
|
171
|
+
<input
|
|
172
|
+
type="checkbox"
|
|
173
|
+
checked={openInNewTab}
|
|
174
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => setOpenInNewTab(e.target.checked)}
|
|
175
|
+
/>
|
|
176
|
+
<span>{translations.link.openInNewTab}</span>
|
|
177
|
+
</label>
|
|
178
|
+
<p className="link-dialog-help-text">{translations.link.openInNewTabHelp}</p>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="link-dialog-form-group">
|
|
182
|
+
<label className="link-dialog-checkbox-label">
|
|
183
|
+
<input
|
|
184
|
+
type="checkbox"
|
|
185
|
+
checked={addNoopener}
|
|
186
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => setAddNoopener(e.target.checked)}
|
|
187
|
+
/>
|
|
188
|
+
<span>{translations.link.addNofollow}</span>
|
|
189
|
+
</label>
|
|
190
|
+
<p className="link-dialog-help-text">{translations.link.addNofollowHelp}</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
156
193
|
{error && <div className="link-dialog-error">{error}</div>}
|
|
157
194
|
</div>
|
|
158
195
|
|
|
@@ -169,7 +206,7 @@ export const AddLinkDialog = ({
|
|
|
169
206
|
onClick={handleSubmit}
|
|
170
207
|
type="button"
|
|
171
208
|
>
|
|
172
|
-
{
|
|
209
|
+
{linkDialogContext?.href ? translations.link.editLink : translations.link.addLink}
|
|
173
210
|
</button>
|
|
174
211
|
</div>
|
|
175
212
|
</CommonModal>
|
package/src/editor/editor.tsx
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import "../styles/styles.css";
|
|
37
37
|
import { createSuggestions } from "./commands/suggestion";
|
|
38
38
|
import { getTranslations, type SupportedLanguage } from "../utils/get-translations";
|
|
39
|
+
import { LinkDialogContextProps } from "@/types/link";
|
|
39
40
|
|
|
40
41
|
interface TiptapProps {
|
|
41
42
|
isEditable?: boolean;
|
|
@@ -60,11 +61,6 @@ interface TiptapProps {
|
|
|
60
61
|
error?: string;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
interface LinkDialogContextProps {
|
|
64
|
-
selectedText?: string;
|
|
65
|
-
url?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
64
|
interface KatexDialogContextProps {
|
|
69
65
|
latex?: string;
|
|
70
66
|
pos?: number;
|
|
@@ -137,6 +133,7 @@ function TiptapContent({
|
|
|
137
133
|
return false;
|
|
138
134
|
},
|
|
139
135
|
},
|
|
136
|
+
onCreate: ({ editor }) => handleChange?.(editor.getJSON()),
|
|
140
137
|
onBlur: ({ editor }) => handleChange?.(editor.getJSON()),
|
|
141
138
|
},
|
|
142
139
|
[lang]
|
|
@@ -168,8 +165,8 @@ function TiptapContent({
|
|
|
168
165
|
createHandleVideoAdd(editor, handleCloseDialog, savedDialogCursorPos)(url);
|
|
169
166
|
};
|
|
170
167
|
|
|
171
|
-
const handleLinkAdd = (
|
|
172
|
-
createHandleLinkAdd(editor, handleCloseDialog,
|
|
168
|
+
const handleLinkAdd = (params: any) => {
|
|
169
|
+
createHandleLinkAdd(editor, handleCloseDialog, params, savedDialogCursorPos);
|
|
173
170
|
};
|
|
174
171
|
|
|
175
172
|
const handleKatexAdd = (latex: string) => {
|
|
@@ -215,7 +212,7 @@ function TiptapContent({
|
|
|
215
212
|
>
|
|
216
213
|
<BubbleMenuContent
|
|
217
214
|
editor={editor}
|
|
218
|
-
onLinkClick={(
|
|
215
|
+
onLinkClick={(params: LinkDialogContextProps) => setLinkDialogContext(params)}
|
|
219
216
|
/>
|
|
220
217
|
</BubbleMenu>
|
|
221
218
|
)}
|
|
@@ -250,8 +247,7 @@ function TiptapContent({
|
|
|
250
247
|
open={true}
|
|
251
248
|
onClose={handleCloseDialog}
|
|
252
249
|
onLinkAdd={handleLinkAdd}
|
|
253
|
-
|
|
254
|
-
initialUrl={linkDialogContext?.url || ""}
|
|
250
|
+
linkDialogContext={linkDialogContext}
|
|
255
251
|
/>
|
|
256
252
|
)}
|
|
257
253
|
|
|
@@ -2,6 +2,7 @@ import type { Editor } from "@tiptap/react";
|
|
|
2
2
|
import { TextSelection } from "@tiptap/pm/state";
|
|
3
3
|
|
|
4
4
|
import type { FigureAttrs, TiptapImageAttrs } from "../../types";
|
|
5
|
+
import { LinkDialogContextProps } from "@/types/link";
|
|
5
6
|
|
|
6
7
|
export const createHandleImageAdd = (
|
|
7
8
|
editor: Editor | null,
|
|
@@ -42,7 +43,6 @@ export const createHandleImageEdit = (
|
|
|
42
43
|
pos?: number
|
|
43
44
|
) => {
|
|
44
45
|
return (attrs: Partial<FigureAttrs>) => {
|
|
45
|
-
console.log("👀 ~ createHandleImageEdit ~ attrs:", attrs);
|
|
46
46
|
if (!editor || pos === undefined) return;
|
|
47
47
|
|
|
48
48
|
editor.chain().focus().setNodeSelection(pos).updateFigure(attrs).run();
|
|
@@ -86,42 +86,43 @@ export const createHandleVideoAdd = (
|
|
|
86
86
|
export const createHandleLinkAdd = (
|
|
87
87
|
editor: Editor | null,
|
|
88
88
|
closeDialogCommand: () => void,
|
|
89
|
+
linkParams: LinkDialogContextProps,
|
|
89
90
|
savedPos?: number
|
|
90
91
|
) => {
|
|
91
|
-
|
|
92
|
-
if (!editor) return;
|
|
92
|
+
if (!editor) return;
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
const hasSelection = !editor.state.selection.empty;
|
|
95
|
+
const titleLength = linkParams.title?.length ?? 0;
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
97
|
+
if (hasSelection) {
|
|
98
|
+
editor
|
|
99
|
+
.chain()
|
|
100
|
+
.focus()
|
|
101
|
+
.command(({ tr, state }) => {
|
|
102
|
+
const { from, to } = state.selection;
|
|
103
|
+
tr.insertText(linkParams.title ?? "", from, to);
|
|
104
|
+
tr.addMark(from, from + titleLength, editor.schema.marks.link.create({ ...linkParams }));
|
|
105
|
+
return true;
|
|
106
|
+
})
|
|
107
|
+
.run();
|
|
108
|
+
} else {
|
|
109
|
+
const { state } = editor;
|
|
110
|
+
const insertPos = savedPos ?? state.selection.from;
|
|
111
|
+
const titleLength = linkParams.title?.length ?? 0;
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
};
|
|
113
|
+
editor
|
|
114
|
+
.chain()
|
|
115
|
+
.focus()
|
|
116
|
+
.command(({ tr }) => {
|
|
117
|
+
const linkMark = editor.schema.marks.link.create({ ...linkParams });
|
|
118
|
+
const textNode = editor.schema.text(linkParams.title ?? "", [linkMark]);
|
|
119
|
+
tr.insert(insertPos, textNode);
|
|
120
|
+
tr.setSelection(TextSelection.create(tr.doc, insertPos, insertPos + titleLength));
|
|
121
|
+
return true;
|
|
122
|
+
})
|
|
123
|
+
.run();
|
|
124
|
+
}
|
|
125
|
+
closeDialogCommand();
|
|
125
126
|
};
|
|
126
127
|
|
|
127
128
|
export const createHandleKatexAdd = (
|
|
@@ -24,6 +24,32 @@ export const BaseLegacyBookAlert = Node.create({
|
|
|
24
24
|
type: {
|
|
25
25
|
default: "error",
|
|
26
26
|
},
|
|
27
|
+
bgColor: {
|
|
28
|
+
default: null,
|
|
29
|
+
parseHTML: (element) => {
|
|
30
|
+
if (typeof element === "string") return null;
|
|
31
|
+
return element.getAttribute("data-bg-color");
|
|
32
|
+
},
|
|
33
|
+
renderHTML: (attributes) => {
|
|
34
|
+
if (!attributes.bgColor) return {};
|
|
35
|
+
return {
|
|
36
|
+
"data-bg-color": attributes.bgColor,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
emoji: {
|
|
41
|
+
default: null,
|
|
42
|
+
parseHTML: (element) => {
|
|
43
|
+
if (typeof element === "string") return null;
|
|
44
|
+
return element.getAttribute("data-emoji");
|
|
45
|
+
},
|
|
46
|
+
renderHTML: (attributes) => {
|
|
47
|
+
if (!attributes.emoji) return {};
|
|
48
|
+
return {
|
|
49
|
+
"data-emoji": attributes.emoji,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
},
|
|
27
53
|
};
|
|
28
54
|
},
|
|
29
55
|
|
|
@@ -65,6 +91,32 @@ export const BaseLegacyBookTip = Node.create({
|
|
|
65
91
|
type: {
|
|
66
92
|
default: "info",
|
|
67
93
|
},
|
|
94
|
+
bgColor: {
|
|
95
|
+
default: null,
|
|
96
|
+
parseHTML: (element) => {
|
|
97
|
+
if (typeof element === "string") return null;
|
|
98
|
+
return element.getAttribute("data-bg-color");
|
|
99
|
+
},
|
|
100
|
+
renderHTML: (attributes) => {
|
|
101
|
+
if (!attributes.bgColor) return {};
|
|
102
|
+
return {
|
|
103
|
+
"data-bg-color": attributes.bgColor,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
emoji: {
|
|
108
|
+
default: null,
|
|
109
|
+
parseHTML: (element) => {
|
|
110
|
+
if (typeof element === "string") return null;
|
|
111
|
+
return element.getAttribute("data-emoji");
|
|
112
|
+
},
|
|
113
|
+
renderHTML: (attributes) => {
|
|
114
|
+
if (!attributes.emoji) return {};
|
|
115
|
+
return {
|
|
116
|
+
"data-emoji": attributes.emoji,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
},
|
|
68
120
|
};
|
|
69
121
|
},
|
|
70
122
|
|
|
@@ -52,6 +52,13 @@ export const BaseFigure = Image.extend<FigureOptions>({
|
|
|
52
52
|
default: null,
|
|
53
53
|
parseHTML: (element) => element.querySelector("figcaption")?.textContent || null,
|
|
54
54
|
},
|
|
55
|
+
storageKey: {
|
|
56
|
+
default: null,
|
|
57
|
+
parseHTML: (element) => element.getAttribute("data-storage-key"),
|
|
58
|
+
renderHTML: (attributes) => ({
|
|
59
|
+
"data-storage-key": attributes.storageKey,
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
55
62
|
};
|
|
56
63
|
},
|
|
57
64
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Link from
|
|
1
|
+
import Link from "@tiptap/extension-link";
|
|
2
2
|
|
|
3
3
|
export const LinkExtension = Link.extend({
|
|
4
4
|
addOptions() {
|
|
@@ -7,12 +7,12 @@ export const LinkExtension = Link.extend({
|
|
|
7
7
|
openOnClick: false,
|
|
8
8
|
linkOnPaste: true,
|
|
9
9
|
autolink: true,
|
|
10
|
-
protocols: [
|
|
10
|
+
protocols: ["http", "https", "mailto", "tel"],
|
|
11
11
|
};
|
|
12
12
|
},
|
|
13
13
|
}).configure({
|
|
14
14
|
openOnClick: false,
|
|
15
15
|
linkOnPaste: true,
|
|
16
16
|
autolink: true,
|
|
17
|
-
protocols: [
|
|
17
|
+
protocols: ["http", "https", "mailto", "tel"],
|
|
18
18
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
interface TableProps {
|
|
4
4
|
children: ReactNode;
|
|
@@ -9,7 +9,10 @@ interface TableProps {
|
|
|
9
9
|
* Applies column widths from Tiptap table cells
|
|
10
10
|
*/
|
|
11
11
|
export function Table({ children }: TableProps) {
|
|
12
|
-
|
|
12
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
const [undefinedColWidth, setUndefinedColWidth] = useState<number | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
let colWidths: (number | undefined)[] = [];
|
|
13
16
|
|
|
14
17
|
if (Array.isArray(children) && children.length > 0) {
|
|
15
18
|
const firstRow = children[0];
|
|
@@ -19,24 +22,47 @@ export function Table({ children }: TableProps) {
|
|
|
19
22
|
? firstRow.props.children
|
|
20
23
|
: [firstRow.props.children];
|
|
21
24
|
|
|
22
|
-
colWidths = cells
|
|
23
|
-
.map((cell: any) => cell?.props?.node?.attrs?.colwidth?.[0])
|
|
24
|
-
.filter((width: number | undefined) => width !== undefined);
|
|
25
|
+
colWidths = cells.map((cell: any) => cell?.props?.node?.attrs?.colwidth?.[0]);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// Calculate widths based on container
|
|
30
|
+
const definedWidths = colWidths.filter((w): w is number => w !== undefined);
|
|
31
|
+
const totalDefinedWidth = definedWidths.reduce((total, width) => total + width, 0);
|
|
32
|
+
const undefinedCount = colWidths.filter((w) => w === undefined).length;
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (wrapperRef.current && undefinedCount > 0) {
|
|
36
|
+
const containerWidth = wrapperRef.current.clientWidth;
|
|
37
|
+
const remainingSpace = containerWidth - totalDefinedWidth;
|
|
38
|
+
const calculatedWidth = Math.floor(remainingSpace / undefinedCount);
|
|
39
|
+
setUndefinedColWidth(calculatedWidth > 0 ? calculatedWidth : undefined);
|
|
40
|
+
}
|
|
41
|
+
}, [totalDefinedWidth, undefinedCount]);
|
|
42
|
+
|
|
43
|
+
// Calculate total table width
|
|
28
44
|
const tableWidth =
|
|
29
|
-
|
|
45
|
+
undefinedCount > 0 && undefinedColWidth
|
|
46
|
+
? totalDefinedWidth + (undefinedColWidth * undefinedCount)
|
|
47
|
+
: totalDefinedWidth > 0
|
|
48
|
+
? totalDefinedWidth
|
|
49
|
+
: undefined;
|
|
30
50
|
|
|
31
51
|
return (
|
|
32
|
-
<div className="tableWrapper">
|
|
52
|
+
<div ref={wrapperRef} className="tableWrapper">
|
|
33
53
|
<table style={tableWidth ? { width: `${tableWidth}px` } : undefined}>
|
|
34
54
|
{colWidths.length > 0 && (
|
|
35
55
|
<colgroup>
|
|
36
56
|
{colWidths.map((width, index) => (
|
|
37
57
|
<col
|
|
38
58
|
key={index}
|
|
39
|
-
style={
|
|
59
|
+
style={
|
|
60
|
+
width
|
|
61
|
+
? { width: `${width}px` }
|
|
62
|
+
: undefinedColWidth
|
|
63
|
+
? { width: `${undefinedColWidth}px` }
|
|
64
|
+
: undefined
|
|
65
|
+
}
|
|
40
66
|
/>
|
|
41
67
|
))}
|
|
42
68
|
</colgroup>
|
|
@@ -6,7 +6,7 @@ import { FigureAttrs } from "../../../types";
|
|
|
6
6
|
import { Figure } from "../components";
|
|
7
7
|
|
|
8
8
|
export const FigureView = ({ node, getPos, editor }: NodeViewProps) => {
|
|
9
|
-
const { fileId, src, alt, title, caption, width, height } = node.attrs as FigureAttrs;
|
|
9
|
+
const { fileId, src, alt, title, caption, width, height, storageKey } = node.attrs as FigureAttrs;
|
|
10
10
|
|
|
11
11
|
const imageExtension = editor.extensionManager.extensions.find((ext) => ext.name === "image");
|
|
12
12
|
const getImageById = imageExtension?.options.getImageById;
|