tetrons 2.3.21 → 2.3.23

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 (29) hide show
  1. package/dist/components/UI/Button.tsx +0 -0
  2. package/dist/components/UI/Dropdown.tsx +0 -0
  3. package/dist/components/tetrons/EditorContent.tsx +282 -0
  4. package/dist/components/tetrons/ResizableImage.ts +39 -0
  5. package/dist/components/tetrons/ResizableImageComponent.tsx +77 -0
  6. package/dist/components/tetrons/ResizableVideo.ts +66 -0
  7. package/dist/components/tetrons/ResizableVideoComponent.tsx +56 -0
  8. package/dist/components/tetrons/helpers.ts +0 -0
  9. package/dist/components/tetrons/toolbar/ActionGroup.tsx +218 -0
  10. package/dist/components/tetrons/toolbar/ClipboardGroup.tsx +58 -0
  11. package/dist/components/tetrons/toolbar/FileGroup.tsx +66 -0
  12. package/dist/components/tetrons/toolbar/FontStyleGroup.tsx +194 -0
  13. package/dist/components/tetrons/toolbar/InsertGroup.tsx +267 -0
  14. package/dist/components/tetrons/toolbar/ListAlignGroup.tsx +69 -0
  15. package/dist/components/tetrons/toolbar/MiscGroup.tsx +71 -0
  16. package/dist/components/tetrons/toolbar/TableContextMenu.tsx +91 -0
  17. package/dist/components/tetrons/toolbar/TetronsToolbar.tsx +71 -0
  18. package/dist/components/tetrons/toolbar/ToolbarButton.tsx +36 -0
  19. package/dist/components/tetrons/toolbar/extensions/Comment.ts +72 -0
  20. package/dist/components/tetrons/toolbar/extensions/Embed.ts +113 -0
  21. package/dist/components/tetrons/toolbar/extensions/FontFamily.ts +43 -0
  22. package/dist/components/tetrons/toolbar/extensions/FontSize.ts +43 -0
  23. package/dist/components/tetrons/toolbar/extensions/ResizableTable.ts +16 -0
  24. package/dist/components/tetrons/toolbar/marks/Subscript.ts +45 -0
  25. package/dist/components/tetrons/toolbar/marks/Superscript.ts +45 -0
  26. package/dist/index.js +0 -1
  27. package/dist/index.mjs +0 -1
  28. package/package.json +7 -7
  29. package/dist/tetrons-UCHWNATC.css +0 -372
