@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyberyozh/text-editor",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Rich text editor library based on Tiptap for CyberYozh",
5
5
  "author": "CyberYozh",
6
6
  "license": "MIT",
@@ -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';
@@ -1,3 +1,4 @@
1
1
  export { Button } from './button';
2
2
  export { Input } from './input';
3
3
  export { CommonModal } from './common-modal';
4
+ 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 && <div className="link-dialog-error">{error}</div>}
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 && <div className="video-dialog-error">{error}</div>}
131
+ {error && <DialogError error={error} />}
132
132
  </div>
133
133
 
134
134
  <div className="video-dialog-actions">
@@ -100,3 +100,7 @@
100
100
  background: var(--ui-secondary-bg);
101
101
  color: var(--ui-text-primary);
102
102
  }
103
+
104
+ .image-dialog-error-spacing {
105
+ margin-bottom: 16px;
106
+ }
@@ -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 handleSave = mode === "edit" ? handleEditImage : handleAddImage;
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);
@@ -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";
@@ -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
  }
@@ -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
  }
@@ -0,0 +1,3 @@
1
+ export interface CombinedErrors extends Error {
2
+ cause: Array<Error>
3
+ }
@@ -1 +1,2 @@
1
+ export * from './common';
1
2
  export * from './image-handler';
@@ -1,2 +1,3 @@
1
1
  export { copyToClipboard } from './copy-to-clipboard';
2
2
  export { pluralize } from './pluralize';
3
+ export { isCombinedError } from './is-combined-error';
@@ -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
+ }