@cyberyozh/text-editor 0.1.7 → 0.1.9

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 (36) hide show
  1. package/dist/editor.css +12 -2
  2. package/dist/editor.css.map +1 -1
  3. package/dist/editor.d.mts +1 -0
  4. package/dist/editor.d.ts +1 -0
  5. package/dist/editor.js +202 -139
  6. package/dist/editor.js.map +1 -1
  7. package/dist/editor.mjs +200 -137
  8. package/dist/editor.mjs.map +1 -1
  9. package/dist/render.js +76 -11
  10. package/dist/render.js.map +1 -1
  11. package/dist/render.mjs +76 -11
  12. package/dist/render.mjs.map +1 -1
  13. package/dist/styles/index.css +12 -2
  14. package/package.json +2 -4
  15. package/src/editor/controls/bubble-menu-content.tsx +4 -8
  16. package/src/editor/controls/callout-menu.tsx +2 -0
  17. package/src/editor/controls/drag-handle.tsx +6 -1
  18. package/src/editor/dialogs/add-link-dialog/add-link-dialog.css +12 -4
  19. package/src/editor/dialogs/add-link-dialog/add-link-dialog.tsx +65 -28
  20. package/src/editor/dialogs/image-dialog/image-dialog.tsx +1 -1
  21. package/src/editor/editor.tsx +19 -12
  22. package/src/editor/handlers/dialog-handlers.ts +87 -156
  23. package/src/editor/handlers/index.ts +1 -2
  24. package/src/extensions/base/compatibility.ts +52 -0
  25. package/src/extensions/base/figure.ts +7 -0
  26. package/src/extensions/base/link.ts +3 -3
  27. package/src/extensions/views/components/table.tsx +23 -4
  28. package/src/extensions/views/node-views/figure.tsx +1 -1
  29. package/src/render/static-renderer-config.tsx +1 -17
  30. package/src/styles/editor/base.css +1 -1
  31. package/src/styles/extensions/callout.css +1 -1
  32. package/src/translations/en.json +5 -1
  33. package/src/translations/ru.json +5 -1
  34. package/src/types/image-handler.ts +2 -0
  35. package/src/types/link.ts +9 -0
  36. package/src/editor/handlers/create-image-upload-handler.ts +0 -24
