@cyberyozh/text-editor 0.1.11 → 0.1.12

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.12",
4
4
  "description": "Rich text editor library based on Tiptap for CyberYozh",
5
5
  "author": "CyberYozh",
6
6
  "license": "MIT",
@@ -67,15 +67,37 @@ export const ImageDialog = ({
67
67
  img.src = imageUrl;
68
68
 
69
69
  img.onload = () => {
70
- const { width, height } = img;
70
+ const naturalWidth = img.width;
71
+ const naturalHeight = img.height;
72
+ const naturalAspectRatio = naturalHeight / naturalWidth;
73
+
74
+ let maxWidth: number | undefined;
75
+ try {
76
+ const editorElement = document.querySelector(".tiptap-content-wrapper") as HTMLElement;
77
+ if (editorElement && editorElement.clientWidth > 0) {
78
+ maxWidth = editorElement.clientWidth - 32;
79
+ }
80
+ } catch {}
81
+
82
+ let finalWidth: number;
83
+ let finalHeight: number;
84
+
85
+ if (maxWidth && naturalWidth > maxWidth) {
86
+ finalWidth = maxWidth;
87
+ finalHeight = Math.round(maxWidth * naturalAspectRatio);
88
+ } else {
89
+ finalWidth = naturalWidth;
90
+ finalHeight = naturalHeight;
91
+ }
92
+
71
93
  setFormState((prev) => ({
72
94
  ...prev,
73
95
  file,
74
96
  fileUrl: imageUrl,
75
97
  title: file.name,
76
98
  alt: file.name,
77
- width,
78
- height,
99
+ width: finalWidth,
100
+ height: finalHeight,
79
101
  }));
80
102
  };
81
103
  };
@@ -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,194 @@
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
+
84
+ imageContainer.appendChild(img);
85
+
86
+ if (editor.isEditable) {
87
+ const editButton = document.createElement("button");
88
+ editButton.type = "button";
89
+ editButton.className = "figure-edit-button";
90
+ editButton.contentEditable = "false";
91
+ editButton.innerHTML =
92
+ '<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>';
93
+
94
+ editButton.addEventListener("click", () => {
95
+ const onFigureClick = imageExtension?.options.onFigureClick;
96
+ const pos = getPos();
97
+ if (onFigureClick && typeof pos === "number") {
98
+ onFigureClick(attrs, pos);
99
+ }
100
+ });
101
+
102
+ imageContainer.appendChild(editButton);
103
+ }
104
+
105
+ const resizableView = new ResizableNodeView({
106
+ element: imageContainer,
107
+ node,
108
+ getPos,
109
+ onResize: (w, h) => {
110
+ const { width, height } = constrainDimensions(w, h);
111
+ updateImageDimensions(width, height);
112
+ },
113
+ onCommit: (w, h) => {
114
+ const { width, height } = constrainDimensions(w, h);
115
+ const pos = getPos();
116
+
117
+ if (typeof pos === "number") {
118
+ updateImageDimensions(Math.round(width), Math.round(height));
119
+
120
+ editor
121
+ .chain()
122
+ .focus()
123
+ .setNodeSelection(pos)
124
+ .updateAttributes("image", {
125
+ width: Math.round(width),
126
+ height: Math.round(height),
127
+ })
128
+ .run();
129
+ }
130
+ },
131
+ onUpdate: (updatedNode) => {
132
+ if (updatedNode.attrs.width) {
133
+ imageContainer.style.width = `${updatedNode.attrs.width}px`;
134
+ }
135
+ if (updatedNode.attrs.height) {
136
+ imageContainer.style.height = `${updatedNode.attrs.height}px`;
137
+ }
138
+
139
+ const existingCaption = figure.querySelector("figcaption");
140
+ if (updatedNode.attrs.caption && !existingCaption) {
141
+ const figcaption = document.createElement("figcaption");
142
+ figcaption.textContent = updatedNode.attrs.caption;
143
+ figure.appendChild(figcaption);
144
+ } else if (existingCaption) {
145
+ if (updatedNode.attrs.caption) {
146
+ existingCaption.textContent = updatedNode.attrs.caption;
147
+ } else {
148
+ existingCaption.remove();
149
+ }
150
+ }
151
+
152
+ if (updatedNode.type !== node.type) return false;
153
+ return true;
154
+ },
155
+ options: {
156
+ directions: resizeOptions.directions || [
157
+ "bottom-right",
158
+ "bottom-left",
159
+ "top-right",
160
+ "top-left",
161
+ ],
162
+ min: {
163
+ width: resizeOptions.minWidth || 100,
164
+ height: resizeOptions.minHeight || 100,
165
+ },
166
+ max: maxWidth && maxHeight ? { width: maxWidth, height: maxHeight } : undefined,
167
+ preserveAspectRatio: true,
168
+ className: {
169
+ container: "tiptap-resize-container",
170
+ wrapper: "tiptap-resize-wrapper",
171
+ handle: "tiptap-resize-handle",
172
+ resizing: "is-resizing",
173
+ },
174
+ },
175
+ });
176
+
177
+ figure.appendChild(resizableView.dom);
178
+
179
+ if (attrs.caption) {
180
+ const figcaption = document.createElement("figcaption");
181
+ figcaption.textContent = attrs.caption;
182
+ figure.appendChild(figcaption);
183
+ }
184
+
185
+ return {
186
+ dom: figure,
187
+ contentDOM: resizableView.contentDOM,
188
+ update: resizableView.update.bind(resizableView),
189
+ destroy: resizableView.destroy?.bind(resizableView),
190
+ };
191
+ };
9
192
  },
