@cyberyozh/text-editor 0.3.2 → 0.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 (40) hide show
  1. package/dist/editor.css +35 -43
  2. package/dist/editor.css.map +1 -1
  3. package/dist/editor.d.mts +20 -2
  4. package/dist/editor.d.ts +20 -2
  5. package/dist/editor.js +397 -327
  6. package/dist/editor.js.map +1 -1
  7. package/dist/editor.mjs +320 -246
  8. package/dist/editor.mjs.map +1 -1
  9. package/dist/render.d.mts +9 -1
  10. package/dist/render.d.ts +9 -1
  11. package/dist/render.js +40 -10
  12. package/dist/render.js.map +1 -1
  13. package/dist/render.mjs +45 -11
  14. package/dist/render.mjs.map +1 -1
  15. package/dist/styles/index.css +35 -43
  16. package/package.json +1 -1
  17. package/src/editor/components/dialog-error/dialog-error.css +35 -0
  18. package/src/editor/components/dialog-error/dialog-error.tsx +65 -0
  19. package/src/editor/components/dialog-error/index.ts +1 -0
  20. package/src/editor/components/index.ts +1 -0
  21. package/src/editor/dialogs/add-link-dialog/add-link-dialog.css +0 -9
  22. package/src/editor/dialogs/add-link-dialog/add-link-dialog.tsx +2 -2
  23. package/src/editor/dialogs/add-video-dialog/add-video-dialog.css +0 -9
  24. package/src/editor/dialogs/add-video-dialog/add-video-dialog.tsx +2 -2
  25. package/src/editor/dialogs/image-dialog/image-dialog.css +1 -29
  26. package/src/editor/dialogs/image-dialog/image-dialog.tsx +7 -13
  27. package/src/editor/editor.tsx +8 -0
  28. package/src/editor/index.ts +1 -0
  29. package/src/extensions/base/link.test.ts +60 -0
  30. package/src/extensions/base/link.ts +61 -9
  31. package/src/extensions/editor/get-editor-extensions.ts +3 -1
  32. package/src/styles/components.css +1 -0
  33. package/src/translations/en.json +4 -4
  34. package/src/translations/ru.json +4 -4
  35. package/src/types/common.ts +3 -0
  36. package/src/types/index.ts +1 -0
  37. package/src/utils/get-shared-extensions.ts +20 -4
  38. package/src/utils/html-to-tiptap-content.ts +13 -4
  39. package/src/utils/index.ts +1 -0
  40. package/src/utils/is-combined-error.ts +9 -0
@@ -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,34 +581,8 @@
563
581
  background: var(--ui-secondary-bg);
564
582
  color: var(--ui-text-primary);
565
583
  }
566
- .image-dialog-error {
567
- position: relative;
584
+ .image-dialog-error-spacing {
568
585
  margin-bottom: 16px;
569
- padding: 8px 12px;
570
- border: 1px solid var(--ui-error-border);
571
- border-radius: 4px;
572
- background-color: var(--ui-error-bg);
573
- color: var(--ui-error-text);
574
- font-size: 13px;
575
- }
576
- .image-dialog-error-clear-button {
577
- display: flex;
578
- position: absolute;
579
- top: 8px;
580
- right: 8px;
581
- align-items: center;
582
- justify-content: center;
583
- width: 20px;
584
- height: 20px;
585
- padding: 0;
586
- border: none;
587
- background-color: transparent;
588
- cursor: pointer;
589
- }
590
- .image-dialog-error-clear-button svg {
591
- width: 14px;
592
- height: 14px;
593
- color: var(--ui-error-text);
594
586
  }
595
587
 
596
588
  /* src/styles/components.css */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyberyozh/text-editor",
3
- "version": "0.3.2",
3
+ "version": "0.5.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">
@@ -101,34 +101,6 @@
101
101
  color: var(--ui-text-primary);
102
102
  }
103
103
 