@@ -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: 1;
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.7",
3
+ "version": "0.1.9",
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",
@@ -88,8 +88,6 @@
88
88
  "devDependencies": {
89
89
  "@changesets/cli": "^2.29.7",
90
90
  "@types/node": "^20",
91
- "@types/react": "^19",
92
- "@types/react-dom": "^19",
93
91
  "eslint": "^8",
94
92
  "tsup": "^8.0.0",
95
93
  "typescript": "^5"
@@ -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?: (selectedText: string, url?: string) => void;
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
- let linkUrl: string | undefined;
105
- if (isLink) {
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 = node?.type.name === "callout";
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: (title: string, url: string) => void;
12
- initialTitle?: string;
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
- initialTitle = "",
21
- initialUrl = "",
20
+ linkDialogContext,
22
21
  }: AddLinkDialogProps) => {
23
22
  const translations = useTiptapTranslations();
24
- const [title, setTitle] = useState(initialTitle);
25
- const [url, setUrl] = useState(initialUrl);
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
- setTitle(initialTitle);
32
- setUrl(initialUrl);
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, initialTitle, initialUrl]);
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.trim()) {
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
- // Preserve protocols like mailto:, tel:, etc; only add https for plain URLs
88
- const hasProtocol = url.includes("://") || url.startsWith("mailto:") || url.startsWith("tel:");
98
+ const hasProtocol =
99
+ url?.includes("://") || url?.startsWith("mailto:") || url?.startsWith("tel:");
89
100
  const finalUrl = hasProtocol ? url : `https://${url}`;
90
- onLinkAdd(title, finalUrl);
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
- setTitle(initialTitle);
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
- {initialUrl ? translations.link.editLink : translations.link.addLink}
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
- {initialUrl ? translations.link.editLink : translations.link.addLink}
209
+ {linkDialogContext?.href ? translations.link.editLink : translations.link.addLink}
173
210
  </button>
174
211
  </div>
175
212
  </CommonModal>
@@ -45,7 +45,6 @@ export const ImageDialog = ({
45
45
  const [dragActive, setDragActive] = useState(false);
46
46
  const inputRef = useRef<HTMLInputElement | null>(null);
47
47
 
48
- // Resolve the image URL from fileId or use src directly
49
48
  const resolvedInitialUrl = initialAttrs
50
49
  ? resolveImageSrc({ fileId: initialAttrs.fileId, src: initialAttrs.src }, { getImageById })
51
50
  : "";
@@ -142,6 +141,7 @@ export const ImageDialog = ({
142
141
  caption: formState.caption,
143
142
  width: formState.width,
144
143
  height: formState.height,
144
+ storageKey: result.storageKey,
145
145
  });
146
146
 
147
147
  handleClose();
@@ -24,7 +24,7 @@ import {
24
24
  import { ImageDialog, AddKatexDialog, AddLinkDialog, AddVideoDialog } from "./dialogs";
25
25
  import { getEditorExtensions } from "../extensions/editor";
26
26
 
27
- import type { FigureAttrs, ImageHandlers } from "../types/image-handler";
27
+ import type { FigureAttrs, ImageHandlers, TiptapImageAttrs } from "../types/image-handler";
28
28
  import {
29
29
  createHandleImageAdd,
30
30
  createHandleImageEdit,
@@ -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]
@@ -157,10 +154,21 @@ function TiptapContent({
157
154
  closeDialogCommand();
158
155
  };
159
156
 
160
- const handleImageAdd = createHandleImageAdd(editor, handleCloseDialog, savedDialogCursorPos);
157
+ // Create handlers with current savedDialogCursorPos by accessing state directly
158
+ const handleImageAdd = (attrs: TiptapImageAttrs) => {
159
+ createHandleImageAdd(editor, handleCloseDialog, savedDialogCursorPos)(attrs);
160
+ };
161
+
161
162
  const handleImageEdit = createHandleImageEdit(editor, handleCloseDialog, editFigureContext?.pos);
162
- const handleVideoAdd = createHandleVideoAdd(editor, handleCloseDialog, savedDialogCursorPos);
163
- const handleLinkAdd = createHandleLinkAdd(editor, handleCloseDialog, savedDialogCursorPos);
163
+
164
+ const handleVideoAdd = (url: string) => {
165
+ createHandleVideoAdd(editor, handleCloseDialog, savedDialogCursorPos)(url);
166
+ };
167
+
168
+ const handleLinkAdd = (params: any) => {
169
+ createHandleLinkAdd(editor, handleCloseDialog, params, savedDialogCursorPos);
170
+ };
171
+
164
172
  const handleKatexAdd = (latex: string) => {
165
173
  const katexHandler = createHandleKatexAdd(editor, handleCloseDialog, savedDialogCursorPos);
166
174
  katexHandler(latex, katexDialogContext?.pos);
@@ -204,7 +212,7 @@ function TiptapContent({
204
212
  >
205
213
  <BubbleMenuContent
206
214
  editor={editor}
207
- onLinkClick={(selectedText, url) => setLinkDialogContext({ selectedText, url })}
215
+ onLinkClick={(params: LinkDialogContextProps) => setLinkDialogContext(params)}
208
216
  />
209
217
  </BubbleMenu>
210
218
  )}
@@ -239,8 +247,7 @@ function TiptapContent({
239
247
  open={true}
240
248
  onClose={handleCloseDialog}
241
249
  onLinkAdd={handleLinkAdd}
242
- initialTitle={linkDialogContext?.selectedText || ""}
243
- initialUrl={linkDialogContext?.url || ""}
250
+ linkDialogContext={linkDialogContext}
244
251
  />
245
252
  )}
246
253