@cyberyozh/text-editor 0.3.1 → 0.4.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/dist/editor.css +37 -16
- package/dist/editor.css.map +1 -1
- package/dist/editor.d.mts +5 -1
- package/dist/editor.d.ts +5 -1
- package/dist/editor.js +367 -297
- package/dist/editor.js.map +1 -1
- package/dist/editor.mjs +286 -216
- package/dist/editor.mjs.map +1 -1
- package/dist/styles/index.css +37 -16
- package/package.json +1 -1
- package/src/editor/components/dialog-error/dialog-error.css +35 -0
- package/src/editor/components/dialog-error/dialog-error.tsx +65 -0
- package/src/editor/components/dialog-error/index.ts +1 -0
- package/src/editor/components/index.ts +1 -0
- package/src/editor/dialogs/add-link-dialog/add-link-dialog.css +0 -9
- package/src/editor/dialogs/add-link-dialog/add-link-dialog.tsx +2 -2
- package/src/editor/dialogs/add-video-dialog/add-video-dialog.css +0 -9
- package/src/editor/dialogs/add-video-dialog/add-video-dialog.tsx +2 -2
- package/src/editor/dialogs/image-dialog/image-dialog.css +4 -0
- package/src/editor/dialogs/image-dialog/image-dialog.tsx +28 -2
- package/src/editor/handlers/insertion-handlers.ts +1 -2
- package/src/editor/index.ts +1 -0
- package/src/styles/components.css +1 -0
- package/src/translations/en.json +5 -2
- package/src/translations/ru.json +5 -2
- package/src/types/common.ts +3 -0
- package/src/types/index.ts +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/is-combined-error.ts +9 -0
package/dist/styles/index.css
CHANGED
|
@@ -158,6 +158,40 @@
|
|
|
158
158
|
padding: 24px;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/* src/editor/components/dialog-error/dialog-error.css */
|
|
162
|
+
.dialog-error {
|
|
163
|
+
position: relative;
|
|
164
|
+
padding: 8px 12px;
|
|
165
|
+
border: 1px solid var(--ui-error-border);
|
|
166
|
+
border-radius: 4px;
|
|
167
|
+
background-color: var(--ui-error-bg);
|
|
168
|
+
color: var(--ui-error-text);
|
|
169
|
+
font-size: 13px;
|
|
170
|
+
}
|
|
171
|
+
.dialog-error-list {
|
|
172
|
+
margin: 0;
|
|
173
|
+
padding-left: 16px;
|
|
174
|
+
}
|
|
175
|
+
.dialog-error-clear-button {
|
|
176
|
+
display: flex;
|
|
177
|
+
position: absolute;
|
|
178
|
+
top: 8px;
|
|
179
|
+
right: 8px;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
width: 20px;
|
|
183
|
+
height: 20px;
|
|
184
|
+
padding: 0;
|
|
185
|
+
border: none;
|
|
186
|
+
background-color: transparent;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
}
|
|
189
|
+
.dialog-error-clear-button svg {
|
|
190
|
+
width: 14px;
|
|
191
|
+
height: 14px;
|
|
192
|
+
color: var(--ui-error-text);
|
|
193
|
+
}
|
|
194
|
+
|
|
161
195
|
/* src/editor/dialogs/add-katex-dialog/add-katex-dialog.css */
|
|
162
196
|
.katex-dialog-title {
|
|
163
197
|
margin: 0 0 16px 0;
|
|
@@ -298,14 +332,6 @@
|
|
|
298
332
|
.link-dialog-input::placeholder {
|
|
299
333
|
color: var(--tiptap-color-text-placeholder);
|
|
300
334
|
}
|
|
301
|
-
.link-dialog-error {
|
|
302
|
-
padding: 8px 12px;
|
|
303
|
-
border: 1px solid var(--ui-error-border);
|
|
304
|
-
border-radius: 4px;
|
|
305
|
-
background-color: var(--ui-error-bg);
|
|
306
|
-
color: var(--ui-error-text);
|
|
307
|
-
font-size: 13px;
|
|
308
|
-
}
|
|
309
335
|
.link-dialog-actions {
|
|
310
336
|
display: flex;
|
|
311
337
|
justify-content: flex-end;
|
|
@@ -423,14 +449,6 @@
|
|
|
423
449
|
.video-dialog-platform-list li {
|
|
424
450
|
margin: 4px 0;
|
|
425
451
|
}
|
|
426
|
-
.video-dialog-error {
|
|
427
|
-
padding: 8px 12px;
|
|
428
|
-
border: 1px solid var(--ui-error-border);
|
|
429
|
-
border-radius: 4px;
|
|
430
|
-
background-color: var(--ui-error-bg);
|
|
431
|
-
color: var(--ui-error-text);
|
|
432
|
-
font-size: 13px;
|
|
433
|
-
}
|
|
434
452
|
.video-dialog-actions {
|
|
435
453
|
display: flex;
|
|
436
454
|
justify-content: flex-end;
|
|
@@ -563,6 +581,9 @@
|
|
|
563
581
|
background: var(--ui-secondary-bg);
|
|
564
582
|
color: var(--ui-text-primary);
|
|
565
583
|
}
|
|
584
|
+
.image-dialog-error-spacing {
|
|
585
|
+
margin-bottom: 16px;
|
|
586
|
+
}
|
|
566
587
|
|
|
567
588
|
/* src/styles/components.css */
|
|
568
589
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.dialog-error {
|
|
2
|
+
position: relative;
|
|
3
|
+
padding: 8px 12px;
|
|
4
|
+
border: 1px solid var(--ui-error-border);
|
|
5
|
+
border-radius: 4px;
|
|
6
|
+
background-color: var(--ui-error-bg);
|
|
7
|
+
color: var(--ui-error-text);
|
|
8
|
+
font-size: 13px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.dialog-error-list {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding-left: 16px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.dialog-error-clear-button {
|
|
17
|
+
display: flex;
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: 8px;
|
|
20
|
+
right: 8px;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
width: 20px;
|
|
24
|
+
height: 20px;
|
|
25
|
+
padding: 0;
|
|
26
|
+
border: none;
|
|
27
|
+
background-color: transparent;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.dialog-error-clear-button svg {
|
|
32
|
+
width: 14px;
|
|
33
|
+
height: 14px;
|
|
34
|
+
color: var(--ui-error-text);
|
|
35
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { isCombinedError } from "@/utils";
|
|
4
|
+
|
|
5
|
+
import { TiptapX } from "../../../icons";
|
|
6
|
+
import { useTiptapTranslations } from "../../context";
|
|
7
|
+
import { useMemo } from "react";
|
|
8
|
+
|
|
9
|
+
interface DialogErrorProps {
|
|
10
|
+
error: unknown;
|
|
11
|
+
onClose?: () => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DialogError = ({ error, onClose, className }: DialogErrorProps) => {
|
|
16
|
+
const translations = useTiptapTranslations();
|
|
17
|
+
|
|
18
|
+
const renderedMessage = useMemo(() => {
|
|
19
|
+
if (typeof error === "string") {
|
|
20
|
+
return error || translations.common.unknownError;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isCombinedError(error)) {
|
|
24
|
+
const safeErrors = error.cause.filter((e) => Boolean(e.message));
|
|
25
|
+
const errorsToDisplay =
|
|
26
|
+
safeErrors.length === error.cause.length
|
|
27
|
+
? safeErrors
|
|
28
|
+
: [new Error(translations.common.unknownError), ...safeErrors];
|
|
29
|
+
|
|
30
|
+
if (errorsToDisplay.length === 1) {
|
|
31
|
+
return errorsToDisplay[0].message;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ul className="dialog-error-list">
|
|
36
|
+
{errorsToDisplay.map((cause, index) => (
|
|
37
|
+
<li key={`${index}_${cause.message}`}>{cause.message}</li>
|
|
38
|
+
))}
|
|
39
|
+
</ul>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (error instanceof Error) {
|
|
44
|
+
return error.message || translations.common.unknownError;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return translations.common.unknownError;
|
|
48
|
+
}, [error, translations]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={`dialog-error ${className || ""}`}>
|
|
52
|
+
{renderedMessage}
|
|
53
|
+
{onClose && (
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
className="dialog-error-clear-button"
|
|
57
|
+
onClick={onClose}
|
|
58
|
+
aria-label={translations.common.clearError}
|
|
59
|
+
>
|
|
60
|
+
<TiptapX />
|
|
61
|
+
</button>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DialogError } from './dialog-error';
|
|
@@ -48,15 +48,6 @@
|
|
|
48
48
|
color: var(--tiptap-color-text-placeholder);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
.link-dialog-error {
|
|
52
|
-
padding: 8px 12px;
|
|
53
|
-
border: 1px solid var(--ui-error-border);
|
|
54
|
-
border-radius: 4px;
|
|
55
|
-
background-color: var(--ui-error-bg);
|
|
56
|
-
color: var(--ui-error-text);
|
|
57
|
-
font-size: 13px;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
51
|
.link-dialog-actions {
|
|
61
52
|
display: flex;
|
|
62
53
|
justify-content: flex-end;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
|
-
import { CommonModal } from "../../components";
|
|
5
|
+
import { CommonModal, DialogError } from "../../components";
|
|
6
6
|
import { useTiptapTranslations } from "../../context";
|
|
7
7
|
import { LinkDialogContextProps } from "@/types/link";
|
|
8
8
|
|
|
@@ -186,7 +186,7 @@ export const AddLinkDialog = ({
|
|
|
186
186
|
<p className="link-dialog-help-text">{translations.link.addNofollowHelp}</p>
|
|
187
187
|
</div>
|
|
188
188
|
|
|
189
|
-
{error && <
|
|
189
|
+
{error && <DialogError error={error} />}
|
|
190
190
|
</div>
|
|
191
191
|
|
|
192
192
|
<div className="link-dialog-actions">
|
|
@@ -83,15 +83,6 @@
|
|
|
83
83
|
margin: 4px 0;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
.video-dialog-error {
|
|
87
|
-
padding: 8px 12px;
|
|
88
|
-
border: 1px solid var(--ui-error-border);
|
|
89
|
-
border-radius: 4px;
|
|
90
|
-
background-color: var(--ui-error-bg);
|
|
91
|
-
color: var(--ui-error-text);
|
|
92
|
-
font-size: 13px;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
86
|
.video-dialog-actions {
|
|
96
87
|
display: flex;
|
|
97
88
|
justify-content: flex-end;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
|
-
import { CommonModal } from "../../components";
|
|
5
|
+
import { CommonModal, DialogError } from "../../components";
|
|
6
6
|
import { detectVideoHosting, VideoHosting } from "../../../utils/detect-video-hosting";
|
|
7
7
|
import { useTiptapTranslations } from "../../context";
|
|
8
8
|
|
|
@@ -128,7 +128,7 @@ export const AddVideoDialog = ({ open, onClose, onVideoAdd }: AddVideoDialogProp
|
|
|
128
128
|
</ul>
|
|
129
129
|
</div>
|
|
130
130
|
|
|
131
|
-
{error && <
|
|
131
|
+
{error && <DialogError error={error} />}
|
|
132
132
|
</div>
|
|
133
133
|
|
|
134
134
|
<div className="video-dialog-actions">
|
|
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { TiptapX } from "../../../icons";
|
|
6
6
|
|
|
7
|
-
import { Button, CommonModal, Input } from "../../components";
|
|
7
|
+
import { Button, CommonModal, DialogError, Input } from "../../components";
|
|
8
8
|
import { ImageUploadHandler, TiptapImageAttrs, ImageGetHandler, FigureAttrs } from "../../../types";
|
|
9
9
|
import { resolveImageSrc } from "../../../utils/image-src-resolver";
|
|
10
10
|
import { useTiptapTranslations } from "../../context";
|
|
@@ -90,13 +90,26 @@ export const ImageDialog = ({
|
|
|
90
90
|
height: constrainedDimensions.height,
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
const [fileUploadError, setFileUploadError] = useState<Error | null>(null);
|
|
94
|
+
|
|
93
95
|
const handleSelectFile = (file: File | undefined) => {
|
|
96
|
+
setFileUploadError(null);
|
|
97
|
+
|
|
94
98
|
if (!file) return;
|
|
95
99
|
|
|
100
|
+
if (!file.type.startsWith('image/')) {
|
|
101
|
+
setFileUploadError(new Error(translations.imageDialog.invalidFileType));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
96
105
|
const imageUrl = URL.createObjectURL(file);
|
|
97
106
|
const img = new Image();
|
|
98
107
|
img.src = imageUrl;
|
|
99
108
|
|
|
109
|
+
img.onerror = () => {
|
|
110
|
+
setFileUploadError(new Error(translations.imageDialog.invalidFileType));
|
|
111
|
+
};
|
|
112
|
+
|
|
100
113
|
img.onload = () => {
|
|
101
114
|
const naturalWidth = img.width;
|
|
102
115
|
const naturalHeight = img.height;
|
|
@@ -189,6 +202,7 @@ export const ImageDialog = ({
|
|
|
189
202
|
if (!formState.file) return;
|
|
190
203
|
|
|
191
204
|
setIsUploading(true);
|
|
205
|
+
|
|
192
206
|
try {
|
|
193
207
|
const result = await uploadHandler(formState.file);
|
|
194
208
|
|
|
@@ -204,6 +218,8 @@ export const ImageDialog = ({
|
|
|
204
218
|
});
|
|
205
219
|
|
|
206
220
|
handleClose();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
setFileUploadError(error instanceof Error ? error : new Error(translations.common.unknownError));
|
|
207
223
|
} finally {
|
|
208
224
|
setIsUploading(false);
|
|
209
225
|
}
|
|
@@ -223,7 +239,9 @@ export const ImageDialog = ({
|
|
|
223
239
|
handleClose();
|
|
224
240
|
};
|
|
225
241
|
|
|
226
|
-
const
|
|
242
|
+
const isEditMode = mode === "edit";
|
|
243
|
+
const canSave = isEditMode || Boolean(formState.file);
|
|
244
|
+
const handleSave = isEditMode ? handleEditImage : handleAddImage;
|
|
227
245
|
|
|
228
246
|
const handleClose = () => {
|
|
229
247
|
clearImageFormFields();
|
|
@@ -315,6 +333,13 @@ export const ImageDialog = ({
|
|
|
315
333
|
)}
|
|
316
334
|
</div>
|
|
317
335
|
</div>
|
|
336
|
+
{fileUploadError && (
|
|
337
|
+
<DialogError
|
|
338
|
+
error={fileUploadError}
|
|
339
|
+
onClose={() => setFileUploadError(null)}
|
|
340
|
+
className="image-dialog-error-spacing"
|
|
341
|
+
/>
|
|
342
|
+
)}
|
|
318
343
|
<div className="image-dialog-actions">
|
|
319
344
|
<Button
|
|
320
345
|
type="button"
|
|
@@ -328,6 +353,7 @@ export const ImageDialog = ({
|
|
|
328
353
|
isLoading={isUploading}
|
|
329
354
|
type="button"
|
|
330
355
|
onClick={handleSave}
|
|
356
|
+
disabled={!canSave}
|
|
331
357
|
>
|
|
332
358
|
{saveButtonText}
|
|
333
359
|
</Button>
|
|
@@ -17,11 +17,10 @@ export function useInsertionHandlers(imageHandlers?: ImageHandlers) {
|
|
|
17
17
|
}, [insertedImage, activeDialog]);
|
|
18
18
|
|
|
19
19
|
function genericInsertionHandler(file: File | undefined, event: Event) {
|
|
20
|
-
if (!file || !file.type.startsWith("image/")) return;
|
|
21
|
-
|
|
22
20
|
event.preventDefault();
|
|
23
21
|
event.stopPropagation();
|
|
24
22
|
|
|
23
|
+
if (!file || !file.type.startsWith("image/")) return;
|
|
25
24
|
if (!imageHandlers?.upload) return;
|
|
26
25
|
|
|
27
26
|
setInsertedImage(file);
|
package/src/editor/index.ts
CHANGED
|
@@ -2,3 +2,4 @@ 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";
|
|
5
|
+
export { type CombinedErrors as TipTapCombinedErrors } from '@/types/common'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
@import "../editor/components/button/button.css";
|
|
3
3
|
@import "../editor/components/input/input.css";
|
|
4
4
|
@import "../editor/components/common-modal/common-modal.css";
|
|
5
|
+
@import "../editor/components/dialog-error/dialog-error.css";
|
|
5
6
|
|
|
6
7
|
/* Dialog Styles */
|
|
7
8
|
@import "../editor/dialogs/add-katex-dialog/add-katex-dialog.css";
|
package/src/translations/en.json
CHANGED
|
@@ -112,7 +112,8 @@
|
|
|
112
112
|
"captionLabel": "Image caption",
|
|
113
113
|
"captionPlaceholder": "Description under image...",
|
|
114
114
|
"save": "Save",
|
|
115
|
-
"add": "Add"
|
|
115
|
+
"add": "Add",
|
|
116
|
+
"invalidFileType": "Invalid file format."
|
|
116
117
|
},
|
|
117
118
|
"videoUpload": {
|
|
118
119
|
"title": "Add video",
|
|
@@ -157,6 +158,8 @@
|
|
|
157
158
|
"words": ["word", "words", "words"]
|
|
158
159
|
},
|
|
159
160
|
"common": {
|
|
160
|
-
"cancel": "Cancel"
|
|
161
|
+
"cancel": "Cancel",
|
|
162
|
+
"unknownError": "Unknown error",
|
|
163
|
+
"clearError": "Clear error"
|
|
161
164
|
}
|
|
162
165
|
}
|
package/src/translations/ru.json
CHANGED
|
@@ -112,7 +112,8 @@
|
|
|
112
112
|
"captionLabel": "Подпись к изображению",
|
|
113
113
|
"captionPlaceholder": "Описание под изображением...",
|
|
114
114
|
"save": "Сохранить",
|
|
115
|
-
"add": "Добавить"
|
|
115
|
+
"add": "Добавить",
|
|
116
|
+
"invalidFileType": "Неверный формат файла."
|
|
116
117
|
},
|
|
117
118
|
"videoUpload": {
|
|
118
119
|
"title": "Добавить видео",
|
|
@@ -157,6 +158,8 @@
|
|
|
157
158
|
"words": ["слово", "слова", "слов"]
|
|
158
159
|
},
|
|
159
160
|
"common": {
|
|
160
|
-
"cancel": "Отменить"
|
|
161
|
+
"cancel": "Отменить",
|
|
162
|
+
"unknownError": "Неизвестная ошибка",
|
|
163
|
+
"clearError": "Сбросить ошибку"
|
|
161
164
|
}
|
|
162
165
|
}
|
package/src/types/index.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CombinedErrors } from "@/types";
|
|
2
|
+
|
|
3
|
+
export function isCombinedError(error: unknown): error is CombinedErrors {
|
|
4
|
+
return (
|
|
5
|
+
error instanceof Error &&
|
|
6
|
+
Array.isArray(error?.cause) &&
|
|
7
|
+
error.cause.length > 0 && error.cause.every((e) => e instanceof Error)
|
|
8
|
+
);
|
|
9
|
+
}
|