@cyberyozh/text-editor 0.1.11 → 0.1.13

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.
@@ -570,11 +570,13 @@
570
570
  display: flex;
571
571
  flex-direction: column;
572
572
  align-items: center;
573
+ max-width: 100%;
573
574
  margin: 24px auto;
574
575
  }
575
576
  .figure-image-container {
576
577
  display: inline-block;
577
578
  position: relative;
579
+ max-width: 100%;
578
580
  }
579
581
  .figure-image-container img {
580
582
  display: block;
@@ -582,6 +584,11 @@
582
584
  height: auto;
583
585
  border-radius: 12px;
584
586
  }
587
+ [data-resize-wrapper] .figure-image-container img {
588
+ width: 100%;
589
+ height: 100%;
590
+ object-fit: contain;
591
+ }
585
592
  .figure-edit-button {
586
593
  display: flex;
587
594
  position: absolute;
@@ -624,6 +631,64 @@
624
631
  letter-spacing: -0.32px;
625
632
  text-align: center;
626
633
  }
634
+ .tiptap-resize-container {
635
+ display: inline-block;
636
+ position: relative;
637
+ justify-content: center;
638
+ max-width: 100%;
639
+ margin: 0 auto;
640
+ }
641
+ .tiptap-resize-wrapper {
642
+ display: inline-block;
643
+ position: relative;
644
+ margin: 0 auto;
645
+ }
646
+ [data-resize-handle] {
647
+ z-index: 10;
648
+ position: absolute;
649
+ background: var(--tiptap-color-primary, #3b82f6);
650
+ opacity: 0;
651
+ transition: opacity 0.2s ease;
652
+ }
653
+ [data-resize-container]:hover [data-resize-handle],
654
+ [data-resize-container].is-resizing [data-resize-handle] {
655
+ opacity: 1;
656
+ }
657
+ [data-resize-handle=top-left],
658
+ [data-resize-handle=top-right],
659
+ [data-resize-handle=bottom-left],
660
+ [data-resize-handle=bottom-right] {
661
+ width: 10px;
662
+ height: 10px;
663
+ border: 1px solid white;
664
+ border-radius: 50%;
665
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
666
+ }
667
+ [data-resize-handle=top-left] {
668
+ top: -4px !important;
669
+ left: -4px !important;
670
+ cursor: nwse-resize;
671
+ }
672
+ [data-resize-handle=top-right] {
673
+ top: -4px !important;
674
+ right: -4px !important;
675
+ cursor: nesw-resize;
676
+ }
677
+ [data-resize-handle=bottom-left] {
678
+ left: -4px !important;
679
+ cursor: nesw-resize;
680
+ }
681
+ [data-resize-handle=bottom-right] {
682
+ right: -4px !important;
683
+ cursor: nwse-resize;
684
+ }
685
+ [data-resize-container].is-resizing {
686
+ user-select: none;
687
+ }
688
+ [data-resize-container].is-resizing [data-resize-handle] {
689
+ background: var(--tiptap-color-primary-active, #2563eb);
690
+ opacity: 1;
691
+ }
627
692
 
628
693
  /* src/styles/extensions/table.css */
629
694
  :root {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyberyozh/text-editor",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Rich text editor library based on Tiptap for CyberYozh",
5
5
  "author": "CyberYozh",
6
6
  "license": "MIT",
@@ -49,14 +49,43 @@ export const ImageDialog = ({
49
49
  ? resolveImageSrc({ fileId: initialAttrs.fileId, src: initialAttrs.src }, { getImageById })
50
50
  : "";
51
51
 
52
+ const getConstrainedDimensions = () => {
53
+ if (!initialAttrs?.width || !initialAttrs?.height) {
54
+ return { width: 0, height: 0 };
55
+ }
56
+
57
+ let maxWidth: number | undefined;
58
+ try {
59
+ const editorElement = document.querySelector(".tiptap-content-wrapper") as HTMLElement;
60
+ if (editorElement && editorElement.clientWidth > 0) {
61
+ maxWidth = editorElement.clientWidth - 32;
62
+ }
63
+ } catch {}
64
+
65
+ if (maxWidth && initialAttrs.width > maxWidth) {
66
+ const aspectRatio = initialAttrs.height / initialAttrs.width;
67
+ return {
68
+ width: maxWidth,
69
+ height: Math.round(maxWidth * aspectRatio),
70
+ };
71
+ }
72
+
73
+ return {
74
+ width: initialAttrs.width,
75
+ height: initialAttrs.height,
76
+ };
77
+ };
78
+
79
+ const constrainedDimensions = getConstrainedDimensions();
80
+
52
81
  const [formState, setFormState] = useState<ImageFormState>({
53
82
  file: null,
54
83
  title: initialAttrs?.title || "",
55
84
  alt: initialAttrs?.alt || "",
56
85
  caption: initialAttrs?.caption || "",
57
86
  fileUrl: resolvedInitialUrl,
58
- width: initialAttrs?.width || 0,
59
- height: initialAttrs?.height || 0,
87
+ width: constrainedDimensions.width,
88
+ height: constrainedDimensions.height,
60
89
  });
61
90
 
62
91
  const handleSelectFile = (file: File | undefined) => {
@@ -67,15 +96,37 @@ export const ImageDialog = ({
67
96
  img.src = imageUrl;
68
97
 
69
98
  img.onload = () => {
70
- const { width, height } = img;
99
+ const naturalWidth = img.width;
100
+ const naturalHeight = img.height;
101
+ const naturalAspectRatio = naturalHeight / naturalWidth;
102
+
103
+ let maxWidth: number | undefined;
104
+ try {
105
+ const editorElement = document.querySelector(".tiptap-content-wrapper") as HTMLElement;
106
+ if (editorElement && editorElement.clientWidth > 0) {
107
+ maxWidth = editorElement.clientWidth - 32;
108
+ }
109
+ } catch {}
110
+
111
+ let finalWidth: number;
112
+ let finalHeight: number;
113
+
114
+ if (maxWidth && naturalWidth > maxWidth) {
115
+ finalWidth = maxWidth;
116
+ finalHeight = Math.round(maxWidth * naturalAspectRatio);
117
+ } else {
118
+ finalWidth = naturalWidth;
119
+ finalHeight = naturalHeight;
120
+ }
121
+
71
122
  setFormState((prev) => ({
72
123
  ...prev,
73
124
  file,
74
125
  fileUrl: imageUrl,
75
126
  title: file.name,
76
127
  alt: file.name,
77
- width,
78
- height,
128
+ width: finalWidth,
129
+ height: finalHeight,
79
130
  }));
80
131
  };
81
132
  };
@@ -157,6 +208,8 @@ export const ImageDialog = ({
157
208
  alt: formState.alt,
158
209
  title: formState.title,
159
210
  caption: formState.caption,
211
+ width: formState.width || undefined,
212
+ height: formState.height || undefined,
160
213
  });
161
214
 
162
215
  handleClose();
@@ -77,7 +77,7 @@ function TiptapContent({
77
77
  }: TiptapProps) {
78
78
  const [linkDialogContext, setLinkDialogContext] = useState<LinkDialogContextProps | null>(null);
79
79
  const [katexDialogContext, setKatexDialogContext] = useState<KatexDialogContextProps | null>(
80
- null
80
+ null,
81
81
  );
82
82
  const [editFigureContext, setEditFigureContext] = useState<{
83
83
  attrs: FigureAttrs;
@@ -136,7 +136,7 @@ function TiptapContent({
136
136
  onCreate: ({ editor }) => handleChange?.(editor.getJSON()),
137
137
  onBlur: ({ editor }) => handleChange?.(editor.getJSON()),
138
138
  },
139
- [lang]
139
+ [lang],
140
140
  );
141
141
 
142
142
  useEffect(() => {
@@ -1,3 +1,4 @@
1
1
  export { Tiptap } from "./editor";
2
2
  export type { SupportedLanguage as TiptapSupportedLanguages } from "../utils/get-translations";
3
3
  export { type JSONContent } from "@tiptap/react";
4
+ export { htmlToTiptapContent } from "../utils/html-to-tiptap-content";
@@ -1,10 +1,9 @@
1
1
  import { mergeAttributes, nodePasteRule } from "@tiptap/core";
2
- import Image from "@tiptap/extension-image";
2
+ import Image, { ImageOptions } from "@tiptap/extension-image";
3
3
  import { resolveImageSrc } from "../../utils/image-src-resolver";
4
4
  import type { FigureAttrs } from "../../types";
5
5
 
6
- export interface FigureOptions {
7
- HTMLAttributes: Record<string, unknown>;
6
+ export interface FigureOptions extends ImageOptions {
8
7
  getImageById?: (fileId: string) => string | null;
9
8
  onFigureClick?: (attrs: FigureAttrs, pos: number) => void;
10
9
  }
@@ -27,11 +26,12 @@ export const BaseFigure = Image.extend<FigureOptions>({
27
26
  selectable: false,
28
27
 
29
28
  addOptions() {
29
+ const parentOptions = this.parent?.() || {};
30
30
  return {
31
- HTMLAttributes: {},
31
+ ...parentOptions,
32
32
  getImageById: undefined,
33
33
  onFigureClick: undefined,
34
- };
34
+ } as FigureOptions;
35
35
  },
36
36
 
37
37
  addAttributes() {
@@ -1,11 +1,213 @@
1
1
  import { ReactNodeViewRenderer } from "@tiptap/react";
2
+ import { ResizableNodeView } from "@tiptap/core";
2
3
  import { BaseFigure } from "../base";
3
4
  import { FigureView } from "../views/node-views";
4
5
  import { BaseLegacyFigure } from "../base/compatibility";
5
6
 
6
7
  export const Figure = BaseFigure.extend({
7
8
  addNodeView() {
8
- return ReactNodeViewRenderer(FigureView);
9
+ const resizeOptions = this.options.resize;
10
+
11
+ if (!resizeOptions) {
12
+ return ReactNodeViewRenderer(FigureView);
13
+ }
14
+
15
+ return (props) => {
16
+ const { node, getPos, editor } = props;
17
+ const attrs = node.attrs;
18
+
19
+ const imageExtension = editor.extensionManager.extensions.find(
20
+ (ext) => ext.name === "image",
21
+ ) as typeof BaseFigure | undefined;
22
+ const getImageById = imageExtension?.options.getImageById;
23
+
24
+ let imgSrc = attrs.src;
25
+ if (attrs.fileId && getImageById) {
26
+ const resolvedSrc = getImageById(attrs.fileId);
27
+ if (resolvedSrc) imgSrc = resolvedSrc;
28
+ }
29
+
30
+ let maxWidth: number | undefined;
31
+ try {
32
+ if (editor.view?.dom) {
33
+ const contentWrapper = editor.view.dom.closest(".tiptap-content-wrapper") as HTMLElement;
34
+ if (contentWrapper && contentWrapper.clientWidth > 0) {
35
+ maxWidth = contentWrapper.clientWidth - 32;
36
+ }
37
+ }
38
+ } catch {}
39
+
40
+ let naturalAspectRatio: number | undefined;
41
+ let maxHeight: number | undefined;
42
+
43
+ const constrainDimensions = (w: number, h: number) => {
44
+ let constrainedW = w;
45
+ let constrainedH = h;
46
+
47
+ if (maxWidth && maxHeight && naturalAspectRatio && w >= maxWidth) {
48
+ constrainedW = maxWidth;
49
+ constrainedH = maxHeight;
50
+ }
51
+
52
+ return { width: constrainedW, height: constrainedH };
53
+ };
54
+
55
+ const updateImageDimensions = (width: number, height: number) => {
56
+ imageContainer.style.width = `${width}px`;
57
+ imageContainer.style.height = `${height}px`;
58
+ };
59
+
60
+ const figure = document.createElement("figure");
61
+ figure.setAttribute("data-drag-handle", "");
62
+
63
+ const imageContainer = document.createElement("div");
64
+ imageContainer.className = "figure-image-container";
65
+
66
+ const img = document.createElement("img");
67
+ img.src = imgSrc || "";
68
+ if (attrs.alt) img.alt = attrs.alt;
69
+ if (attrs.title) img.title = attrs.title;
70
+
71
+ if (attrs.width && attrs.height) {
72
+ imageContainer.style.width = `${attrs.width}px`;
73
+ imageContainer.style.height = `${attrs.height}px`;
74
+ }
75
+
76
+ img.onload = () => {
77
+ naturalAspectRatio = img.naturalHeight / img.naturalWidth;
78
+
79
+ if (maxWidth && naturalAspectRatio) {
80
+ maxHeight = Math.round(maxWidth * naturalAspectRatio);
81
+ }
82
+
83
+ if (attrs.width && attrs.height && maxWidth && attrs.width > maxWidth) {
84
+ const width = maxWidth;
85
+ const height = Math.round((attrs.height / attrs.width) * maxWidth);
86
+
87
+ imageContainer.style.width = `${width}px`;
88
+ imageContainer.style.height = `${height}px`;
89
+
90
+ const pos = getPos();
91
+ if (typeof pos === "number") {
92
+ editor
93
+ .chain()
94
+ .updateAttributes("image", {
95
+ width: Math.round(width),
96
+ height: Math.round(height),
97
+ })
98
+ .run();
99
+ }
100
+ }
101
+ };
102
+
103
+ imageContainer.appendChild(img);
104
+
105
+ if (editor.isEditable) {
106
+ const editButton = document.createElement("button");
107
+ editButton.type = "button";
108
+ editButton.className = "figure-edit-button";
109
+ editButton.contentEditable = "false";
110
+ editButton.innerHTML =
111
+ '<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none"><path d="M2.52441 14.3751V17.5001H5.64941L14.8661 8.28346L11.7411 5.15846L2.52441 14.3751ZM17.2827 5.8668C17.6077 5.5418 17.6077 5.0168 17.2827 4.6918L15.3327 2.7418C15.0077 2.4168 14.4827 2.4168 14.1577 2.7418L12.6327 4.2668L15.7577 7.3918L17.2827 5.8668Z" fill="currentColor"/></svg>';
112
+
113
+ editButton.addEventListener("click", () => {
114
+ const onFigureClick = imageExtension?.options.onFigureClick;
115
+ const pos = getPos();
116
+ if (onFigureClick && typeof pos === "number") {
117
+ onFigureClick(attrs, pos);
118
+ }
119
+ });
120
+
121
+ imageContainer.appendChild(editButton);
122
+ }
123
+
124
+ const resizableView = new ResizableNodeView({
125
+ element: imageContainer,
126
+ node,
127
+ getPos,
128
+ onResize: (w, h) => {
129
+ const { width, height } = constrainDimensions(w, h);
130
+ updateImageDimensions(width, height);
131
+ },
132
+ onCommit: (w, h) => {
133
+ const { width, height } = constrainDimensions(w, h);
134
+ const pos = getPos();
135
+
136
+ if (typeof pos === "number") {
137
+ updateImageDimensions(Math.round(width), Math.round(height));
138
+
139
+ editor
140
+ .chain()
141
+ .focus()
142
+ .setNodeSelection(pos)
143
+ .updateAttributes("image", {
144
+ width: Math.round(width),
145
+ height: Math.round(height),
146
+ })
147
+ .run();
148
+ }
149
+ },
150
+ onUpdate: (updatedNode) => {
151
+ if (updatedNode.attrs.width) {
152
+ imageContainer.style.width = `${updatedNode.attrs.width}px`;
153
+ }
154
+ if (updatedNode.attrs.height) {
155
+ imageContainer.style.height = `${updatedNode.attrs.height}px`;
156
+ }
157
+
158
+ const existingCaption = figure.querySelector("figcaption");
159
+ if (updatedNode.attrs.caption && !existingCaption) {
160
+ const figcaption = document.createElement("figcaption");
161
+ figcaption.textContent = updatedNode.attrs.caption;
162
+ figure.appendChild(figcaption);
163
+ } else if (existingCaption) {
164
+ if (updatedNode.attrs.caption) {
165
+ existingCaption.textContent = updatedNode.attrs.caption;
166
+ } else {
167
+ existingCaption.remove();
168
+ }
169
+ }
170
+
171
+ if (updatedNode.type !== node.type) return false;
172
+ return true;
173
+ },
174
+ options: {
175
+ directions: resizeOptions.directions || [
176
+ "bottom-right",
177
+ "bottom-left",
178
+ "top-right",
179
+ "top-left",
180
+ ],
181
+ min: {
182
+ width: resizeOptions.minWidth || 100,
183
+ height: resizeOptions.minHeight || 100,
184
+ },
185
+ max: maxWidth && maxHeight ? { width: maxWidth, height: maxHeight } : undefined,
186
+ preserveAspectRatio: true,
187
+ className: {
188
+ container: "tiptap-resize-container",
189
+ wrapper: "tiptap-resize-wrapper",
190
+ handle: "tiptap-resize-handle",
191
+ resizing: "is-resizing",
192
+ },
193
+ },
194
+ });
195
+
196
+ figure.appendChild(resizableView.dom);
197
+
198
+ if (attrs.caption) {
199
+ const figcaption = document.createElement("figcaption");
200
+ figcaption.textContent = attrs.caption;
201
+ figure.appendChild(figcaption);
202
+ }
203
+
204
+ return {
205
+ dom: figure,
206
+ contentDOM: resizableView.contentDOM,
207
+ update: resizableView.update.bind(resizableView),
208
+ destroy: resizableView.destroy?.bind(resizableView),
209
+ };
210
+ };
9
211
  },
10
212
  });
11
213
 
@@ -61,8 +61,14 @@ export function getEditorExtensions(options: GetExtensionsOptions) {
61
61
  PromptContent,
62
62
  Video,
63
63
 
64
- // Configure with optional getImageById for URL resolution and onFigureClick callback
65
- Figure.configure({ getImageById, onFigureClick }),
64
+ Figure.configure({
65
+ getImageById,
66
+ onFigureClick,
67
+ resize: {
68
+ enabled: true,
69
+ directions: ["top-left", "top-right", "bottom-left", "bottom-right"],
70
+ },
71
+ }),
66
72
  Mathematics.configure({
67
73
  katexOptions: {
68
74
  throwOnError: false,
@@ -1,25 +1 @@
1
- // export { Callout, BookAlert, BookTip } from "./callout";
2
- // export { CodeBlock } from "./code-block";
3
- // export { Commands } from "./commands";
4
- // export { CopyableText } from "./copyable-text";
5
- // export {
6
- // CustomQuote,
7
- // CustomQuoteText,
8
- // CustomQuoteAuthor,
9
- // CustomQuoteSign,
10
- // CustomBlockquote,
11
- // CustomQuoteSE,
12
- // } from "./custom-quote";
13
- // export { Figure, LegacyFigure, type FigureAttrs } from "./figure";
14
- // export { InlineLatexSE } from "./inline-latex";
15
- // export {
16
- // Prompt,
17
- // PromptHeader,
18
- // PromptContent,
19
- // PromptSE,
20
- // PromptSEHeader,
21
- // PromptSEContent,
22
- // } from "./prompt";
23
- // export { Video, YouTube } from "./video";
24
-
25
1
  export { getEditorExtensions } from "./get-editor-extensions";
@@ -46,9 +46,8 @@ export function getRenderExtensions() {
46
46
  BaseVideo,
47
47
  BaseFigure,
48
48
 
49
- // Need to update
50
49
  TableKit.configure({
51
- table: { resizable: false },
50
+ table: { resizable: false, renderWrapper: true },
52
51
  }),
53
52
 
54
53
  Mathematics.configure({
@@ -4,5 +4,4 @@ export { CopyableText } from "./copyable-text";
4
4
  export { CustomQuote, CustomQuoteText, CustomQuoteAuthor, CustomQuoteSign } from "./custom-quote";
5
5
  export { Figure } from "./figure";
6
6
  export { PromptHeader, PromptContent } from "./prompt";
7
- export { Table } from "./table";
8
7
  export { Video } from "./video";
@@ -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, storageKey } = node.attrs as FigureAttrs;
9
+ const { fileId, src, alt, title, caption, width, height } = 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;
@@ -20,3 +20,4 @@
20
20
  */
21
21
 
22
22
  export { TiptapRender } from "./tiptap-render";
23
+ export { htmlToTiptapContent } from "../utils/html-to-tiptap-content";
@@ -24,7 +24,6 @@ import {
24
24
  CustomQuoteSign,
25
25
  PromptHeader,
26
26
  PromptContent,
27
- Table,
28
27
  Video,
29
28
  Figure,
30
29
  } from "@/extensions/views/components";
@@ -95,9 +94,6 @@ export const getNodeMapping = (getImageById?: (fileId: string) => string | null)
95
94
  video({ node }: any) {
96
95
  return <Video src={node.attrs.src} />;
97
96
  },
98
- table({ children }: any) {
99
- return <Table>{children}</Table>;
100
- },
101
97
  figure({ node }: any) {
102
98
  return (
103
99
  <Figure
@@ -2,12 +2,14 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  align-items: center;
5
+ max-width: 100%;
5
6
  margin: 24px auto;
6
7
  }
7
8
 
8
9
  .figure-image-container {
9
10
  display: inline-block;
10
11
  position: relative;
12
+ max-width: 100%;
11
13
  }
12
14
 
13
15
  .figure-image-container img {
@@ -17,6 +19,12 @@
17
19
  border-radius: 12px;
18
20
  }
19
21
 
22
+ [data-resize-wrapper] .figure-image-container img {
23
+ width: 100%;
24
+ height: 100%;
25
+ object-fit: contain;
26
+ }
27
+
20
28
  .figure-edit-button {
21
29
  display: flex;
22
30
  position: absolute;
@@ -64,3 +72,72 @@
64
72
  letter-spacing: -0.32px;
65
73
  text-align: center;
66
74
  }
75
+
76
+ .tiptap-resize-container {
77
+ display: inline-block;
78
+ position: relative;
79
+ justify-content: center;
80
+ max-width: 100%;
81
+ margin: 0 auto;
82
+ }
83
+
84
+ .tiptap-resize-wrapper {
85
+ display: inline-block;
86
+ position: relative;
87
+ margin: 0 auto;
88
+ }
89
+
90
+ [data-resize-handle] {
91
+ z-index: 10;
92
+ position: absolute;
93
+ background: var(--tiptap-color-primary, #3b82f6);
94
+ opacity: 0;
95
+ transition: opacity 0.2s ease;
96
+ }
97
+
98
+ [data-resize-container]:hover [data-resize-handle],
99
+ [data-resize-container].is-resizing [data-resize-handle] {
100
+ opacity: 1;
101
+ }
102
+
103
+ [data-resize-handle="top-left"],
104
+ [data-resize-handle="top-right"],
105
+ [data-resize-handle="bottom-left"],
106
+ [data-resize-handle="bottom-right"] {
107
+ width: 10px;
108
+ height: 10px;
109
+ border: 1px solid white;
110
+ border-radius: 50%;
111
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
112
+ }
113
+
114
+ [data-resize-handle="top-left"] {
115
+ top: -4px !important;
116
+ left: -4px !important;
117
+ cursor: nwse-resize;
118
+ }
119
+
120
+ [data-resize-handle="top-right"] {
121
+ top: -4px !important;
122
+ right: -4px !important;
123
+ cursor: nesw-resize;
124
+ }
125
+
126
+ [data-resize-handle="bottom-left"] {
127
+ left: -4px !important;
128
+ cursor: nesw-resize;
129
+ }
130
+
131
+ [data-resize-handle="bottom-right"] {
132
+ right: -4px !important;
133
+ cursor: nwse-resize;
134
+ }
135
+
136
+ [data-resize-container].is-resizing {
137
+ user-select: none;
138
+ }
139
+
140
+ [data-resize-container].is-resizing [data-resize-handle] {
141
+ background: var(--tiptap-color-primary-active, #2563eb);
142
+ opacity: 1;
143
+ }