@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.
@@ -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.8",
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?: (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>
@@ -141,6 +141,7 @@ export const ImageDialog = ({
141
141
  caption: formState.caption,
142
142
  width: formState.width,
143
143
  height: formState.height,
144
+ storageKey: result.storageKey,
144
145
  });
145
146
 
146
147
  handleClose();
@@ -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 = (title: string, url: string) => {
172
- createHandleLinkAdd(editor, handleCloseDialog, savedDialogCursorPos)(title, url);
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={(selectedText, url) => setLinkDialogContext({ selectedText, url })}
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
- initialTitle={linkDialogContext?.selectedText || ""}
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
- return (title: string, url: string) => {
92
- if (!editor) return;
92
+ if (!editor) return;
93
93
 
94
- const hasSelection = !editor.state.selection.empty;
94
+ const hasSelection = !editor.state.selection.empty;
95
+ const titleLength = linkParams.title?.length ?? 0;
95
96
 
96
- if (hasSelection) {
97
- editor
98
- .chain()
99
- .focus()
100
- .command(({ tr, state }) => {
101
- const { from, to } = state.selection;
102
- tr.insertText(title, from, to);
103
- tr.addMark(from, from + title.length, editor.schema.marks.link.create({ href: url }));
104
- return true;
105
- })
106
- .run();
107
- } else {
108
- const { state } = editor;
109
- const insertPos = savedPos ?? state.selection.from;
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
- editor
112
- .chain()
113
- .focus()
114
- .command(({ tr }) => {
115
- const linkMark = editor.schema.marks.link.create({ href: url });
116
- const textNode = editor.schema.text(title, [linkMark]);
117
- tr.insert(insertPos, textNode);
118
- tr.setSelection(TextSelection.create(tr.doc, insertPos, insertPos + title.length));
119
- return true;
120
- })
121
- .run();
122
- }
123
- closeDialogCommand();
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 '@tiptap/extension-link';
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: ['http', 'https', 'mailto', 'tel'],
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: ['http', 'https', 'mailto', 'tel'],
17
+ protocols: ["http", "https", "mailto", "tel"],
18
18
  });
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
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
- let colWidths: number[] = [];
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
- colWidths.length > 0 ? colWidths.reduce((total, width) => total + width, 0) : null;
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={{ width: `${width}px` }}
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;
@@ -8,7 +8,7 @@
8
8
  font-family: var(--tiptap-font-family);
9
9
  }
10
10
 
11
- .tiptap-content-wrapper:focus {
11
+ .tiptap-content-wrapper:focus-within {
12
12
  border-color: var(--tiptap-color-primary-light);
13
13
  outline: none;
14
14
  }