tetrons 2.3.22 ā 2.3.24
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/app/page.d.ts +2 -0
- package/dist/app/page.jsx +51 -0
- package/dist/components/UI/Button.tsx +0 -0
- package/dist/components/UI/Dropdown.tsx +0 -0
- package/dist/components/components/tetrons/EditorContent.tsx +60 -62
- package/dist/components/tetrons/EditorContent.d.ts +6 -0
- package/dist/components/tetrons/EditorContent.tsx +280 -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/extensions/Spellcheck.ts +50 -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.d.ts +7 -0
- package/dist/components/tetrons/toolbar/MiscGroup.jsx +55 -0
- package/dist/components/tetrons/toolbar/MiscGroup.tsx +104 -0
- package/dist/components/tetrons/toolbar/TableContextMenu.tsx +91 -0
- package/dist/components/tetrons/toolbar/TetronsToolbar.d.ts +6 -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.d.ts +1 -1
- package/dist/index.js +17366 -22
- package/dist/index.mjs +4705 -4592
- package/dist/styles/styles/tetrons.css +1 -1
- package/dist/styles/tetrons.css +1 -1
- package/dist/utils/checkGrammar.d.ts +25 -0
- package/dist/utils/checkGrammar.js +17 -0
- package/package.json +9 -9
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
MdTableChart,
|
|
6
|
+
MdInsertPhoto,
|
|
7
|
+
MdInsertLink,
|
|
8
|
+
MdInsertComment,
|
|
9
|
+
MdInsertEmoticon,
|
|
10
|
+
MdHorizontalRule,
|
|
11
|
+
MdVideoLibrary,
|
|
12
|
+
MdOutlineOndemandVideo,
|
|
13
|
+
} from "react-icons/md";
|
|
14
|
+
import { Editor } from "@tiptap/react";
|
|
15
|
+
import ToolbarButton from "./ToolbarButton";
|
|
16
|
+
import Picker from "@emoji-mart/react";
|
|
17
|
+
|
|
18
|
+
type Emoji = {
|
|
19
|
+
native: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default function InsertGroup({ editor }: { editor: Editor }) {
|
|
23
|
+
const [showTableGrid, setShowTableGrid] = useState(false);
|
|
24
|
+
const [selectedRows, setSelectedRows] = useState(1);
|
|
25
|
+
const [selectedCols, setSelectedCols] = useState(1);
|
|
26
|
+
|
|
27
|
+
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
const videoInputRef = useRef<HTMLInputElement>(null);
|
|
29
|
+
|
|
30
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
31
|
+
|
|
32
|
+
const addEmoji = (emoji: Emoji) => {
|
|
33
|
+
editor.chain().focus().insertContent(emoji.native).run();
|
|
34
|
+
setShowPicker(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleTableCellHover = (row: number, col: number) => {
|
|
38
|
+
setSelectedRows(row);
|
|
39
|
+
setSelectedCols(col);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleTableInsert = (rows: number, cols: number) => {
|
|
43
|
+
editor
|
|
44
|
+
.chain()
|
|
45
|
+
.focus()
|
|
46
|
+
.insertTable({
|
|
47
|
+
rows: rows || 1,
|
|
48
|
+
cols: cols || 1,
|
|
49
|
+
withHeaderRow: true,
|
|
50
|
+
})
|
|
51
|
+
.run();
|
|
52
|
+
|
|
53
|
+
setShowTableGrid(false);
|
|
54
|
+
setSelectedRows(1);
|
|
55
|
+
setSelectedCols(1);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
59
|
+
const file = e.target.files?.[0];
|
|
60
|
+
if (file) {
|
|
61
|
+
const reader = new FileReader();
|
|
62
|
+
reader.onload = () => {
|
|
63
|
+
editor
|
|
64
|
+
.chain()
|
|
65
|
+
.focus()
|
|
66
|
+
.setImage({ src: reader.result as string })
|
|
67
|
+
.run();
|
|
68
|
+
};
|
|
69
|
+
reader.readAsDataURL(file);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleVideoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
74
|
+
const file = e.target.files?.[0];
|
|
75
|
+
if (file) {
|
|
76
|
+
const reader = new FileReader();
|
|
77
|
+
reader.onload = () => {
|
|
78
|
+
editor
|
|
79
|
+
.chain()
|
|
80
|
+
.focus()
|
|
81
|
+
.setVideo({ src: reader.result as string, controls: true })
|
|
82
|
+
.run();
|
|
83
|
+
};
|
|
84
|
+
reader.readAsDataURL(file);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function normalizeEmbedUrl(url: string): string {
|
|
89
|
+
try {
|
|
90
|
+
const u = new URL(url);
|
|
91
|
+
const hostname = u.hostname.replace("www.", "").toLowerCase();
|
|
92
|
+
if (hostname === "youtube.com" || hostname === "youtu.be") {
|
|
93
|
+
let videoId = u.searchParams.get("v");
|
|
94
|
+
if (!videoId && hostname === "youtu.be") {
|
|
95
|
+
videoId = u.pathname.slice(1);
|
|
96
|
+
}
|
|
97
|
+
if (videoId) {
|
|
98
|
+
return `https://www.youtube.com/embed/${videoId}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (hostname === "vimeo.com") {
|
|
102
|
+
const videoId = u.pathname.split("/")[1];
|
|
103
|
+
if (videoId) {
|
|
104
|
+
return `https://player.vimeo.com/video/${videoId}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
hostname === "google.com" ||
|
|
109
|
+
hostname === "maps.google.com" ||
|
|
110
|
+
hostname === "goo.gl"
|
|
111
|
+
) {
|
|
112
|
+
if (url.includes("/maps/embed")) {
|
|
113
|
+
return url;
|
|
114
|
+
}
|
|
115
|
+
return url.replace("/maps/", "/maps/embed/");
|
|
116
|
+
}
|
|
117
|
+
return url;
|
|
118
|
+
} catch {
|
|
119
|
+
return url;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="insert-group">
|
|
125
|
+
<input
|
|
126
|
+
type="file"
|
|
127
|
+
accept="image/*"
|
|
128
|
+
ref={imageInputRef}
|
|
129
|
+
className="hidden-input"
|
|
130
|
+
onChange={handleImageUpload}
|
|
131
|
+
aria-label="Upload Image"
|
|
132
|
+
title="Upload Image"
|
|
133
|
+
/>
|
|
134
|
+
<input
|
|
135
|
+
type="file"
|
|
136
|
+
accept="video/*"
|
|
137
|
+
ref={videoInputRef}
|
|
138
|
+
className="hidden-input"
|
|
139
|
+
onChange={handleVideoUpload}
|
|
140
|
+
aria-label="Upload Video"
|
|
141
|
+
title="Upload Video"
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<ToolbarButton
|
|
145
|
+
icon={MdTableChart}
|
|
146
|
+
label="Insert Table"
|
|
147
|
+
onClick={() => setShowTableGrid(!showTableGrid)}
|
|
148
|
+
/>
|
|
149
|
+
{showTableGrid && (
|
|
150
|
+
<div
|
|
151
|
+
className="table-grid-popup"
|
|
152
|
+
onMouseLeave={() => setShowTableGrid(false)}
|
|
153
|
+
>
|
|
154
|
+
<div className="table-grid">
|
|
155
|
+
{[...Array(10)].map((_, row) =>
|
|
156
|
+
[...Array(10)].map((_, col) => {
|
|
157
|
+
const isSelected = row < selectedRows && col < selectedCols;
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
key={`${row}-${col}`}
|
|
161
|
+
className={`table-grid-cell ${
|
|
162
|
+
isSelected ? "selected" : ""
|
|
163
|
+
}`}
|
|
164
|
+
onMouseEnter={() => handleTableCellHover(row + 1, col + 1)}
|
|
165
|
+
onClick={() => handleTableInsert(row + 1, col + 1)}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
})
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
<div className="table-grid-label">
|
|
172
|
+
{selectedRows} x {selectedCols}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<ToolbarButton
|
|
178
|
+
icon={MdInsertPhoto}
|
|
179
|
+
label="Insert Image"
|
|
180
|
+
onClick={() => imageInputRef.current?.click()}
|
|
181
|
+
/>
|
|
182
|
+
<ToolbarButton
|
|
183
|
+
icon={MdVideoLibrary}
|
|
184
|
+
label="Insert Video"
|
|
185
|
+
onClick={() => videoInputRef.current?.click()}
|
|
186
|
+
/>
|
|
187
|
+
<ToolbarButton
|
|
188
|
+
icon={MdInsertLink}
|
|
189
|
+
label="Insert Link"
|
|
190
|
+
onClick={() => {
|
|
191
|
+
const url = prompt("Enter URL");
|
|
192
|
+
if (url) {
|
|
193
|
+
editor
|
|
194
|
+
.chain()
|
|
195
|
+
.focus()
|
|
196
|
+
.extendMarkRange("link")
|
|
197
|
+
.setLink({ href: url })
|
|
198
|
+
.run();
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
<ToolbarButton
|
|
203
|
+
icon={MdInsertComment}
|
|
204
|
+
label="Insert Comment"
|
|
205
|
+
onClick={() => {
|
|
206
|
+
const comment = prompt("Enter your comment");
|
|
207
|
+
if (
|
|
208
|
+
comment &&
|
|
209
|
+
editor.can().chain().focus().setComment(comment).run()
|
|
210
|
+
) {
|
|
211
|
+
editor.chain().focus().setComment(comment).run();
|
|
212
|
+
} else {
|
|
213
|
+
console.warn("Cannot apply comment ā maybe no selection?");
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
<div className="relative">
|
|
218
|
+
<ToolbarButton
|
|
219
|
+
icon={MdInsertEmoticon}
|
|
220
|
+
label="Emoji"
|
|
221
|
+
onClick={() => setShowPicker(!showPicker)}
|
|
222
|
+
/>
|
|
223
|
+
{showPicker && (
|
|
224
|
+
<div className="emoji-picker">
|
|
225
|
+
<Picker
|
|
226
|
+
onEmojiSelect={addEmoji}
|
|
227
|
+
theme="auto"
|
|
228
|
+
emoji="point_up"
|
|
229
|
+
showPreview={false}
|
|
230
|
+
showSkinTones={true}
|
|
231
|
+
emojiTooltip={true}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
<ToolbarButton
|
|
237
|
+
icon={MdHorizontalRule}
|
|
238
|
+
label="Horizontal Line"
|
|
239
|
+
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
|
240
|
+
/>
|
|
241
|
+
<ToolbarButton
|
|
242
|
+
icon={MdOutlineOndemandVideo}
|
|
243
|
+
label="Embed"
|
|
244
|
+
onClick={() => {
|
|
245
|
+
const url = prompt(
|
|
246
|
+
"Enter embed URL (YouTube, Vimeo, Google Maps, etc.)"
|
|
247
|
+
);
|
|
248
|
+
if (!url) return;
|
|
249
|
+
|
|
250
|
+
const embedUrl = normalizeEmbedUrl(url);
|
|
251
|
+
const width = prompt("Enter width in pixels", "560");
|
|
252
|
+
const height = prompt("Enter height in pixels", "315");
|
|
253
|
+
|
|
254
|
+
editor
|
|
255
|
+
.chain()
|
|
256
|
+
.focus()
|
|
257
|
+
.setEmbed({
|
|
258
|
+
src: embedUrl,
|
|
259
|
+
width: width ? parseInt(width) : 560,
|
|
260
|
+
height: height ? parseInt(height) : 315,
|
|
261
|
+
})
|
|
262
|
+
.run();
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {
|
|
4
|
+
MdFormatListBulleted,
|
|
5
|
+
MdFormatListNumbered,
|
|
6
|
+
MdFormatIndentDecrease,
|
|
7
|
+
MdFormatIndentIncrease,
|
|
8
|
+
MdFormatAlignLeft,
|
|
9
|
+
MdFormatAlignCenter,
|
|
10
|
+
MdFormatAlignRight,
|
|
11
|
+
MdFormatAlignJustify,
|
|
12
|
+
} from "react-icons/md";
|
|
13
|
+
import { Editor } from "@tiptap/react";
|
|
14
|
+
import ToolbarButton from "./ToolbarButton";
|
|
15
|
+
|
|
16
|
+
export default function ListAlignGroup({ editor }: { editor: Editor }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="list-align-group">
|
|
19
|
+
<ToolbarButton
|
|
20
|
+
icon={MdFormatListBulleted}
|
|
21
|
+
title="Bulleted List"
|
|
22
|
+
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
23
|
+
disabled={!editor.can().toggleBulletList()}
|
|
24
|
+
/>
|
|
25
|
+
<ToolbarButton
|
|
26
|
+
icon={MdFormatListNumbered}
|
|
27
|
+
title="Numbered List"
|
|
28
|
+
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
29
|
+
disabled={!editor.can().toggleOrderedList()}
|
|
30
|
+
/>
|
|
31
|
+
<ToolbarButton
|
|
32
|
+
icon={MdFormatIndentIncrease}
|
|
33
|
+
title="Increase Indent"
|
|
34
|
+
onClick={() => editor.chain().focus().sinkListItem("listItem").run()}
|
|
35
|
+
disabled={!editor.can().sinkListItem("listItem")}
|
|
36
|
+
/>
|
|
37
|
+
<ToolbarButton
|
|
38
|
+
icon={MdFormatIndentDecrease}
|
|
39
|
+
title="Decrease Indent"
|
|
40
|
+
onClick={() => editor.chain().focus().liftListItem("listItem").run()}
|
|
41
|
+
disabled={!editor.can().liftListItem("listItem")}
|
|
42
|
+
/>
|
|
43
|
+
<ToolbarButton
|
|
44
|
+
icon={MdFormatAlignLeft}
|
|
45
|
+
title="Align Left"
|
|
46
|
+
onClick={() => editor.chain().focus().setTextAlign("left").run()}
|
|
47
|
+
disabled={!editor.can().setTextAlign("left")}
|
|
48
|
+
/>
|
|
49
|
+
<ToolbarButton
|
|
50
|
+
icon={MdFormatAlignCenter}
|
|
51
|
+
title="Align Center"
|
|
52
|
+
onClick={() => editor.chain().focus().setTextAlign("center").run()}
|
|
53
|
+
disabled={!editor.can().setTextAlign("center")}
|
|
54
|
+
/>
|
|
55
|
+
<ToolbarButton
|
|
56
|
+
icon={MdFormatAlignRight}
|
|
57
|
+
title="Align Right"
|
|
58
|
+
onClick={() => editor.chain().focus().setTextAlign("right").run()}
|
|
59
|
+
disabled={!editor.can().setTextAlign("right")}
|
|
60
|
+
/>
|
|
61
|
+
<ToolbarButton
|
|
62
|
+
icon={MdFormatAlignJustify}
|
|
63
|
+
title="Justify"
|
|
64
|
+
onClick={() => editor.chain().focus().setTextAlign("justify").run()}
|
|
65
|
+
disabled={!editor.can().setTextAlign("justify")}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { MdUndo, MdRedo, MdRefresh, MdVisibility, MdCode, MdSpellcheck, } from "react-icons/md";
|
|
3
|
+
import ToolbarButton from "./ToolbarButton";
|
|
4
|
+
import { checkGrammar } from "../../../utils/checkGrammar";
|
|
5
|
+
export default function MiscGroup({ editor }) {
|
|
6
|
+
const handlePreview = () => {
|
|
7
|
+
const html = editor.getHTML();
|
|
8
|
+
const previewWindow = window.open("", "_blank");
|
|
9
|
+
if (previewWindow) {
|
|
10
|
+
previewWindow.document.open();
|
|
11
|
+
previewWindow.document.write(`
|
|
12
|
+
<html>
|
|
13
|
+
<head>
|
|
14
|
+
<title>Preview</title>
|
|
15
|
+
<style>
|
|
16
|
+
body { font-family: sans-serif; padding: 2rem; line-height: 1.6; }
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>${html}</body>
|
|
20
|
+
</html>
|
|
21
|
+
`);
|
|
22
|
+
previewWindow.document.close();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const handleGrammarCheck = async () => {
|
|
26
|
+
const text = editor.getText();
|
|
27
|
+
try {
|
|
28
|
+
const issues = await checkGrammar(text);
|
|
29
|
+
if (issues.length === 0) {
|
|
30
|
+
alert("ā
No grammar issues found.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let message = "š Grammar Suggestions:\n\n";
|
|
34
|
+
issues.forEach((issue, idx) => {
|
|
35
|
+
const replacements = issue.replacements
|
|
36
|
+
.map((r) => r.value)
|
|
37
|
+
.join(", ");
|
|
38
|
+
message += `${idx + 1}. "${issue.context.text}"\nā ${replacements}\nReason: ${issue.message}\n\n`;
|
|
39
|
+
});
|
|
40
|
+
alert(message);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(err);
|
|
44
|
+
alert("ā Failed to check grammar. Please try again later.");
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return (<div className="misc-group">
|
|
48
|
+
<ToolbarButton icon={MdUndo} label="Undo" onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()}/>
|
|
49
|
+
<ToolbarButton icon={MdRedo} label="Redo" onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()}/>
|
|
50
|
+
<ToolbarButton icon={MdRefresh} label="Reset Formatting" onClick={() => editor.chain().focus().unsetAllMarks().clearNodes().run()}/>
|
|
51
|
+
<ToolbarButton icon={MdCode} label="Toggle Code Block" onClick={() => editor.chain().focus().toggleCodeBlock().run()} isActive={editor.isActive("codeBlock")}/>
|
|
52
|
+
<ToolbarButton icon={MdVisibility} label="Preview" onClick={handlePreview}/>
|
|
53
|
+
<ToolbarButton icon={MdSpellcheck} label="Check Grammar" onClick={handleGrammarCheck}/>
|
|
54
|
+
</div>);
|
|
55
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
MdUndo,
|
|
4
|
+
MdRedo,
|
|
5
|
+
MdRefresh,
|
|
6
|
+
MdVisibility,
|
|
7
|
+
MdCode,
|
|
8
|
+
MdSpellcheck,
|
|
9
|
+
} from "react-icons/md";
|
|
10
|
+
import { Editor } from "@tiptap/react";
|
|
11
|
+
import ToolbarButton from "./ToolbarButton";
|
|
12
|
+
import { checkGrammar, GrammarMatch } from "../../../utils/checkGrammar";
|
|
13
|
+
|
|
14
|
+
interface MiscGroupProps {
|
|
15
|
+
editor: Editor;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function MiscGroup({ editor }: MiscGroupProps) {
|
|
19
|
+
const handlePreview = () => {
|
|
20
|
+
const html = editor.getHTML();
|
|
21
|
+
const previewWindow = window.open("", "_blank");
|
|
22
|
+
if (previewWindow) {
|
|
23
|
+
previewWindow.document.open();
|
|
24
|
+
previewWindow.document.write(`
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<title>Preview</title>
|
|
28
|
+
<style>
|
|
29
|
+
body { font-family: sans-serif; padding: 2rem; line-height: 1.6; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>${html}</body>
|
|
33
|
+
</html>
|
|
34
|
+
`);
|
|
35
|
+
previewWindow.document.close();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleGrammarCheck = async () => {
|
|
40
|
+
const text = editor.getText();
|
|
41
|
+
try {
|
|
42
|
+
const issues: GrammarMatch[] = await checkGrammar(text);
|
|
43
|
+
if (issues.length === 0) {
|
|
44
|
+
alert("ā
No grammar issues found.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let message = "š Grammar Suggestions:\n\n";
|
|
49
|
+
issues.forEach((issue: GrammarMatch, idx: number) => {
|
|
50
|
+
const replacements = issue.replacements
|
|
51
|
+
.map((r: { value: string }) => r.value)
|
|
52
|
+
.join(", ");
|
|
53
|
+
message += `${idx + 1}. "${
|
|
54
|
+
issue.context.text
|
|
55
|
+
}"\nā ${replacements}\nReason: ${issue.message}\n\n`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
alert(message);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(err);
|
|
61
|
+
alert("ā Failed to check grammar. Please try again later.");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="misc-group">
|
|
67
|
+
<ToolbarButton
|
|
68
|
+
icon={MdUndo}
|
|
69
|
+
label="Undo"
|
|
70
|
+
onClick={() => editor.chain().focus().undo().run()}
|
|
71
|
+
disabled={!editor.can().undo()}
|
|
72
|
+
/>
|
|
73
|
+
<ToolbarButton
|
|
74
|
+
icon={MdRedo}
|
|
75
|
+
label="Redo"
|
|
76
|
+
onClick={() => editor.chain().focus().redo().run()}
|
|
77
|
+
disabled={!editor.can().redo()}
|
|
78
|
+
/>
|
|
79
|
+
<ToolbarButton
|
|
80
|
+
icon={MdRefresh}
|
|
81
|
+
label="Reset Formatting"
|
|
82
|
+
onClick={() =>
|
|
83
|
+
editor.chain().focus().unsetAllMarks().clearNodes().run()
|
|
84
|
+
}
|
|
85
|
+
/>
|
|
86
|
+
<ToolbarButton
|
|
87
|
+
icon={MdCode}
|
|
88
|
+
label="Toggle Code Block"
|
|
89
|
+
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
|
90
|
+
isActive={editor.isActive("codeBlock")}
|
|
91
|
+
/>
|
|
92
|
+
<ToolbarButton
|
|
93
|
+
icon={MdVisibility}
|
|
94
|
+
label="Preview"
|
|
95
|
+
onClick={handlePreview}
|
|
96
|
+
/>
|
|
97
|
+
<ToolbarButton
|
|
98
|
+
icon={MdSpellcheck}
|
|
99
|
+
label="Check Grammar"
|
|
100
|
+
onClick={handleGrammarCheck}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|