104
- .image-dialog-error {
105
- position: relative;
104
+ .image-dialog-error-spacing {
106
105
  margin-bottom: 16px;
107
- padding: 8px 12px;
108
- border: 1px solid var(--ui-error-border);
109
- border-radius: 4px;
110
- background-color: var(--ui-error-bg);
111
- color: var(--ui-error-text);
112
- font-size: 13px;
113
- }
114
-
115
- .image-dialog-error-clear-button {
116
- display: flex;
117
- position: absolute;
118
- top: 8px;
119
- right: 8px;
120
- align-items: center;
121
- justify-content: center;
122
- width: 20px;
123
- height: 20px;
124
- padding: 0;
125
- border: none;
126
- background-color: transparent;
127
- cursor: pointer;
128
- }
129
-
130
- .image-dialog-error-clear-button svg {
131
- width: 14px;
132
- height: 14px;
133
- color: var(--ui-error-text);
134
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";
@@ -219,7 +219,7 @@ export const ImageDialog = ({
219
219
 
220
220
  handleClose();
221
221
  } catch (error) {
222
- setFileUploadError(error instanceof Error ? error : new Error(translations.imageDialog.unknownError));
222
+ setFileUploadError(error instanceof Error ? error : new Error(translations.common.unknownError));
223
223
  } finally {
224
224
  setIsUploading(false);
225
225
  }
@@ -334,17 +334,11 @@ export const ImageDialog = ({
334
334
  </div>
335
335
  </div>
336
336
  {fileUploadError && (
337
- <div className="image-dialog-error">
338
- {fileUploadError.message || translations.imageDialog.unknownError}
339
- <button
340
- type="button"
341
- className="image-dialog-error-clear-button"
342
- onClick={() => setFileUploadError(null)}
343
- aria-label={translations.imageDialog.clearError}
344
- >
345
- <TiptapX />
346
- </button>
347
- </div>
337
+ <DialogError
338
+ error={fileUploadError}
339
+ onClose={() => setFileUploadError(null)}
340
+ className="image-dialog-error-spacing"
341
+ />
348
342
  )}
349
343
  <div className="image-dialog-actions">
350
344
  <Button
@@ -60,6 +60,12 @@ export interface TiptapProps {
60
60
  */
61
61
  lang?: SupportedLanguage;
62
62
  error?: string;
63
+ /**
64
+ * Domains (with all their subdomains) whose links are treated as internal:
65
+ * rel="noopener noreferrer nofollow" is not added on paste / HTML parsing.
66
+ * Read once when the editor instance is created.
67
+ */
68
+ trustedDomains?: string[];
63
69
  }
64
70
 
65
71
  export interface KatexDialogContextProps {
@@ -75,6 +81,7 @@ function TiptapContent({
75
81
  isDisabled = false,
76
82
  lang,
77
83
  error,
84
+ trustedDomains,
78
85
  }: TiptapProps) {
79
86
  const [linkDialogContext, setLinkDialogContext] = useState<LinkDialogContextProps | null>(null);
80
87
  const [katexDialogContext, setKatexDialogContext] = useState<KatexDialogContextProps | null>(
@@ -117,6 +124,7 @@ function TiptapContent({
117
124
  suggestion: createSuggestions(translations, !!imageHandlers),
118
125
  isEditable: isEditable && !disabled,
119
126
  getImageById: imageHandlers?.getImage,
127
+ trustedDomains,
120
128
  onKatexClick: (latex, pos) => {
121
129
  setKatexDialogContext({ latex, pos });
122
130
  openDialogCommand("katex");
@@ -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'
@@ -0,0 +1,60 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { isTrustedDomain } from "./link";
3
+
4
+ describe("isTrustedDomain", () => {
5
+ afterEach(() => {
6
+ vi.unstubAllGlobals();
7
+ });
8
+
9
+ it("returns false for external urls when no trusted domains configured", () => {
10
+ expect(isTrustedDomain("https://example.com/page", [])).toBe(false);
11
+ });
12
+
13
+ it("matches an exact trusted domain", () => {
14
+ expect(
15
+ isTrustedDomain("https://cyberyozh.com/blog", ["cyberyozh.com"]),
16
+ ).toBe(true);
17
+ });
18
+
19
+ it("matches subdomains of a trusted domain", () => {
20
+ expect(
21
+ isTrustedDomain("https://app.cyberyozh.com/account", ["cyberyozh.com"]),
22
+ ).toBe(true);
23
+ expect(
24
+ isTrustedDomain("https://www.deep.cyberyozh.pro/", ["cyberyozh.pro"]),
25
+ ).toBe(true);
26
+ });
27
+
28
+ it("does not match lookalike domains sharing a suffix without a dot boundary", () => {
29
+ expect(
30
+ isTrustedDomain("https://evilcyberyozh.com/", ["cyberyozh.com"]),
31
+ ).toBe(false);
32
+ expect(
33
+ isTrustedDomain("https://cyberyozh.com.attacker.io/", ["cyberyozh.com"]),
34
+ ).toBe(false);
35
+ });
36
+
37
+ it("matches any domain from the list", () => {
38
+ const trusted = ["cyberyozh.com", "cyberyozh.pro", "scriptrun.ai"];
39
+ expect(isTrustedDomain("https://scriptrun.ai/docs", trusted)).toBe(true);
40
+ expect(isTrustedDomain("https://cyberyozh.pro/", trusted)).toBe(true);
41
+ expect(isTrustedDomain("https://github.com/", trusted)).toBe(false);
42
+ });
43
+
44
+ it("treats the current window hostname as trusted regardless of the list", () => {
45
+ vi.stubGlobal("window", { location: { hostname: "app.detect.expert" } });
46
+ expect(isTrustedDomain("https://app.detect.expert/page", [])).toBe(true);
47
+ expect(isTrustedDomain("https://other.detect.expert/page", [])).toBe(false);
48
+ });
49
+
50
+ it("returns false for unparseable urls", () => {
51
+ expect(isTrustedDomain("not a url", ["cyberyozh.com"])).toBe(false);
52
+ expect(isTrustedDomain("/relative/path", ["cyberyozh.com"])).toBe(false);
53
+ });
54
+
55
+ it("is case-insensitive for the hostname", () => {
56
+ expect(
57
+ isTrustedDomain("https://App.CyberYozh.com/", ["cyberyozh.com"]),
58
+ ).toBe(true);
59
+ });
60
+ });
@@ -1,8 +1,17 @@
1
- import Link, { isAllowedUri } from "@tiptap/extension-link";
1
+ import Link, { isAllowedUri, type LinkOptions } from "@tiptap/extension-link";
2
2
  import { getMarkRange, markPasteRule } from "@tiptap/core";
3
3
  import { Plugin, TextSelection } from "@tiptap/pm/state";
4
4
  import { find } from "linkifyjs";
5
5
 
6
+ export interface ExtendedLinkOptions extends LinkOptions {
7
+ /**
8
+ * Domains (with all their subdomains) whose links are treated as internal:
9
+ * rel="noopener noreferrer nofollow" is not added on paste / HTML parsing.
10
+ * The current window hostname is always trusted.
11
+ */
12
+ trustedDomains: string[];
13
+ }
14
+
6
15
  const isSameDomain = (url: string): boolean => {
7
16
  if (typeof window === "undefined") return false;
8
17
  try {
@@ -12,8 +21,38 @@ const isSameDomain = (url: string): boolean => {
12
21
  }
13
22
  };
14
23
 
15
- export const LinkExtension = Link.extend({
24
+ export const isTrustedDomain = (
25
+ url: string,
26
+ trustedDomains: string[],
27
+ ): boolean => {
28
+ try {
29
+ const { hostname } = new URL(url);
30
+ if (
31
+ typeof window !== "undefined" &&
32
+ hostname === window.location.hostname
33
+ ) {
34
+ return true;
35
+ }
36
+ return trustedDomains.some((trusted) => {
37
+ const domain = trusted.toLowerCase();
38
+ return hostname === domain || hostname.endsWith(`.${domain}`);
39
+ });
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+
45
+ export const LinkExtension = Link.extend<ExtendedLinkOptions>({
46
+ addOptions() {
47
+ return {
48
+ ...(this.parent?.() ?? ({} as ExtendedLinkOptions)),
49
+ trustedDomains: [] as string[],
50
+ };
51
+ },
52
+
16
53
  addAttributes() {
54
+ const { trustedDomains } = this.options;
55
+
17
56
  return {
18
57
  ...this.parent?.(),
19
58
  target: {
@@ -27,7 +66,9 @@ export const LinkExtension = Link.extend({
27
66
  default: null,
28
67
  parseHTML: (element) => {
29
68
  const href = element.getAttribute("href") ?? "";
30
- return isSameDomain(href) ? null : "noopener noreferrer nofollow";
69
+ return isTrustedDomain(href, trustedDomains)
70
+ ? null
71
+ : "noopener noreferrer nofollow";
31
72
  },
32
73
  },
33
74
  };
@@ -53,16 +94,23 @@ export const LinkExtension = Link.extend({
53
94
  defaultProtocol,
54
95
  }),
55
96
  )
56
- .filter((link) => this.options.shouldAutoLink?.(link.value) !== false)
57
- .map((link) => ({ text: link.value, data: { href: link.href }, index: link.start })),
97
+ .filter(
98
+ (link) => this.options.shouldAutoLink?.(link.value) !== false,
99
+ )
100
+ .map((link) => ({
101
+ text: link.value,
102
+ data: { href: link.href },
103
+ index: link.start,
104
+ })),
58
105
  type: this.type,
59
106
  getAttributes: (match) => {
60
107
  const href = match.data?.href ?? "";
61
- const sameDomain = isSameDomain(href);
62
108
  return {
63
109
  href,
64
- target: sameDomain ? "_self" : "_blank",
65
- rel: sameDomain ? null : "noopener noreferrer nofollow",
110
+ target: isSameDomain(href) ? "_self" : "_blank",
111
+ rel: isTrustedDomain(href, this.options.trustedDomains)
112
+ ? null
113
+ : "noopener noreferrer nofollow",
66
114
  };
67
115
  },
68
116
  }),
@@ -80,7 +128,11 @@ export const LinkExtension = Link.extend({
80
128
  const range = getMarkRange(view.state.doc.resolve(pos), this.type);
81
129
  if (!range) return false;
82
130
 
83
- const selection = TextSelection.create(view.state.doc, range.from, range.to);
131
+ const selection = TextSelection.create(
132
+ view.state.doc,
133
+ range.from,
134
+ range.to,
135
+ );
84
136
  view.dispatch(view.state.tr.setSelection(selection));
85
137
  return true;
86
138
  },
@@ -29,6 +29,7 @@ interface GetExtensionsOptions {
29
29
  getImageById?: (fileId: string) => string | null;
30
30
  onKatexClick?: (latex: string, pos?: number) => void;
31
31
  onFigureClick?: (attrs: any, pos: number) => void;
32
+ trustedDomains?: string[];
32
33
  }
33
34
 
34
35
  /**
@@ -44,8 +45,9 @@ export function getEditorExtensions(options: GetExtensionsOptions) {
44
45
  getImageById,
45
46
  onKatexClick,
46
47
  onFigureClick,
48
+ trustedDomains,
47
49
  } = options;
48
- const sharedExtensions = getSharedExtensions();
50
+ const sharedExtensions = getSharedExtensions({ trustedDomains });
49
51
 
50
52
  return [
51
53
  ...sharedExtensions,
@@ -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";
@@ -113,9 +113,7 @@
113
113
  "captionPlaceholder": "Description under image...",
114
114
  "save": "Save",
115
115
  "add": "Add",
116
- "invalidFileType": "File is not an image",
117
- "unknownError": "Unknown error",
118
- "clearError": "Clear error"
116
+ "invalidFileType": "Invalid file format."
119
117
  },
120
118
  "videoUpload": {
121
119
  "title": "Add video",
@@ -160,6 +158,8 @@
160
158
  "words": ["word", "words", "words"]
161
159
  },
162
160
  "common": {
163
- "cancel": "Cancel"
161
+ "cancel": "Cancel",
162
+ "unknownError": "Unknown error",
163
+ "clearError": "Clear error"
164
164
  }
165
165
  }
@@ -113,9 +113,7 @@
113
113
  "captionPlaceholder": "Описание под изображением...",
114
114
  "save": "Сохранить",
115
115
  "add": "Добавить",
116
- "invalidFileType": "Файл не является изображением",
117
- "unknownError": "Неизвестная ошибка",
118
- "clearError": "Сбросить ошибку"
116
+ "invalidFileType": "Неверный формат файла."
119
117
  },
120
118
  "videoUpload": {
121
119
  "title": "Добавить видео",
@@ -160,6 +158,8 @@
160
158
  "words": ["слово", "слова", "слов"]
161
159
  },
162
160
  "common": {
163
- "cancel": "Отменить"
161
+ "cancel": "Отменить",
162
+ "unknownError": "Неизвестная ошибка",
163
+ "clearError": "Сбросить ошибку"
164
164
  }
165
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';