@@ -0,0 +1,218 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef, useState } from "react";
4
+ import { Editor } from "@tiptap/react";
5
+ import {
6
+ MdZoomIn,
7
+ MdZoomOut,
8
+ MdPrint,
9
+ MdSave,
10
+ MdDownload,
11
+ } from "react-icons/md";
12
+ import ToolbarButton from "./ToolbarButton";
13
+
14
+ type ActionGroupProps = {
15
+ editor: Editor;
16
+ };
17
+
18
+ export default function ActionGroup({ editor }: ActionGroupProps) {
19
+ const [dropdownOpen, setDropdownOpen] = useState(false);
20
+ const dropdownRef = useRef<HTMLDivElement>(null);
21
+
22
+ useEffect(() => {
23
+ const handleClickOutside = (event: MouseEvent) => {
24
+ if (
25
+ dropdownRef.current &&
26
+ !dropdownRef.current.contains(event.target as Node)
27
+ ) {
28
+ setDropdownOpen(false);
29
+ }
30
+ };
31
+
32
+ if (dropdownOpen) {
33
+ document.addEventListener("mousedown", handleClickOutside);
34
+ } else {
35
+ document.removeEventListener("mousedown", handleClickOutside);
36
+ }
37
+
38
+ return () => {
39
+ document.removeEventListener("mousedown", handleClickOutside);
40
+ };
41
+ }, [dropdownOpen]);
42
+
43
+ const zoomIn = () => {
44
+ const element = document.querySelector(
45
+ ".ProseMirror"
46
+ ) as HTMLElement | null;
47
+ if (element) {
48
+ const style = element.style as ZoomableStyle;
49
+ const currentZoom = parseFloat(style.zoom || "1");
50
+ const next = Math.min(currentZoom + 0.1, 2);
51
+ style.zoom = next.toString();
52
+ }
53
+ };
54
+
55
+ const zoomOut = () => {
56
+ const element = document.querySelector(
57
+ ".ProseMirror"
58
+ ) as HTMLElement | null;
59
+ if (element) {
60
+ const style = element.style as ZoomableStyle;
61
+ const currentZoom = parseFloat(style.zoom || "1");
62
+ const next = Math.max(currentZoom - 0.1, 0.5);
63
+ style.zoom = next.toString();
64
+ }
65
+ };
66
+
67
+ const handlePrint = () => {
68
+ const html = editor.getHTML();
69
+ const printWindow = window.open("", "_blank");
70
+ if (printWindow) {
71
+ printWindow.document.write(`
72
+ <html>
73
+ <head>
74
+ <title>Print</title>
75
+ </head>
76
+ <body>${html}</body>
77
+ </html>
78
+ `);
79
+ printWindow.document.close();
80
+ printWindow.focus();
81
+ printWindow.print();
82
+ }
83
+ };
84
+
85
+ const handleSave = async () => {
86
+ const content = editor.getJSON();
87
+ try {
88
+ const response = await fetch("/api/save", {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify(content),
92
+ });
93
+ if (!response.ok) throw new Error("Failed to save file");
94
+
95
+ const result = await response.json();
96
+ console.log(result.message);
97
+ alert("Content saved successfully!");
98
+ } catch (error) {
99
+ console.error("Error saving content:", error);
100
+ alert("Failed to save content.");
101
+ }
102
+ };
103
+
104
+ const handleDownloadPDF = async () => {
105
+ const html = editor.getHTML();
106
+ const container = document.createElement("div");
107
+ container.innerHTML = html;
108
+ container.classList.add("p-4", "prose");
109
+ document.body.appendChild(container);
110
+
111
+ try {
112
+ const domToPdf = (await import("dom-to-pdf")).default;
113
+ const options = {
114
+ filename: "document.pdf",
115
+ overrideWidth: 800,
116
+ overrideHeight: 1120,
117
+ };
118
+
119
+ domToPdf(container, options, () => {
120
+ console.log("PDF downloaded!");
121
+ });
122
+ } catch (error) {
123
+ console.error("PDF download failed", error);
124
+ alert("Failed to download PDF.");
125
+ } finally {
126
+ document.body.removeChild(container);
127
+ }
128
+ };
129
+
130
+ const handleDownloadHTML = () => {
131
+ const html = editor.getHTML();
132
+ const blob = new Blob([html], { type: "text/html" });
133
+ const link = document.createElement("a");
134
+ link.href = URL.createObjectURL(blob);
135
+ link.download = "document.html";
136
+ document.body.appendChild(link);
137
+ link.click();
138
+ document.body.removeChild(link);
139
+ };
140
+
141
+ const handleDownloadDOCX = async () => {
142
+ const { Document, Packer, Paragraph } = await import("docx");
143
+ const text = editor.getText();
144
+
145
+ const doc = new Document({
146
+ sections: [
147
+ {
148
+ properties: {},
149
+ children: [new Paragraph(text)],
150
+ },
151
+ ],
152
+ });
153
+
154
+ const blob = await Packer.toBlob(doc);
155
+ const link = document.createElement("a");
156
+ link.href = URL.createObjectURL(blob);
157
+ link.download = "document.docx";
158
+ document.body.appendChild(link);
159
+ link.click();
160
+ document.body.removeChild(link);
161
+ };
162
+
163
+ return (
164
+ <div className="action-group" role="group" aria-label="Editor actions">
165
+ <ToolbarButton icon={MdZoomIn} onClick={zoomIn} title="Zoom In" />
166
+ <ToolbarButton icon={MdZoomOut} onClick={zoomOut} title="Zoom Out" />
167
+ <ToolbarButton icon={MdPrint} onClick={handlePrint} title="Print" />
168
+ <ToolbarButton icon={MdSave} onClick={handleSave} title="Save" />
169
+
170
+ <div className="relative" ref={dropdownRef}>
171
+ <button
172
+ type="button"
173
+ onClick={() => setDropdownOpen((open) => !open)}
174
+ aria-haspopup="menu"
175
+ aria-expanded={dropdownOpen ? "true" : "false"}
176
+ className="export-button"
177
+ title="Export"
178
+ >
179
+ <MdDownload />
180
+ <span className="text-sm"></span>
181
+ </button>
182
+
183
+ {dropdownOpen && (
184
+ <div className="export-dropdown">
185
+ <button
186
+ type="button"
187
+ onClick={() => {
188
+ setDropdownOpen(false);
189
+ handleDownloadPDF();
190
+ }}
191
+ >
192
+ Export as PDF
193
+ </button>
194
+ <button
195
+ type="button"
196
+ onClick={() => {
197
+ setDropdownOpen(false);
198
+ handleDownloadHTML();
199
+ }}
200
+ >
201
+ Export as HTML
202
+ </button>
203
+ <button
204
+ type="button"
205
+ onClick={() => {
206
+ setDropdownOpen(false);
207
+ handleDownloadDOCX();
208
+ }}
209
+ >
210
+ Export as DOCX
211
+ </button>
212
+ </div>
213
+ )}
214
+ </div>
215
+ </div>
216
+ );
217
+
218
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ MdContentPaste,
3
+ MdContentCut,
4
+ MdContentCopy,
5
+ MdFormatPaint,
6
+ } from "react-icons/md";
7
+ import React from "react";
8
+ import { Editor } from "@tiptap/react";
9
+ import ToolbarButton from "./ToolbarButton";
10
+
11
+ export default function ClipboardGroup({ editor }: { editor: Editor }) {
12
+ return (
13
+ <div className="clipboard-group">
14
+ <ToolbarButton
15
+ icon={MdContentPaste}
16
+ title="Paste"
17
+ onClick={async () => {
18
+ try {
19
+ const text = await navigator.clipboard.readText();
20
+ editor.chain().focus().insertContent(text).run();
21
+ } catch (error) {
22
+ console.error("Failed to read clipboard contents:", error);
23
+ }
24
+ }}
25
+ />
26
+ <ToolbarButton
27
+ icon={MdContentCut}
28
+ title="Cut"
29
+ onClick={() => {
30
+ const { from, to } = editor.state.selection;
31
+ if (from === to) return;
32
+ const selectedText = editor.state.doc.textBetween(from, to);
33
+ navigator.clipboard.writeText(selectedText).then(() => {
34
+ editor.chain().focus().deleteRange({ from, to }).run();
35
+ });
36
+ }}
37
+ />
38
+ <ToolbarButton
39
+ icon={MdContentCopy}
40
+ title="Copy"
41
+ onClick={() => {
42
+ const { from, to } = editor.state.selection;
43
+ if (from === to) return;
44
+ const selectedText = editor.state.doc.textBetween(from, to);
45
+ navigator.clipboard.writeText(selectedText);
46
+ }}
47
+ />
48
+ <ToolbarButton
49
+ icon={MdFormatPaint}
50
+ title="Format Painter"
51
+ onClick={() => {
52
+ const currentMarks = editor.getAttributes("textStyle");
53
+ localStorage.setItem("formatPainter", JSON.stringify(currentMarks));
54
+ }}
55
+ />
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { Editor } from "@tiptap/react";
4
+ import { FaRegFolderOpen } from "react-icons/fa";
5
+ import { VscNewFile } from "react-icons/vsc";
6
+ import ToolbarButton from "./ToolbarButton";
7
+ import React, { useRef } from "react";
8
+
9
+ type FileGroupProps = {
10
+ editor: Editor;
11
+ };
12
+
13
+ export default function FileGroup({ editor }: FileGroupProps) {
14
+ const fileInputRef = useRef<HTMLInputElement>(null);
15
+
16
+ const handleNew = () => {
17
+ if (
18
+ confirm(
19
+ "Are you sure you want to create a new document? Unsaved changes will be lost."
20
+ )
21
+ ) {
22
+ editor.commands.clearContent();
23
+ }
24
+ };
25
+
26
+ const handleOpen = () => {
27
+ fileInputRef.current?.click();
28
+ };
29
+
30
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
31
+ const file = e.target.files?.[0];
32
+ if (!file) return;
33
+
34
+ try {
35
+ const text = await file.text();
36
+ const json = JSON.parse(text);
37
+ editor.commands.setContent(json);
38
+ } catch (error) {
39
+ console.error("Error reading file:", error);
40
+ alert(
41
+ "Failed to open or parse the file. Make sure it's a valid JSON file."
42
+ );
43
+ } finally {
44
+ e.target.value = "";
45
+ }
46
+ };
47
+
48
+ return (
49
+ <div className="file-group" role="group" aria-label="File actions">
50
+ <input
51
+ type="file"
52
+ accept=".json"
53
+ ref={fileInputRef}
54
+ onChange={handleFileChange}
55
+ className="hidden"
56
+ aria-label="Open JSON file"
57
+ />
58
+ <ToolbarButton icon={VscNewFile} onClick={handleNew} title="New" />
59
+ <ToolbarButton
60
+ icon={FaRegFolderOpen}
61
+ onClick={handleOpen}
62
+ title="Open File"
63
+ />
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ import {
4
+ MdFormatBold,
5
+ MdFormatItalic,
6
+ MdFormatUnderlined,
7
+ MdStrikethroughS,
8
+ MdSubscript,
9
+ MdSuperscript,
10
+ MdFormatClear,
11
+ MdFormatPaint,
12
+ } from "react-icons/md";
13
+ import { ImTextColor } from "react-icons/im";
14
+ import { BiSolidColorFill } from "react-icons/bi";
15
+ import { Editor } from "@tiptap/react";
16
+ import ToolbarButton from "./ToolbarButton";
17
+ import React, { useEffect, useState } from "react";
18
+
19
+ interface FontStyleGroupProps {
20
+ editor: Editor;
21
+ }
22
+
23
+ export default function FontStyleGroup({ editor }: FontStyleGroupProps) {
24
+ const [textColor, setTextColor] = useState("#000000");
25
+ const [highlightColor, setHighlightColor] = useState("#ffff00");
26
+ const [fontFamily, setFontFamily] = useState("Arial");
27
+ const [fontSize, setFontSize] = useState("16px");
28
+
29
+ useEffect(() => {
30
+ if (!editor) return;
31
+
32
+ const updateStates = () => {
33
+ const highlight = editor.getAttributes("highlight");
34
+ setHighlightColor(highlight?.color || "#ffff00");
35
+
36
+ const color = editor.getAttributes("textStyle")?.color;
37
+ setTextColor(color || "#000000");
38
+
39
+ const fontAttr = editor.getAttributes("fontFamily")?.font || "Arial";
40
+ setFontFamily(fontAttr);
41
+
42
+ const sizeAttr = editor.getAttributes("fontSize")?.size || "16px";
43
+ setFontSize(sizeAttr);
44
+ };
45
+
46
+ updateStates();
47
+
48
+ editor.on("selectionUpdate", updateStates);
49
+ editor.on("transaction", updateStates);
50
+
51
+ return () => {
52
+ editor.off("selectionUpdate", updateStates);
53
+ editor.off("transaction", updateStates);
54
+ };
55
+ }, [editor]);
56
+
57
+ return (
58
+ <div className="font-style-group">
59
+ <select
60
+ title="Font Family"
61
+ value={fontFamily}
62
+ onChange={(e) => {
63
+ const value = e.target.value;
64
+ setFontFamily(value);
65
+ editor.chain().focus().setFontFamily(value).run();
66
+ }}
67
+ >
68
+ <option value="Arial">Arial</option>
69
+ <option value="Georgia">Georgia</option>
70
+ <option value="Times New Roman">Times New Roman</option>
71
+ <option value="Courier New">Courier New</option>
72
+ <option value="Verdana">Verdana</option>
73
+ </select>
74
+
75
+ <select
76
+ title="Font Size"
77
+ value={fontSize}
78
+ onChange={(e) => {
79
+ const value = e.target.value;
80
+ setFontSize(value);
81
+ editor.chain().focus().setFontSize(value).run();
82
+ }}
83
+ >
84
+ <option value="12px">12</option>
85
+ <option value="14px">14</option>
86
+ <option value="16px">16</option>
87
+ <option value="18px">18</option>
88
+ <option value="24px">24</option>
89
+ <option value="36px">36</option>
90
+ <option value="48px">48</option>
91
+ <option value="64px">64</option>
92
+ <option value="72px">72</option>
93
+ </select>
94
+
95
+ <ToolbarButton
96
+ icon={MdFormatBold}
97
+ label="Bold"
98
+ onClick={() => editor.chain().focus().toggleBold().run()}
99
+ isActive={editor.isActive("bold")}
100
+ />
101
+ <ToolbarButton
102
+ icon={MdFormatItalic}
103
+ label="Italic"
104
+ onClick={() => editor.chain().focus().toggleItalic().run()}
105
+ isActive={editor.isActive("italic")}
106
+ />
107
+ <ToolbarButton
108
+ icon={MdFormatUnderlined}
109
+ label="Underline"
110
+ onClick={() => editor.chain().focus().toggleUnderline().run()}
111
+ isActive={editor.isActive("underline")}
112
+ />
113
+ <ToolbarButton
114
+ icon={MdStrikethroughS}
115
+ label="Strikethrough"
116
+ onClick={() => editor.chain().focus().toggleStrike().run()}
117
+ isActive={editor.isActive("strike")}
118
+ />
119
+ <ToolbarButton
120
+ icon={MdSubscript}
121
+ label="Subscript"
122
+ onClick={() => editor.chain().focus().toggleSubscript().run()}
123
+ isActive={editor.isActive("subscript")}
124
+ />
125
+ <ToolbarButton
126
+ icon={MdSuperscript}
127
+ label="Superscript"
128
+ onClick={() => editor.chain().focus().toggleSuperscript().run()}
129
+ isActive={editor.isActive("superscript")}
130
+ />
131
+
132
+ <label
133
+ title="Font Color"
134
+ aria-label="Font Color"
135
+ className="color-label"
136
+ style={{ "--indicator-color": textColor } as React.CSSProperties}
137
+ >
138
+ <ImTextColor size={20} />
139
+ <div className="color-indicator" />
140
+ <input
141
+ type="color"
142
+ value={textColor}
143
+ onChange={(e) => {
144
+ const color = e.target.value;
145
+ setTextColor(color);
146
+ editor.chain().focus().setColor(color).run();
147
+ }}
148
+ />
149
+ </label>
150
+
151
+ <label
152
+ title="Highlight Color"
153
+ aria-label="Highlight Color"
154
+ className="color-label"
155
+ style={{ "--indicator-color": highlightColor } as React.CSSProperties}
156
+ >
157
+ <BiSolidColorFill size={20} />
158
+ <div className="color-indicator" />
159
+ <input
160
+ type="color"
161
+ value={highlightColor}
162
+ onChange={(e) => {
163
+ const color = e.target.value;
164
+ setHighlightColor(color);
165
+ editor.chain().focus().setHighlight({ color }).run();
166
+ }}
167
+ />
168
+ </label>
169
+
170
+ <ToolbarButton
171
+ icon={MdFormatClear}
172
+ label="Clear Formatting"
173
+ onClick={() => editor.chain().focus().unsetAllMarks().run()}
174
+ />
175
+ <ToolbarButton
176
+ icon={MdFormatPaint}
177
+ label="Apply Painter Format"
178
+ onClick={() => {
179
+ const format = JSON.parse(
180
+ localStorage.getItem("formatPainter") || "{}"
181
+ );
182
+ if (format.color) editor.chain().focus().setColor(format.color).run();
183
+ if (format.backgroundColor) {
184
+ editor
185
+ .chain()
186
+ .focus()
187
+ .setHighlight({ color: format.backgroundColor })
188
+ .run();
189
+ }
190
+ }}
191
+ />
192
+ </div>
193
+ );
194
+ }