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.
- package/dist/components/UI/Button.tsx +0 -0
- package/dist/components/UI/Dropdown.tsx +0 -0
- package/dist/components/tetrons/EditorContent.tsx +282 -0
- package/dist/components/tetrons/ResizableImage.ts +39 -0
- package/dist/components/tetrons/ResizableImageComponent.tsx +77 -0
- package/dist/components/tetrons/ResizableVideo.ts +66 -0
- package/dist/components/tetrons/ResizableVideoComponent.tsx +56 -0
- package/dist/components/tetrons/helpers.ts +0 -0
- package/dist/components/tetrons/toolbar/ActionGroup.tsx +218 -0
- package/dist/components/tetrons/toolbar/ClipboardGroup.tsx +58 -0
- package/dist/components/tetrons/toolbar/FileGroup.tsx +66 -0
- package/dist/components/tetrons/toolbar/FontStyleGroup.tsx +194 -0
- package/dist/components/tetrons/toolbar/InsertGroup.tsx +267 -0
- package/dist/components/tetrons/toolbar/ListAlignGroup.tsx +69 -0
- package/dist/components/tetrons/toolbar/MiscGroup.tsx +71 -0
- package/dist/components/tetrons/toolbar/TableContextMenu.tsx +91 -0
- package/dist/components/tetrons/toolbar/TetronsToolbar.tsx +71 -0
- package/dist/components/tetrons/toolbar/ToolbarButton.tsx +36 -0
- package/dist/components/tetrons/toolbar/extensions/Comment.ts +72 -0
- package/dist/components/tetrons/toolbar/extensions/Embed.ts +113 -0
- package/dist/components/tetrons/toolbar/extensions/FontFamily.ts +43 -0
- package/dist/components/tetrons/toolbar/extensions/FontSize.ts +43 -0
- package/dist/components/tetrons/toolbar/extensions/ResizableTable.ts +16 -0
- package/dist/components/tetrons/toolbar/marks/Subscript.ts +45 -0
- package/dist/components/tetrons/toolbar/marks/Superscript.ts +45 -0
- package/dist/index.js +0 -1
- package/dist/index.mjs +0 -1
- package/package.json +7 -7
- 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
|
+
}
|