10
193
  });
11
194
 
@@ -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;
@@ -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
+ }
@@ -1,75 +0,0 @@
1
- import { useEffect, useRef, useState, type ReactNode } from "react";
2
-
3
- interface TableProps {
4
- children: ReactNode;
5
- }
6
-
7
- /**
8
- * Table component for static rendering
9
- * Applies column widths from Tiptap table cells
10
- */
11
- export function Table({ children }: TableProps) {
12
- const wrapperRef = useRef<HTMLDivElement>(null);
13
- const [undefinedColWidth, setUndefinedColWidth] = useState<number | undefined>(undefined);
14
-
15
- let colWidths: (number | undefined)[] = [];
16
-
17
- if (Array.isArray(children) && children.length > 0) {
18
- const firstRow = children[0];
19
-
20
- if (firstRow?.props?.children) {
21
- const cells = Array.isArray(firstRow.props.children)
22
- ? firstRow.props.children
23
- : [firstRow.props.children];
24
-
25
- colWidths = cells.map((cell: any) => cell?.props?.node?.attrs?.colwidth?.[0]);
26
- }
27
- }
28
-
29
- const definedWidths = colWidths.filter((w): w is number => w !== undefined);
30
- const totalDefinedWidth = definedWidths.reduce((total, width) => total + width, 0);
31
- const undefinedCount = colWidths.filter((w) => w === undefined).length;
32
-
33
- useEffect(() => {
34
- if (wrapperRef.current && undefinedCount > 0) {
35
- const containerWidth = wrapperRef.current.clientWidth;
36
- const remainingSpace = containerWidth - totalDefinedWidth - 2;
37
- const calculatedWidth = Math.floor(remainingSpace / undefinedCount);
38
- setUndefinedColWidth(calculatedWidth > 0 ? calculatedWidth : undefined);
39
- }
40
- }, [totalDefinedWidth, undefinedCount]);
41
-
42
- const tableWidth =
43
- undefinedCount > 0 && undefinedColWidth
44
- ? totalDefinedWidth + undefinedColWidth * undefinedCount + 2
45
- : totalDefinedWidth > 0
46
- ? totalDefinedWidth + 2
47
- : undefined;
48
-
49
- return (
50
- <div
51
- ref={wrapperRef}
52
- className="tableWrapper"
53
- >
54
- <table style={tableWidth ? { width: `${tableWidth}px` } : undefined}>
55
- {colWidths.length > 0 && (
56
- <colgroup>
57
- {colWidths.map((width, index) => (
58
- <col
59
- key={index}
60
- style={
61
- width
62
- ? { width: `${width}px` }
63
- : undefinedColWidth
64
- ? { width: `${undefinedColWidth}px` }
65
- : undefined
66
- }
67
- />
68
- ))}
69
- </colgroup>
70
- )}
71
- <tbody>{children}</tbody>
72
- </table>
73
- </div>
74
- );
75
- }