magic-editor-x 1.0.0
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/LICENSE +21 -0
- package/README.md +890 -0
- package/dist/_chunks/App-B1FgOsWa.mjs +2143 -0
- package/dist/_chunks/App-mtrlABtd.js +2146 -0
- package/dist/_chunks/LicensePage-BnyWSrWs.js +375 -0
- package/dist/_chunks/LicensePage-CWH-AFR-.mjs +373 -0
- package/dist/_chunks/LiveCollaborationPanel-DbDHwr2C.js +222 -0
- package/dist/_chunks/LiveCollaborationPanel-ryjcDAA7.mjs +220 -0
- package/dist/_chunks/Settings-Bk9bxJTy.js +440 -0
- package/dist/_chunks/Settings-D-V2MLVm.mjs +438 -0
- package/dist/_chunks/de-CSrHZWEb.mjs +295 -0
- package/dist/_chunks/de-CzSo1oD2.js +295 -0
- package/dist/_chunks/en-DuQun2v4.mjs +295 -0
- package/dist/_chunks/en-DxIkVPUh.js +295 -0
- package/dist/_chunks/es-DAQ_97zx.js +273 -0
- package/dist/_chunks/es-DEB0CA8S.mjs +273 -0
- package/dist/_chunks/fr-Bqkhvdx2.mjs +273 -0
- package/dist/_chunks/fr-ChPabvNP.js +273 -0
- package/dist/_chunks/getTranslation-C4uWR0DB.mjs +50985 -0
- package/dist/_chunks/getTranslation-D35vbDap.js +51001 -0
- package/dist/_chunks/index-B5MzUyo0.mjs +2541 -0
- package/dist/_chunks/index-BRVqbnOb.mjs +4450 -0
- package/dist/_chunks/index-BiLy_f7C.js +2540 -0
- package/dist/_chunks/index-CQx7-dFP.js +4472 -0
- package/dist/_chunks/pt-BMoYltav.mjs +273 -0
- package/dist/_chunks/pt-Cm74LpyZ.js +273 -0
- package/dist/_chunks/tools-CjnQJ9w2.mjs +2155 -0
- package/dist/_chunks/tools-DNt2tioN.js +2186 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/server/index.js +2554 -0
- package/dist/server/index.mjs +2544 -0
- package/dist/style.css +164 -0
- package/package.json +122 -0
- package/pics/collab-magiceditorX.png +0 -0
- package/pics/editorX.png +0 -0
- package/pics/liveCollabwidget1.png +0 -0
|
@@ -0,0 +1,2155 @@
|
|
|
1
|
+
import Header from "@editorjs/header";
|
|
2
|
+
import Paragraph from "@editorjs/paragraph";
|
|
3
|
+
import NestedList from "@editorjs/nested-list";
|
|
4
|
+
import Checklist from "@editorjs/checklist";
|
|
5
|
+
import Quote from "@editorjs/quote";
|
|
6
|
+
import Warning from "@editorjs/warning";
|
|
7
|
+
import Code from "@editorjs/code";
|
|
8
|
+
import Delimiter from "@editorjs/delimiter";
|
|
9
|
+
import Table from "@editorjs/table";
|
|
10
|
+
import Embed from "@editorjs/embed";
|
|
11
|
+
import Raw from "@editorjs/raw";
|
|
12
|
+
import Image from "@editorjs/image";
|
|
13
|
+
import SimpleImage from "@editorjs/simple-image";
|
|
14
|
+
import LinkTool from "@editorjs/link";
|
|
15
|
+
import Attaches from "@editorjs/attaches";
|
|
16
|
+
import Personality from "@editorjs/personality";
|
|
17
|
+
import Alert from "editorjs-alert";
|
|
18
|
+
import ToggleBlock from "editorjs-toggle-block";
|
|
19
|
+
import CodeFlask from "@calumk/editorjs-codeflask";
|
|
20
|
+
import React__default, { useState, useRef, useEffect } from "react";
|
|
21
|
+
import { createRoot } from "react-dom/client";
|
|
22
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
23
|
+
import styled, { keyframes } from "styled-components";
|
|
24
|
+
import Marker from "@editorjs/marker";
|
|
25
|
+
import InlineCode from "@editorjs/inline-code";
|
|
26
|
+
import Underline from "@editorjs/underline";
|
|
27
|
+
import Strikethrough from "@sotaproject/strikethrough";
|
|
28
|
+
import Tooltip from "editorjs-tooltip";
|
|
29
|
+
import TextVariantTune from "@editorjs/text-variant-tune";
|
|
30
|
+
import AlignmentTune from "editorjs-text-alignment-blocktune";
|
|
31
|
+
import IndentTune from "editorjs-indent-tune";
|
|
32
|
+
import Undo from "editorjs-undo";
|
|
33
|
+
import DragDrop from "editorjs-drag-drop";
|
|
34
|
+
class ButtonTool {
|
|
35
|
+
/**
|
|
36
|
+
* Enable paragraph-like behavior
|
|
37
|
+
*/
|
|
38
|
+
static get pasteConfig() {
|
|
39
|
+
return { tags: ["BUTTON", "A"] };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Tool icon in the toolbox
|
|
43
|
+
*/
|
|
44
|
+
static get toolbox() {
|
|
45
|
+
return {
|
|
46
|
+
title: "Button",
|
|
47
|
+
icon: '<svg width="17" height="15" viewBox="0 0 17 15" xmlns="http://www.w3.org/2000/svg"><path d="M1 2.5C1 1.67157 1.67157 1 2.5 1H14.5C15.3284 1 16 1.67157 16 2.5V12.5C16 13.3284 15.3284 14 14.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5ZM3.5 5C3.22386 5 3 5.22386 3 5.5V9.5C3 9.77614 3.22386 10 3.5 10H13.5C13.7761 10 14 9.77614 14 9.5V5.5C14 5.22386 13.7761 5 13.5 5H3.5Z" fill="currentColor"/></svg>'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Creates an instance of ButtonTool
|
|
52
|
+
* @param {object} params - Constructor params
|
|
53
|
+
* @param {object} params.data - Previously saved data
|
|
54
|
+
* @param {object} params.config - Tool configuration
|
|
55
|
+
* @param {object} params.api - Editor.js API
|
|
56
|
+
*/
|
|
57
|
+
constructor({ data, config, api }) {
|
|
58
|
+
this.api = api;
|
|
59
|
+
this.config = config || {};
|
|
60
|
+
this.data = {
|
|
61
|
+
text: data.text || "",
|
|
62
|
+
link: data.link || "",
|
|
63
|
+
style: data.style || "primary",
|
|
64
|
+
openInNewTab: data.openInNewTab !== false
|
|
65
|
+
};
|
|
66
|
+
this.wrapper = null;
|
|
67
|
+
this.styles = this._getDefaultStyles();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Returns default button styles
|
|
71
|
+
* @returns {object} Style configuration
|
|
72
|
+
*/
|
|
73
|
+
_getDefaultStyles() {
|
|
74
|
+
return {
|
|
75
|
+
primary: {
|
|
76
|
+
background: "#3b82f6",
|
|
77
|
+
color: "#ffffff",
|
|
78
|
+
border: "none",
|
|
79
|
+
hoverBackground: "#2563eb"
|
|
80
|
+
},
|
|
81
|
+
secondary: {
|
|
82
|
+
background: "#64748b",
|
|
83
|
+
color: "#ffffff",
|
|
84
|
+
border: "none",
|
|
85
|
+
hoverBackground: "#475569"
|
|
86
|
+
},
|
|
87
|
+
outline: {
|
|
88
|
+
background: "transparent",
|
|
89
|
+
color: "#3b82f6",
|
|
90
|
+
border: "2px solid #3b82f6",
|
|
91
|
+
hoverBackground: "#eff6ff"
|
|
92
|
+
},
|
|
93
|
+
success: {
|
|
94
|
+
background: "#22c55e",
|
|
95
|
+
color: "#ffffff",
|
|
96
|
+
border: "none",
|
|
97
|
+
hoverBackground: "#16a34a"
|
|
98
|
+
},
|
|
99
|
+
danger: {
|
|
100
|
+
background: "#ef4444",
|
|
101
|
+
color: "#ffffff",
|
|
102
|
+
border: "none",
|
|
103
|
+
hoverBackground: "#dc2626"
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Renders the tool UI
|
|
109
|
+
* @returns {HTMLElement} The tool wrapper element
|
|
110
|
+
*/
|
|
111
|
+
render() {
|
|
112
|
+
this.wrapper = document.createElement("div");
|
|
113
|
+
this.wrapper.classList.add("cdx-button-tool");
|
|
114
|
+
this.wrapper.style.cssText = `
|
|
115
|
+
padding: 12px;
|
|
116
|
+
background: #f8fafc;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
margin: 8px 0;
|
|
119
|
+
`;
|
|
120
|
+
const preview = document.createElement("div");
|
|
121
|
+
preview.style.cssText = "text-align: center; margin-bottom: 12px;";
|
|
122
|
+
const button = this._createButton();
|
|
123
|
+
preview.appendChild(button);
|
|
124
|
+
const form = document.createElement("div");
|
|
125
|
+
form.style.cssText = "display: flex; flex-direction: column; gap: 8px;";
|
|
126
|
+
const textInput = this._createInput("Button Text", this.data.text, (value) => {
|
|
127
|
+
this.data.text = value;
|
|
128
|
+
button.textContent = value || "Click Me";
|
|
129
|
+
});
|
|
130
|
+
const linkInput = this._createInput("Link URL", this.data.link, (value) => {
|
|
131
|
+
this.data.link = value;
|
|
132
|
+
});
|
|
133
|
+
const styleSelect = this._createStyleSelect((value) => {
|
|
134
|
+
this.data.style = value;
|
|
135
|
+
this._updateButtonStyle(button, value);
|
|
136
|
+
});
|
|
137
|
+
const newTabCheckbox = this._createCheckbox("Open in new tab", this.data.openInNewTab, (checked) => {
|
|
138
|
+
this.data.openInNewTab = checked;
|
|
139
|
+
});
|
|
140
|
+
form.appendChild(textInput);
|
|
141
|
+
form.appendChild(linkInput);
|
|
142
|
+
form.appendChild(styleSelect);
|
|
143
|
+
form.appendChild(newTabCheckbox);
|
|
144
|
+
this.wrapper.appendChild(preview);
|
|
145
|
+
this.wrapper.appendChild(form);
|
|
146
|
+
return this.wrapper;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Creates the button element
|
|
150
|
+
* @returns {HTMLElement} Button element
|
|
151
|
+
*/
|
|
152
|
+
_createButton() {
|
|
153
|
+
const button = document.createElement("button");
|
|
154
|
+
button.type = "button";
|
|
155
|
+
button.textContent = this.data.text || "Click Me";
|
|
156
|
+
button.style.cssText = `
|
|
157
|
+
padding: 10px 24px;
|
|
158
|
+
font-size: 14px;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
border-radius: 6px;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
transition: all 0.2s ease;
|
|
163
|
+
outline: none;
|
|
164
|
+
`;
|
|
165
|
+
this._updateButtonStyle(button, this.data.style);
|
|
166
|
+
button.addEventListener("click", (e) => {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
});
|
|
170
|
+
return button;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Updates button styling
|
|
174
|
+
* @param {HTMLElement} button - Button element
|
|
175
|
+
* @param {string} style - Style name
|
|
176
|
+
*/
|
|
177
|
+
_updateButtonStyle(button, style) {
|
|
178
|
+
const styleConfig = this.styles[style] || this.styles.primary;
|
|
179
|
+
button.style.backgroundColor = styleConfig.background;
|
|
180
|
+
button.style.color = styleConfig.color;
|
|
181
|
+
button.style.border = styleConfig.border;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Creates an input field with label
|
|
185
|
+
* @param {string} label - Input label
|
|
186
|
+
* @param {string} value - Initial value
|
|
187
|
+
* @param {Function} onChange - Change handler
|
|
188
|
+
* @returns {HTMLElement} Input container
|
|
189
|
+
*/
|
|
190
|
+
_createInput(label, value, onChange) {
|
|
191
|
+
const container = document.createElement("div");
|
|
192
|
+
container.style.cssText = "display: flex; flex-direction: column; gap: 4px;";
|
|
193
|
+
const labelEl = document.createElement("label");
|
|
194
|
+
labelEl.textContent = label;
|
|
195
|
+
labelEl.style.cssText = "font-size: 12px; color: #64748b; font-weight: 500;";
|
|
196
|
+
const input = document.createElement("input");
|
|
197
|
+
input.type = "text";
|
|
198
|
+
input.value = value;
|
|
199
|
+
input.placeholder = label;
|
|
200
|
+
input.style.cssText = `
|
|
201
|
+
padding: 8px 12px;
|
|
202
|
+
border: 1px solid #e2e8f0;
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
font-size: 14px;
|
|
205
|
+
outline: none;
|
|
206
|
+
transition: border-color 0.2s;
|
|
207
|
+
`;
|
|
208
|
+
input.addEventListener("focus", () => {
|
|
209
|
+
input.style.borderColor = "#3b82f6";
|
|
210
|
+
});
|
|
211
|
+
input.addEventListener("blur", () => {
|
|
212
|
+
input.style.borderColor = "#e2e8f0";
|
|
213
|
+
});
|
|
214
|
+
input.addEventListener("input", (e) => {
|
|
215
|
+
onChange(e.target.value);
|
|
216
|
+
});
|
|
217
|
+
container.appendChild(labelEl);
|
|
218
|
+
container.appendChild(input);
|
|
219
|
+
return container;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Creates style select dropdown
|
|
223
|
+
* @param {Function} onChange - Change handler
|
|
224
|
+
* @returns {HTMLElement} Select container
|
|
225
|
+
*/
|
|
226
|
+
_createStyleSelect(onChange) {
|
|
227
|
+
const container = document.createElement("div");
|
|
228
|
+
container.style.cssText = "display: flex; flex-direction: column; gap: 4px;";
|
|
229
|
+
const label = document.createElement("label");
|
|
230
|
+
label.textContent = "Button Style";
|
|
231
|
+
label.style.cssText = "font-size: 12px; color: #64748b; font-weight: 500;";
|
|
232
|
+
const select = document.createElement("select");
|
|
233
|
+
select.style.cssText = `
|
|
234
|
+
padding: 8px 12px;
|
|
235
|
+
border: 1px solid #e2e8f0;
|
|
236
|
+
border-radius: 6px;
|
|
237
|
+
font-size: 14px;
|
|
238
|
+
outline: none;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
background: white;
|
|
241
|
+
`;
|
|
242
|
+
const options = [
|
|
243
|
+
{ value: "primary", label: "Primary (Blue)" },
|
|
244
|
+
{ value: "secondary", label: "Secondary (Gray)" },
|
|
245
|
+
{ value: "outline", label: "Outline" },
|
|
246
|
+
{ value: "success", label: "Success (Green)" },
|
|
247
|
+
{ value: "danger", label: "Danger (Red)" }
|
|
248
|
+
];
|
|
249
|
+
options.forEach((opt) => {
|
|
250
|
+
const option = document.createElement("option");
|
|
251
|
+
option.value = opt.value;
|
|
252
|
+
option.textContent = opt.label;
|
|
253
|
+
option.selected = this.data.style === opt.value;
|
|
254
|
+
select.appendChild(option);
|
|
255
|
+
});
|
|
256
|
+
select.addEventListener("change", (e) => {
|
|
257
|
+
onChange(e.target.value);
|
|
258
|
+
});
|
|
259
|
+
container.appendChild(label);
|
|
260
|
+
container.appendChild(select);
|
|
261
|
+
return container;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Creates checkbox with label
|
|
265
|
+
* @param {string} label - Checkbox label
|
|
266
|
+
* @param {boolean} checked - Initial state
|
|
267
|
+
* @param {Function} onChange - Change handler
|
|
268
|
+
* @returns {HTMLElement} Checkbox container
|
|
269
|
+
*/
|
|
270
|
+
_createCheckbox(label, checked, onChange) {
|
|
271
|
+
const container = document.createElement("div");
|
|
272
|
+
container.style.cssText = "display: flex; align-items: center; gap: 8px; margin-top: 4px;";
|
|
273
|
+
const checkbox = document.createElement("input");
|
|
274
|
+
checkbox.type = "checkbox";
|
|
275
|
+
checkbox.checked = checked;
|
|
276
|
+
checkbox.style.cssText = "width: 16px; height: 16px; cursor: pointer;";
|
|
277
|
+
checkbox.addEventListener("change", (e) => {
|
|
278
|
+
onChange(e.target.checked);
|
|
279
|
+
});
|
|
280
|
+
const labelEl = document.createElement("label");
|
|
281
|
+
labelEl.textContent = label;
|
|
282
|
+
labelEl.style.cssText = "font-size: 14px; color: #334155; cursor: pointer;";
|
|
283
|
+
labelEl.addEventListener("click", () => {
|
|
284
|
+
checkbox.checked = !checkbox.checked;
|
|
285
|
+
onChange(checkbox.checked);
|
|
286
|
+
});
|
|
287
|
+
container.appendChild(checkbox);
|
|
288
|
+
container.appendChild(labelEl);
|
|
289
|
+
return container;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Extracts data from the tool
|
|
293
|
+
* @returns {object} Saved data
|
|
294
|
+
*/
|
|
295
|
+
save() {
|
|
296
|
+
return {
|
|
297
|
+
text: this.data.text,
|
|
298
|
+
link: this.data.link,
|
|
299
|
+
style: this.data.style,
|
|
300
|
+
openInNewTab: this.data.openInNewTab
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Validates block data
|
|
305
|
+
* @param {object} savedData - Saved data to validate
|
|
306
|
+
* @returns {boolean} True if valid
|
|
307
|
+
*/
|
|
308
|
+
validate(savedData) {
|
|
309
|
+
return savedData.text && savedData.text.trim().length > 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
class HyperlinkTool {
|
|
313
|
+
/**
|
|
314
|
+
* Specifies this is an inline tool
|
|
315
|
+
*/
|
|
316
|
+
static get isInline() {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* CSS class for the tool icon
|
|
321
|
+
*/
|
|
322
|
+
static get CSS() {
|
|
323
|
+
return "ce-inline-tool--hyperlink";
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Sanitize config for Editor.js
|
|
327
|
+
*/
|
|
328
|
+
static get sanitize() {
|
|
329
|
+
return {
|
|
330
|
+
a: {
|
|
331
|
+
href: true,
|
|
332
|
+
target: true,
|
|
333
|
+
rel: true
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Creates an instance of HyperlinkTool
|
|
339
|
+
* @param {object} params - Constructor params
|
|
340
|
+
* @param {object} params.api - Editor.js API
|
|
341
|
+
* @param {object} params.config - Tool configuration
|
|
342
|
+
*/
|
|
343
|
+
constructor({ api, config }) {
|
|
344
|
+
this.api = api;
|
|
345
|
+
this.config = config || {};
|
|
346
|
+
this.defaultTarget = this.config.target || "_blank";
|
|
347
|
+
this.defaultRel = this.config.rel || "noopener noreferrer";
|
|
348
|
+
this.availableTargets = this.config.availableTargets || ["_blank", "_self", "_parent", "_top"];
|
|
349
|
+
this.availableRels = this.config.availableRels || ["nofollow", "noreferrer", "noopener", "sponsored", "ugc"];
|
|
350
|
+
this.button = null;
|
|
351
|
+
this.popup = null;
|
|
352
|
+
this.state = false;
|
|
353
|
+
this.selectedRange = null;
|
|
354
|
+
this.existingLink = null;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Renders the toolbar button
|
|
358
|
+
* @returns {HTMLElement} Button element
|
|
359
|
+
*/
|
|
360
|
+
render() {
|
|
361
|
+
this.button = document.createElement("button");
|
|
362
|
+
this.button.type = "button";
|
|
363
|
+
this.button.classList.add(this.api.styles.inlineToolButton);
|
|
364
|
+
this.button.classList.add(HyperlinkTool.CSS);
|
|
365
|
+
this.button.innerHTML = this._getIcon();
|
|
366
|
+
return this.button;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Returns the link icon SVG
|
|
370
|
+
* @returns {string} SVG icon
|
|
371
|
+
*/
|
|
372
|
+
_getIcon() {
|
|
373
|
+
return `<svg width="14" height="10" viewBox="0 0 14 10" xmlns="http://www.w3.org/2000/svg">
|
|
374
|
+
<path d="M6 1.25H2.75C1.64543 1.25 0.75 2.14543 0.75 3.25V6.75C0.75 7.85457 1.64543 8.75 2.75 8.75H6" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
|
375
|
+
<path d="M8 8.75H11.25C12.3546 8.75 13.25 7.85457 13.25 6.75V3.25C13.25 2.14543 12.3546 1.25 11.25 1.25H8" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
|
376
|
+
<path d="M4 5H10" stroke="currentColor" stroke-width="1.5"/>
|
|
377
|
+
</svg>`;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Handles click/activation of the tool
|
|
381
|
+
* @param {Range} range - Current selection range
|
|
382
|
+
*/
|
|
383
|
+
surround(range) {
|
|
384
|
+
if (this.state) {
|
|
385
|
+
this._unwrap(range);
|
|
386
|
+
} else {
|
|
387
|
+
this._showPopup(range);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Shows the link input popup
|
|
392
|
+
* @param {Range} range - Current selection range
|
|
393
|
+
*/
|
|
394
|
+
_showPopup(range) {
|
|
395
|
+
this.selectedRange = range;
|
|
396
|
+
const parentAnchor = this.api.selection.findParentTag("A");
|
|
397
|
+
this.existingLink = parentAnchor;
|
|
398
|
+
this.popup = document.createElement("div");
|
|
399
|
+
this.popup.classList.add("ce-inline-tool-hyperlink-popup");
|
|
400
|
+
this.popup.style.cssText = `
|
|
401
|
+
position: absolute;
|
|
402
|
+
background: white;
|
|
403
|
+
border: 1px solid #e2e8f0;
|
|
404
|
+
border-radius: 8px;
|
|
405
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
406
|
+
padding: 12px;
|
|
407
|
+
z-index: 999999;
|
|
408
|
+
min-width: 300px;
|
|
409
|
+
`;
|
|
410
|
+
const urlContainer = this._createInputContainer("URL", "url", parentAnchor?.href || "");
|
|
411
|
+
const targetContainer = this._createSelectContainer(
|
|
412
|
+
"Target",
|
|
413
|
+
"target",
|
|
414
|
+
this.availableTargets.map((t) => ({ value: t, label: t })),
|
|
415
|
+
parentAnchor?.target || this.defaultTarget
|
|
416
|
+
);
|
|
417
|
+
const relContainer = this._createRelContainer(parentAnchor?.rel || this.defaultRel);
|
|
418
|
+
const buttonsContainer = document.createElement("div");
|
|
419
|
+
buttonsContainer.style.cssText = "display: flex; gap: 8px; margin-top: 12px; justify-content: flex-end;";
|
|
420
|
+
const cancelBtn = document.createElement("button");
|
|
421
|
+
cancelBtn.type = "button";
|
|
422
|
+
cancelBtn.textContent = "Cancel";
|
|
423
|
+
cancelBtn.style.cssText = `
|
|
424
|
+
padding: 6px 12px;
|
|
425
|
+
border: 1px solid #e2e8f0;
|
|
426
|
+
border-radius: 6px;
|
|
427
|
+
background: white;
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
font-size: 13px;
|
|
430
|
+
`;
|
|
431
|
+
cancelBtn.addEventListener("click", () => this._closePopup());
|
|
432
|
+
const saveBtn = document.createElement("button");
|
|
433
|
+
saveBtn.type = "button";
|
|
434
|
+
saveBtn.textContent = parentAnchor ? "Update" : "Add Link";
|
|
435
|
+
saveBtn.style.cssText = `
|
|
436
|
+
padding: 6px 12px;
|
|
437
|
+
border: none;
|
|
438
|
+
border-radius: 6px;
|
|
439
|
+
background: #3b82f6;
|
|
440
|
+
color: white;
|
|
441
|
+
cursor: pointer;
|
|
442
|
+
font-size: 13px;
|
|
443
|
+
font-weight: 500;
|
|
444
|
+
`;
|
|
445
|
+
saveBtn.addEventListener("click", () => this._saveLink());
|
|
446
|
+
if (parentAnchor) {
|
|
447
|
+
const removeBtn = document.createElement("button");
|
|
448
|
+
removeBtn.type = "button";
|
|
449
|
+
removeBtn.textContent = "Remove";
|
|
450
|
+
removeBtn.style.cssText = `
|
|
451
|
+
padding: 6px 12px;
|
|
452
|
+
border: 1px solid #ef4444;
|
|
453
|
+
border-radius: 6px;
|
|
454
|
+
background: white;
|
|
455
|
+
color: #ef4444;
|
|
456
|
+
cursor: pointer;
|
|
457
|
+
font-size: 13px;
|
|
458
|
+
`;
|
|
459
|
+
removeBtn.addEventListener("click", () => {
|
|
460
|
+
this._unwrap(range);
|
|
461
|
+
this._closePopup();
|
|
462
|
+
});
|
|
463
|
+
buttonsContainer.appendChild(removeBtn);
|
|
464
|
+
}
|
|
465
|
+
buttonsContainer.appendChild(cancelBtn);
|
|
466
|
+
buttonsContainer.appendChild(saveBtn);
|
|
467
|
+
this.popup.appendChild(urlContainer);
|
|
468
|
+
this.popup.appendChild(targetContainer);
|
|
469
|
+
this.popup.appendChild(relContainer);
|
|
470
|
+
this.popup.appendChild(buttonsContainer);
|
|
471
|
+
document.body.appendChild(this.popup);
|
|
472
|
+
this._positionPopup();
|
|
473
|
+
setTimeout(() => {
|
|
474
|
+
const urlInput = this.popup.querySelector('input[name="url"]');
|
|
475
|
+
if (urlInput) urlInput.focus();
|
|
476
|
+
}, 50);
|
|
477
|
+
setTimeout(() => {
|
|
478
|
+
document.addEventListener("click", this._handleOutsideClick);
|
|
479
|
+
}, 100);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Creates an input container
|
|
483
|
+
* @param {string} label - Input label
|
|
484
|
+
* @param {string} name - Input name
|
|
485
|
+
* @param {string} value - Initial value
|
|
486
|
+
* @returns {HTMLElement} Input container
|
|
487
|
+
*/
|
|
488
|
+
_createInputContainer(label, name, value) {
|
|
489
|
+
const container = document.createElement("div");
|
|
490
|
+
container.style.cssText = "margin-bottom: 10px;";
|
|
491
|
+
const labelEl = document.createElement("label");
|
|
492
|
+
labelEl.textContent = label;
|
|
493
|
+
labelEl.style.cssText = "display: block; font-size: 12px; color: #64748b; margin-bottom: 4px; font-weight: 500;";
|
|
494
|
+
const input = document.createElement("input");
|
|
495
|
+
input.type = "text";
|
|
496
|
+
input.name = name;
|
|
497
|
+
input.value = value;
|
|
498
|
+
input.placeholder = "https://example.com";
|
|
499
|
+
input.style.cssText = `
|
|
500
|
+
width: 100%;
|
|
501
|
+
padding: 8px 10px;
|
|
502
|
+
border: 1px solid #e2e8f0;
|
|
503
|
+
border-radius: 6px;
|
|
504
|
+
font-size: 14px;
|
|
505
|
+
outline: none;
|
|
506
|
+
box-sizing: border-box;
|
|
507
|
+
`;
|
|
508
|
+
input.addEventListener("focus", () => {
|
|
509
|
+
input.style.borderColor = "#3b82f6";
|
|
510
|
+
});
|
|
511
|
+
input.addEventListener("blur", () => {
|
|
512
|
+
input.style.borderColor = "#e2e8f0";
|
|
513
|
+
});
|
|
514
|
+
container.appendChild(labelEl);
|
|
515
|
+
container.appendChild(input);
|
|
516
|
+
return container;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Creates a select container
|
|
520
|
+
* @param {string} label - Select label
|
|
521
|
+
* @param {string} name - Select name
|
|
522
|
+
* @param {Array} options - Select options
|
|
523
|
+
* @param {string} value - Initial value
|
|
524
|
+
* @returns {HTMLElement} Select container
|
|
525
|
+
*/
|
|
526
|
+
_createSelectContainer(label, name, options, value) {
|
|
527
|
+
const container = document.createElement("div");
|
|
528
|
+
container.style.cssText = "margin-bottom: 10px;";
|
|
529
|
+
const labelEl = document.createElement("label");
|
|
530
|
+
labelEl.textContent = label;
|
|
531
|
+
labelEl.style.cssText = "display: block; font-size: 12px; color: #64748b; margin-bottom: 4px; font-weight: 500;";
|
|
532
|
+
const select = document.createElement("select");
|
|
533
|
+
select.name = name;
|
|
534
|
+
select.style.cssText = `
|
|
535
|
+
width: 100%;
|
|
536
|
+
padding: 8px 10px;
|
|
537
|
+
border: 1px solid #e2e8f0;
|
|
538
|
+
border-radius: 6px;
|
|
539
|
+
font-size: 14px;
|
|
540
|
+
outline: none;
|
|
541
|
+
background: white;
|
|
542
|
+
cursor: pointer;
|
|
543
|
+
box-sizing: border-box;
|
|
544
|
+
`;
|
|
545
|
+
options.forEach((opt) => {
|
|
546
|
+
const option = document.createElement("option");
|
|
547
|
+
option.value = opt.value;
|
|
548
|
+
option.textContent = opt.label;
|
|
549
|
+
option.selected = value === opt.value;
|
|
550
|
+
select.appendChild(option);
|
|
551
|
+
});
|
|
552
|
+
container.appendChild(labelEl);
|
|
553
|
+
container.appendChild(select);
|
|
554
|
+
return container;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Creates rel attribute checkboxes
|
|
558
|
+
* @param {string} currentRel - Current rel value
|
|
559
|
+
* @returns {HTMLElement} Rel container
|
|
560
|
+
*/
|
|
561
|
+
_createRelContainer(currentRel) {
|
|
562
|
+
const container = document.createElement("div");
|
|
563
|
+
container.style.cssText = "margin-bottom: 8px;";
|
|
564
|
+
const label = document.createElement("label");
|
|
565
|
+
label.textContent = "Rel Attributes";
|
|
566
|
+
label.style.cssText = "display: block; font-size: 12px; color: #64748b; margin-bottom: 6px; font-weight: 500;";
|
|
567
|
+
container.appendChild(label);
|
|
568
|
+
const checkboxContainer = document.createElement("div");
|
|
569
|
+
checkboxContainer.style.cssText = "display: flex; flex-wrap: wrap; gap: 12px;";
|
|
570
|
+
const currentRels = currentRel ? currentRel.split(" ") : [];
|
|
571
|
+
this.availableRels.forEach((rel) => {
|
|
572
|
+
const wrapper = document.createElement("label");
|
|
573
|
+
wrapper.style.cssText = "display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 13px;";
|
|
574
|
+
const checkbox = document.createElement("input");
|
|
575
|
+
checkbox.type = "checkbox";
|
|
576
|
+
checkbox.name = "rel";
|
|
577
|
+
checkbox.value = rel;
|
|
578
|
+
checkbox.checked = currentRels.includes(rel);
|
|
579
|
+
checkbox.style.cssText = "cursor: pointer;";
|
|
580
|
+
wrapper.appendChild(checkbox);
|
|
581
|
+
wrapper.appendChild(document.createTextNode(rel));
|
|
582
|
+
checkboxContainer.appendChild(wrapper);
|
|
583
|
+
});
|
|
584
|
+
container.appendChild(checkboxContainer);
|
|
585
|
+
return container;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Positions the popup near the selection
|
|
589
|
+
*/
|
|
590
|
+
_positionPopup() {
|
|
591
|
+
if (!this.popup) return;
|
|
592
|
+
const selection = window.getSelection();
|
|
593
|
+
if (!selection.rangeCount) return;
|
|
594
|
+
const range = selection.getRangeAt(0);
|
|
595
|
+
const rect = range.getBoundingClientRect();
|
|
596
|
+
const popupRect = this.popup.getBoundingClientRect();
|
|
597
|
+
let left = rect.left + rect.width / 2 - popupRect.width / 2;
|
|
598
|
+
let top = rect.bottom + 10 + window.scrollY;
|
|
599
|
+
if (left < 10) left = 10;
|
|
600
|
+
if (left + popupRect.width > window.innerWidth - 10) {
|
|
601
|
+
left = window.innerWidth - popupRect.width - 10;
|
|
602
|
+
}
|
|
603
|
+
this.popup.style.left = `${left}px`;
|
|
604
|
+
this.popup.style.top = `${top}px`;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Saves the link with user-entered values
|
|
608
|
+
*/
|
|
609
|
+
_saveLink() {
|
|
610
|
+
if (!this.popup) return;
|
|
611
|
+
const urlInput = this.popup.querySelector('input[name="url"]');
|
|
612
|
+
const targetSelect = this.popup.querySelector('select[name="target"]');
|
|
613
|
+
const relCheckboxes = this.popup.querySelectorAll('input[name="rel"]:checked');
|
|
614
|
+
const url = urlInput?.value?.trim();
|
|
615
|
+
if (!url) {
|
|
616
|
+
urlInput.style.borderColor = "#ef4444";
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const target = targetSelect?.value || this.defaultTarget;
|
|
620
|
+
const rel = Array.from(relCheckboxes).map((cb) => cb.value).join(" ") || this.defaultRel;
|
|
621
|
+
this._closePopup();
|
|
622
|
+
if (this.selectedRange) {
|
|
623
|
+
const selection = window.getSelection();
|
|
624
|
+
selection.removeAllRanges();
|
|
625
|
+
selection.addRange(this.selectedRange);
|
|
626
|
+
}
|
|
627
|
+
if (this.existingLink) {
|
|
628
|
+
this.existingLink.href = url;
|
|
629
|
+
this.existingLink.target = target;
|
|
630
|
+
this.existingLink.rel = rel;
|
|
631
|
+
} else {
|
|
632
|
+
this._wrap(url, target, rel);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Wraps selection in anchor tag
|
|
637
|
+
* @param {string} href - Link URL
|
|
638
|
+
* @param {string} target - Link target
|
|
639
|
+
* @param {string} rel - Link rel
|
|
640
|
+
*/
|
|
641
|
+
_wrap(href, target, rel) {
|
|
642
|
+
const selection = window.getSelection();
|
|
643
|
+
if (!selection.rangeCount) return;
|
|
644
|
+
const range = selection.getRangeAt(0);
|
|
645
|
+
const selectedText = range.extractContents();
|
|
646
|
+
const anchor = document.createElement("a");
|
|
647
|
+
anchor.href = href;
|
|
648
|
+
anchor.target = target;
|
|
649
|
+
anchor.rel = rel;
|
|
650
|
+
anchor.appendChild(selectedText);
|
|
651
|
+
range.insertNode(anchor);
|
|
652
|
+
selection.removeAllRanges();
|
|
653
|
+
const newRange = document.createRange();
|
|
654
|
+
newRange.selectNode(anchor);
|
|
655
|
+
selection.addRange(newRange);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Unwraps anchor tag from selection
|
|
659
|
+
* @param {Range} range - Selection range
|
|
660
|
+
*/
|
|
661
|
+
_unwrap(range) {
|
|
662
|
+
const anchor = this.api.selection.findParentTag("A");
|
|
663
|
+
if (!anchor) return;
|
|
664
|
+
const text = anchor.textContent;
|
|
665
|
+
const textNode = document.createTextNode(text);
|
|
666
|
+
anchor.parentNode.replaceChild(textNode, anchor);
|
|
667
|
+
const selection = window.getSelection();
|
|
668
|
+
selection.removeAllRanges();
|
|
669
|
+
const newRange = document.createRange();
|
|
670
|
+
newRange.selectNode(textNode);
|
|
671
|
+
selection.addRange(newRange);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Closes the popup
|
|
675
|
+
*/
|
|
676
|
+
_closePopup() {
|
|
677
|
+
if (this.popup) {
|
|
678
|
+
this.popup.remove();
|
|
679
|
+
this.popup = null;
|
|
680
|
+
}
|
|
681
|
+
document.removeEventListener("click", this._handleOutsideClick);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Handles clicks outside the popup
|
|
685
|
+
* @param {Event} e - Click event
|
|
686
|
+
*/
|
|
687
|
+
_handleOutsideClick = (e) => {
|
|
688
|
+
if (this.popup && !this.popup.contains(e.target) && !this.button.contains(e.target)) {
|
|
689
|
+
this._closePopup();
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* Checks if current selection is inside a link
|
|
694
|
+
* @param {Selection} selection - Current selection
|
|
695
|
+
* @returns {boolean} True if inside link
|
|
696
|
+
*/
|
|
697
|
+
checkState(selection) {
|
|
698
|
+
const anchor = this.api.selection.findParentTag("A");
|
|
699
|
+
this.state = !!anchor;
|
|
700
|
+
this.button.classList.toggle(this.api.styles.inlineToolButtonActive, this.state);
|
|
701
|
+
return this.state;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Shortcut key
|
|
705
|
+
*/
|
|
706
|
+
get shortcut() {
|
|
707
|
+
return this.config.shortcut || "CMD+K";
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Clean up on destroy
|
|
711
|
+
*/
|
|
712
|
+
destroy() {
|
|
713
|
+
this._closePopup();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const API_BASE_URL = "https://magicapi.fitlex.me/api/magic-editor";
|
|
717
|
+
class MagicEditorAPI {
|
|
718
|
+
constructor(licenseKey) {
|
|
719
|
+
this.licenseKey = licenseKey;
|
|
720
|
+
this.baseUrl = API_BASE_URL;
|
|
721
|
+
}
|
|
722
|
+
async request(endpoint, options = {}) {
|
|
723
|
+
if (!this.licenseKey) {
|
|
724
|
+
throw new Error("No license key available");
|
|
725
|
+
}
|
|
726
|
+
let url = `${this.baseUrl}${endpoint}`;
|
|
727
|
+
if (!options.method || options.method === "GET") {
|
|
728
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
729
|
+
url = `${url}${separator}licenseKey=${encodeURIComponent(this.licenseKey)}`;
|
|
730
|
+
}
|
|
731
|
+
const response = await fetch(url, {
|
|
732
|
+
...options,
|
|
733
|
+
headers: {
|
|
734
|
+
"Content-Type": "application/json",
|
|
735
|
+
...options.headers
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
const data = await response.json();
|
|
739
|
+
if (!response.ok) {
|
|
740
|
+
const error = new Error(data.error?.message || "API request failed");
|
|
741
|
+
error.code = data.error?.code;
|
|
742
|
+
error.status = response.status;
|
|
743
|
+
error.upgrade = data.upgrade;
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
return data;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Perform text correction/transformation
|
|
750
|
+
* All AI actions go through this endpoint with different types
|
|
751
|
+
* @param {string} text - Text to process
|
|
752
|
+
* @param {string} type - Type: 'grammar' | 'style' | 'rewrite' | 'expand' | 'summarize' | 'continue' | 'translate'
|
|
753
|
+
* @param {object} options - Additional options (tone, language, etc.)
|
|
754
|
+
* @returns {Promise<object>} Result with corrected text
|
|
755
|
+
*/
|
|
756
|
+
async correct(text, type = "grammar", options = {}) {
|
|
757
|
+
return this.request("/correct", {
|
|
758
|
+
method: "POST",
|
|
759
|
+
body: JSON.stringify({ text, type, options, licenseKey: this.licenseKey })
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
async getUsage() {
|
|
763
|
+
return this.request("/usage");
|
|
764
|
+
}
|
|
765
|
+
async getCredits() {
|
|
766
|
+
return this.request("/credits");
|
|
767
|
+
}
|
|
768
|
+
async getModels() {
|
|
769
|
+
return this.request("/models");
|
|
770
|
+
}
|
|
771
|
+
async getLimits() {
|
|
772
|
+
return this.request("/limits");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const ToolbarContainer = styled.div`
|
|
776
|
+
position: fixed;
|
|
777
|
+
background: white;
|
|
778
|
+
border: 1px solid #e2e8f0;
|
|
779
|
+
border-radius: 8px;
|
|
780
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
|
781
|
+
padding: 4px;
|
|
782
|
+
display: flex;
|
|
783
|
+
gap: 2px;
|
|
784
|
+
z-index: 9999;
|
|
785
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
786
|
+
animation: slideUp 0.2s ease-out;
|
|
787
|
+
|
|
788
|
+
@keyframes slideUp {
|
|
789
|
+
from {
|
|
790
|
+
opacity: 0;
|
|
791
|
+
transform: translateY(4px);
|
|
792
|
+
}
|
|
793
|
+
to {
|
|
794
|
+
opacity: 1;
|
|
795
|
+
transform: translateY(0);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
&::before {
|
|
800
|
+
content: '';
|
|
801
|
+
position: absolute;
|
|
802
|
+
top: -6px;
|
|
803
|
+
left: 50%;
|
|
804
|
+
transform: translateX(-50%);
|
|
805
|
+
width: 12px;
|
|
806
|
+
height: 12px;
|
|
807
|
+
background: white;
|
|
808
|
+
border-left: 1px solid #e2e8f0;
|
|
809
|
+
border-top: 1px solid #e2e8f0;
|
|
810
|
+
transform: translateX(-50%) rotate(45deg);
|
|
811
|
+
}
|
|
812
|
+
`;
|
|
813
|
+
const ActionButton = styled.button`
|
|
814
|
+
padding: 6px 12px;
|
|
815
|
+
display: flex;
|
|
816
|
+
align-items: center;
|
|
817
|
+
gap: 6px;
|
|
818
|
+
font-size: 13px;
|
|
819
|
+
font-weight: 500;
|
|
820
|
+
border: none;
|
|
821
|
+
border-radius: 6px;
|
|
822
|
+
background: transparent;
|
|
823
|
+
color: #334155;
|
|
824
|
+
cursor: pointer;
|
|
825
|
+
transition: all 0.15s ease;
|
|
826
|
+
white-space: nowrap;
|
|
827
|
+
position: relative;
|
|
828
|
+
|
|
829
|
+
&:hover:not(:disabled) {
|
|
830
|
+
background: #f8fafc;
|
|
831
|
+
color: #7C3AED;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
&:active:not(:disabled) {
|
|
835
|
+
transform: scale(0.98);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
&:disabled {
|
|
839
|
+
opacity: 0.5;
|
|
840
|
+
cursor: not-allowed;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
svg {
|
|
844
|
+
width: 16px;
|
|
845
|
+
height: 16px;
|
|
846
|
+
flex-shrink: 0;
|
|
847
|
+
}
|
|
848
|
+
`;
|
|
849
|
+
const Divider = styled.div`
|
|
850
|
+
width: 1px;
|
|
851
|
+
height: 24px;
|
|
852
|
+
background: #e2e8f0;
|
|
853
|
+
margin: 0 4px;
|
|
854
|
+
align-self: center;
|
|
855
|
+
`;
|
|
856
|
+
const Submenu = styled.div`
|
|
857
|
+
position: absolute;
|
|
858
|
+
top: 100%;
|
|
859
|
+
left: 0;
|
|
860
|
+
margin-top: 4px;
|
|
861
|
+
background: white;
|
|
862
|
+
border: 1px solid #e2e8f0;
|
|
863
|
+
border-radius: 8px;
|
|
864
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
865
|
+
padding: 4px;
|
|
866
|
+
min-width: 160px;
|
|
867
|
+
z-index: 10000;
|
|
868
|
+
animation: slideDown 0.15s ease-out;
|
|
869
|
+
|
|
870
|
+
@keyframes slideDown {
|
|
871
|
+
from {
|
|
872
|
+
opacity: 0;
|
|
873
|
+
transform: translateY(-4px);
|
|
874
|
+
}
|
|
875
|
+
to {
|
|
876
|
+
opacity: 1;
|
|
877
|
+
transform: translateY(0);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
`;
|
|
881
|
+
const SubmenuItem = styled.button`
|
|
882
|
+
width: 100%;
|
|
883
|
+
padding: 8px 12px;
|
|
884
|
+
display: flex;
|
|
885
|
+
align-items: center;
|
|
886
|
+
gap: 8px;
|
|
887
|
+
font-size: 13px;
|
|
888
|
+
border: none;
|
|
889
|
+
border-radius: 4px;
|
|
890
|
+
background: transparent;
|
|
891
|
+
color: #334155;
|
|
892
|
+
cursor: pointer;
|
|
893
|
+
text-align: left;
|
|
894
|
+
transition: all 0.1s ease;
|
|
895
|
+
|
|
896
|
+
&:hover {
|
|
897
|
+
background: #f8fafc;
|
|
898
|
+
color: #7C3AED;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
svg {
|
|
902
|
+
width: 14px;
|
|
903
|
+
height: 14px;
|
|
904
|
+
}
|
|
905
|
+
`;
|
|
906
|
+
const LoadingSpinner = styled.div`
|
|
907
|
+
width: 14px;
|
|
908
|
+
height: 14px;
|
|
909
|
+
border: 2px solid #e2e8f0;
|
|
910
|
+
border-top-color: #7C3AED;
|
|
911
|
+
border-radius: 50%;
|
|
912
|
+
animation: spin 0.8s linear infinite;
|
|
913
|
+
|
|
914
|
+
@keyframes spin {
|
|
915
|
+
to { transform: rotate(360deg); }
|
|
916
|
+
}
|
|
917
|
+
`;
|
|
918
|
+
const AIInlineToolbar = ({
|
|
919
|
+
position,
|
|
920
|
+
onAction,
|
|
921
|
+
loading = false,
|
|
922
|
+
disabled = false,
|
|
923
|
+
onClose
|
|
924
|
+
}) => {
|
|
925
|
+
const [activeSubmenu, setActiveSubmenu] = useState(null);
|
|
926
|
+
const toolbarRef = useRef(null);
|
|
927
|
+
useEffect(() => {
|
|
928
|
+
const handleClickOutside = (e) => {
|
|
929
|
+
if (toolbarRef.current && !toolbarRef.current.contains(e.target)) {
|
|
930
|
+
onClose?.();
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
const handleEscape = (e) => {
|
|
934
|
+
if (e.key === "Escape") {
|
|
935
|
+
onClose?.();
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
939
|
+
document.addEventListener("keydown", handleEscape);
|
|
940
|
+
return () => {
|
|
941
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
942
|
+
document.removeEventListener("keydown", handleEscape);
|
|
943
|
+
};
|
|
944
|
+
}, [onClose]);
|
|
945
|
+
const handleAction = (action, options = {}) => {
|
|
946
|
+
setActiveSubmenu(null);
|
|
947
|
+
onAction?.(action, options);
|
|
948
|
+
};
|
|
949
|
+
const toggleSubmenu = (menu) => {
|
|
950
|
+
setActiveSubmenu(activeSubmenu === menu ? null : menu);
|
|
951
|
+
};
|
|
952
|
+
return /* @__PURE__ */ jsxs(
|
|
953
|
+
ToolbarContainer,
|
|
954
|
+
{
|
|
955
|
+
ref: toolbarRef,
|
|
956
|
+
style: {
|
|
957
|
+
left: `${position.left}px`,
|
|
958
|
+
top: `${position.top}px`
|
|
959
|
+
},
|
|
960
|
+
children: [
|
|
961
|
+
/* @__PURE__ */ jsxs(
|
|
962
|
+
ActionButton,
|
|
963
|
+
{
|
|
964
|
+
onClick: () => handleAction("fix"),
|
|
965
|
+
disabled: disabled || loading,
|
|
966
|
+
title: "Fix grammar and spelling",
|
|
967
|
+
children: [
|
|
968
|
+
loading ? /* @__PURE__ */ jsx(LoadingSpinner, {}) : /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) }),
|
|
969
|
+
/* @__PURE__ */ jsx("span", { children: "Fix" })
|
|
970
|
+
]
|
|
971
|
+
}
|
|
972
|
+
),
|
|
973
|
+
/* @__PURE__ */ jsxs(
|
|
974
|
+
ActionButton,
|
|
975
|
+
{
|
|
976
|
+
onClick: () => toggleSubmenu("rewrite"),
|
|
977
|
+
disabled: disabled || loading,
|
|
978
|
+
title: "Rewrite text with different style",
|
|
979
|
+
style: { position: "relative" },
|
|
980
|
+
children: [
|
|
981
|
+
/* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M12 3l1.912 5.813a2 2 0 001.275 1.275L21 12l-5.813 1.912a2 2 0 00-1.275 1.275L12 21l-1.912-5.813a2 2 0 00-1.275-1.275L3 12l5.813-1.912a2 2 0 001.275-1.275L12 3z" }) }),
|
|
982
|
+
/* @__PURE__ */ jsx("span", { children: "Rewrite" }),
|
|
983
|
+
/* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", style: { width: "12px", height: "12px" }, children: /* @__PURE__ */ jsx("polyline", { points: "6 9 12 15 18 9" }) }),
|
|
984
|
+
activeSubmenu === "rewrite" && /* @__PURE__ */ jsxs(Submenu, { children: [
|
|
985
|
+
/* @__PURE__ */ jsxs(SubmenuItem, { onClick: () => handleAction("rewrite", { tone: "professional" }), children: [
|
|
986
|
+
/* @__PURE__ */ jsx("span", { children: "💼" }),
|
|
987
|
+
/* @__PURE__ */ jsx("span", { children: "Professional" })
|
|
988
|
+
] }),
|
|
989
|
+
/* @__PURE__ */ jsxs(SubmenuItem, { onClick: () => handleAction("rewrite", { tone: "casual" }), children: [
|
|
990
|
+
/* @__PURE__ */ jsx("span", { children: "😊" }),
|
|
991
|
+
/* @__PURE__ */ jsx("span", { children: "Casual" })
|
|
992
|
+
] }),
|
|
993
|
+
/* @__PURE__ */ jsxs(SubmenuItem, { onClick: () => handleAction("rewrite", { tone: "friendly" }), children: [
|
|
994
|
+
/* @__PURE__ */ jsx("span", { children: "👋" }),
|
|
995
|
+
/* @__PURE__ */ jsx("span", { children: "Friendly" })
|
|
996
|
+
] })
|
|
997
|
+
] })
|
|
998
|
+
]
|
|
999
|
+
}
|
|
1000
|
+
),
|
|
1001
|
+
/* @__PURE__ */ jsx(Divider, {}),
|
|
1002
|
+
/* @__PURE__ */ jsxs(
|
|
1003
|
+
ActionButton,
|
|
1004
|
+
{
|
|
1005
|
+
onClick: () => handleAction("expand"),
|
|
1006
|
+
disabled: disabled || loading,
|
|
1007
|
+
title: "Make text longer and more detailed",
|
|
1008
|
+
children: [
|
|
1009
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
1010
|
+
/* @__PURE__ */ jsx("polyline", { points: "7 13 12 18 17 13" }),
|
|
1011
|
+
/* @__PURE__ */ jsx("polyline", { points: "7 6 12 11 17 6" })
|
|
1012
|
+
] }),
|
|
1013
|
+
/* @__PURE__ */ jsx("span", { children: "Expand" })
|
|
1014
|
+
]
|
|
1015
|
+
}
|
|
1016
|
+
),
|
|
1017
|
+
/* @__PURE__ */ jsxs(
|
|
1018
|
+
ActionButton,
|
|
1019
|
+
{
|
|
1020
|
+
onClick: () => handleAction("summarize"),
|
|
1021
|
+
disabled: disabled || loading,
|
|
1022
|
+
title: "Make text shorter and concise",
|
|
1023
|
+
children: [
|
|
1024
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
1025
|
+
/* @__PURE__ */ jsx("polyline", { points: "17 11 12 6 7 11" }),
|
|
1026
|
+
/* @__PURE__ */ jsx("polyline", { points: "17 18 12 13 7 18" })
|
|
1027
|
+
] }),
|
|
1028
|
+
/* @__PURE__ */ jsx("span", { children: "Shorten" })
|
|
1029
|
+
]
|
|
1030
|
+
}
|
|
1031
|
+
),
|
|
1032
|
+
/* @__PURE__ */ jsx(Divider, {}),
|
|
1033
|
+
/* @__PURE__ */ jsxs(
|
|
1034
|
+
ActionButton,
|
|
1035
|
+
{
|
|
1036
|
+
onClick: () => handleAction("continue"),
|
|
1037
|
+
disabled: disabled || loading,
|
|
1038
|
+
title: "Continue writing from here",
|
|
1039
|
+
children: [
|
|
1040
|
+
/* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12l7 7 7-7" }) }),
|
|
1041
|
+
/* @__PURE__ */ jsx("span", { children: "Continue" })
|
|
1042
|
+
]
|
|
1043
|
+
}
|
|
1044
|
+
),
|
|
1045
|
+
/* @__PURE__ */ jsxs(
|
|
1046
|
+
ActionButton,
|
|
1047
|
+
{
|
|
1048
|
+
onClick: () => toggleSubmenu("translate"),
|
|
1049
|
+
disabled: disabled || loading,
|
|
1050
|
+
title: "Translate to another language",
|
|
1051
|
+
style: { position: "relative" },
|
|
1052
|
+
children: [
|
|
1053
|
+
/* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M5 8l6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14.5 17h6" }) }),
|
|
1054
|
+
/* @__PURE__ */ jsx("span", { children: "Translate" }),
|
|
1055
|
+
/* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", style: { width: "12px", height: "12px" }, children: /* @__PURE__ */ jsx("polyline", { points: "6 9 12 15 18 9" }) }),
|
|
1056
|
+
activeSubmenu === "translate" && /* @__PURE__ */ jsxs(Submenu, { children: [
|
|
1057
|
+
/* @__PURE__ */ jsxs(SubmenuItem, { onClick: () => handleAction("translate", { language: "en" }), children: [
|
|
1058
|
+
/* @__PURE__ */ jsx("span", { children: "🇬🇧" }),
|
|
1059
|
+
/* @__PURE__ */ jsx("span", { children: "English" })
|
|
1060
|
+
] }),
|
|
1061
|
+
/* @__PURE__ */ jsxs(SubmenuItem, { onClick: () => handleAction("translate", { language: "de" }), children: [
|
|
1062
|
+
/* @__PURE__ */ jsx("span", { children: "🇩🇪" }),
|
|
1063
|
+
/* @__PURE__ */ jsx("span", { children: "German" })
|
|
1064
|
+
] }),
|
|
1065
|
+
/* @__PURE__ */ jsxs(SubmenuItem, { onClick: () => handleAction("translate", { language: "fr" }), children: [
|
|
1066
|
+
/* @__PURE__ */ jsx("span", { children: "🇫🇷" }),
|
|
1067
|
+
/* @__PURE__ */ jsx("span", { children: "French" })
|
|
1068
|
+
] }),
|
|
1069
|
+
/* @__PURE__ */ jsxs(SubmenuItem, { onClick: () => handleAction("translate", { language: "es" }), children: [
|
|
1070
|
+
/* @__PURE__ */ jsx("span", { children: "🇪🇸" }),
|
|
1071
|
+
/* @__PURE__ */ jsx("span", { children: "Spanish" })
|
|
1072
|
+
] })
|
|
1073
|
+
] })
|
|
1074
|
+
]
|
|
1075
|
+
}
|
|
1076
|
+
)
|
|
1077
|
+
]
|
|
1078
|
+
}
|
|
1079
|
+
);
|
|
1080
|
+
};
|
|
1081
|
+
const slideIn = keyframes`
|
|
1082
|
+
from {
|
|
1083
|
+
opacity: 0;
|
|
1084
|
+
transform: translateY(-20px);
|
|
1085
|
+
}
|
|
1086
|
+
to {
|
|
1087
|
+
opacity: 1;
|
|
1088
|
+
transform: translateY(0);
|
|
1089
|
+
}
|
|
1090
|
+
`;
|
|
1091
|
+
const slideOut = keyframes`
|
|
1092
|
+
from {
|
|
1093
|
+
opacity: 1;
|
|
1094
|
+
transform: translateY(0);
|
|
1095
|
+
}
|
|
1096
|
+
to {
|
|
1097
|
+
opacity: 0;
|
|
1098
|
+
transform: translateY(-20px);
|
|
1099
|
+
}
|
|
1100
|
+
`;
|
|
1101
|
+
const ToastContainer = styled.div`
|
|
1102
|
+
position: fixed;
|
|
1103
|
+
top: 20px;
|
|
1104
|
+
right: 20px;
|
|
1105
|
+
z-index: 99999;
|
|
1106
|
+
display: flex;
|
|
1107
|
+
flex-direction: column;
|
|
1108
|
+
gap: 10px;
|
|
1109
|
+
pointer-events: none;
|
|
1110
|
+
`;
|
|
1111
|
+
const ToastItem = styled.div`
|
|
1112
|
+
display: flex;
|
|
1113
|
+
align-items: center;
|
|
1114
|
+
gap: 12px;
|
|
1115
|
+
padding: 12px 16px;
|
|
1116
|
+
background: white;
|
|
1117
|
+
border: 1px solid #e2e8f0;
|
|
1118
|
+
border-radius: 8px;
|
|
1119
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
1120
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
1121
|
+
font-size: 14px;
|
|
1122
|
+
color: #334155;
|
|
1123
|
+
min-width: 280px;
|
|
1124
|
+
max-width: 400px;
|
|
1125
|
+
pointer-events: auto;
|
|
1126
|
+
animation: ${(props) => props.$closing ? slideOut : slideIn} 0.3s ease-out;
|
|
1127
|
+
|
|
1128
|
+
&.success {
|
|
1129
|
+
border-left: 3px solid #10b981;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
&.error {
|
|
1133
|
+
border-left: 3px solid #ef4444;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
&.info {
|
|
1137
|
+
border-left: 3px solid #3b82f6;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
&.warning {
|
|
1141
|
+
border-left: 3px solid #f59e0b;
|
|
1142
|
+
}
|
|
1143
|
+
`;
|
|
1144
|
+
const Icon = styled.div`
|
|
1145
|
+
flex-shrink: 0;
|
|
1146
|
+
width: 20px;
|
|
1147
|
+
height: 20px;
|
|
1148
|
+
display: flex;
|
|
1149
|
+
align-items: center;
|
|
1150
|
+
justify-content: center;
|
|
1151
|
+
font-size: 16px;
|
|
1152
|
+
`;
|
|
1153
|
+
const Message = styled.div`
|
|
1154
|
+
flex: 1;
|
|
1155
|
+
line-height: 1.4;
|
|
1156
|
+
|
|
1157
|
+
strong {
|
|
1158
|
+
font-weight: 600;
|
|
1159
|
+
display: block;
|
|
1160
|
+
margin-bottom: 2px;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
span {
|
|
1164
|
+
font-size: 13px;
|
|
1165
|
+
color: #64748b;
|
|
1166
|
+
}
|
|
1167
|
+
`;
|
|
1168
|
+
const CloseButton = styled.button`
|
|
1169
|
+
flex-shrink: 0;
|
|
1170
|
+
width: 20px;
|
|
1171
|
+
height: 20px;
|
|
1172
|
+
display: flex;
|
|
1173
|
+
align-items: center;
|
|
1174
|
+
justify-content: center;
|
|
1175
|
+
border: none;
|
|
1176
|
+
background: transparent;
|
|
1177
|
+
color: #94a3b8;
|
|
1178
|
+
cursor: pointer;
|
|
1179
|
+
border-radius: 4px;
|
|
1180
|
+
transition: all 0.15s ease;
|
|
1181
|
+
|
|
1182
|
+
&:hover {
|
|
1183
|
+
background: #f1f5f9;
|
|
1184
|
+
color: #334155;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
svg {
|
|
1188
|
+
width: 14px;
|
|
1189
|
+
height: 14px;
|
|
1190
|
+
}
|
|
1191
|
+
`;
|
|
1192
|
+
const UndoButton = styled.button`
|
|
1193
|
+
flex-shrink: 0;
|
|
1194
|
+
padding: 4px 10px;
|
|
1195
|
+
font-size: 12px;
|
|
1196
|
+
font-weight: 500;
|
|
1197
|
+
border: 1px solid #e2e8f0;
|
|
1198
|
+
background: white;
|
|
1199
|
+
color: #7C3AED;
|
|
1200
|
+
border-radius: 4px;
|
|
1201
|
+
cursor: pointer;
|
|
1202
|
+
transition: all 0.15s ease;
|
|
1203
|
+
|
|
1204
|
+
&:hover {
|
|
1205
|
+
background: #f5f3ff;
|
|
1206
|
+
border-color: #7C3AED;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
&:active {
|
|
1210
|
+
transform: scale(0.98);
|
|
1211
|
+
}
|
|
1212
|
+
`;
|
|
1213
|
+
class ToastManager {
|
|
1214
|
+
constructor() {
|
|
1215
|
+
this.toasts = [];
|
|
1216
|
+
this.listeners = [];
|
|
1217
|
+
}
|
|
1218
|
+
subscribe(listener) {
|
|
1219
|
+
this.listeners.push(listener);
|
|
1220
|
+
return () => {
|
|
1221
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
notify() {
|
|
1225
|
+
this.listeners.forEach((listener) => listener(this.toasts));
|
|
1226
|
+
}
|
|
1227
|
+
show(options) {
|
|
1228
|
+
const id = Date.now() + Math.random();
|
|
1229
|
+
const toast = {
|
|
1230
|
+
id,
|
|
1231
|
+
type: options.type || "info",
|
|
1232
|
+
icon: options.icon,
|
|
1233
|
+
message: options.message,
|
|
1234
|
+
description: options.description,
|
|
1235
|
+
duration: options.duration || 3e3,
|
|
1236
|
+
onUndo: options.onUndo,
|
|
1237
|
+
closeable: options.closeable !== false
|
|
1238
|
+
};
|
|
1239
|
+
this.toasts.push(toast);
|
|
1240
|
+
this.notify();
|
|
1241
|
+
if (toast.duration > 0) {
|
|
1242
|
+
setTimeout(() => {
|
|
1243
|
+
this.remove(id);
|
|
1244
|
+
}, toast.duration);
|
|
1245
|
+
}
|
|
1246
|
+
return id;
|
|
1247
|
+
}
|
|
1248
|
+
remove(id) {
|
|
1249
|
+
this.toasts = this.toasts.filter((t) => t.id !== id);
|
|
1250
|
+
this.notify();
|
|
1251
|
+
}
|
|
1252
|
+
success(message, options = {}) {
|
|
1253
|
+
return this.show({
|
|
1254
|
+
type: "success",
|
|
1255
|
+
icon: "✓",
|
|
1256
|
+
message,
|
|
1257
|
+
...options
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
error(message, options = {}) {
|
|
1261
|
+
return this.show({
|
|
1262
|
+
type: "error",
|
|
1263
|
+
icon: "✕",
|
|
1264
|
+
message,
|
|
1265
|
+
...options
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
info(message, options = {}) {
|
|
1269
|
+
return this.show({
|
|
1270
|
+
type: "info",
|
|
1271
|
+
icon: "ℹ",
|
|
1272
|
+
message,
|
|
1273
|
+
...options
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
warning(message, options = {}) {
|
|
1277
|
+
return this.show({
|
|
1278
|
+
type: "warning",
|
|
1279
|
+
icon: "⚠",
|
|
1280
|
+
message,
|
|
1281
|
+
...options
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const toastManager = new ToastManager();
|
|
1286
|
+
const Toast = ({ toast, onClose }) => {
|
|
1287
|
+
const [closing, setClosing] = useState(false);
|
|
1288
|
+
const handleClose = () => {
|
|
1289
|
+
setClosing(true);
|
|
1290
|
+
setTimeout(() => {
|
|
1291
|
+
onClose();
|
|
1292
|
+
}, 300);
|
|
1293
|
+
};
|
|
1294
|
+
const handleUndo = () => {
|
|
1295
|
+
toast.onUndo?.();
|
|
1296
|
+
handleClose();
|
|
1297
|
+
};
|
|
1298
|
+
return /* @__PURE__ */ jsxs(ToastItem, { className: toast.type, $closing: closing, children: [
|
|
1299
|
+
toast.icon && /* @__PURE__ */ jsx(Icon, { children: toast.icon }),
|
|
1300
|
+
/* @__PURE__ */ jsx(Message, { children: typeof toast.message === "string" ? toast.message : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1301
|
+
/* @__PURE__ */ jsx("strong", { children: toast.message }),
|
|
1302
|
+
toast.description && /* @__PURE__ */ jsx("span", { children: toast.description })
|
|
1303
|
+
] }) }),
|
|
1304
|
+
toast.onUndo && /* @__PURE__ */ jsx(UndoButton, { onClick: handleUndo, children: "Undo" }),
|
|
1305
|
+
toast.closeable && /* @__PURE__ */ jsx(CloseButton, { onClick: handleClose, children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
1306
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
1307
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
1308
|
+
] }) })
|
|
1309
|
+
] });
|
|
1310
|
+
};
|
|
1311
|
+
const AIToast = () => {
|
|
1312
|
+
const [toasts, setToasts] = useState([]);
|
|
1313
|
+
useEffect(() => {
|
|
1314
|
+
const unsubscribe = toastManager.subscribe(setToasts);
|
|
1315
|
+
return unsubscribe;
|
|
1316
|
+
}, []);
|
|
1317
|
+
if (toasts.length === 0) {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
return /* @__PURE__ */ jsx(ToastContainer, { children: toasts.map((toast) => /* @__PURE__ */ jsx(
|
|
1321
|
+
Toast,
|
|
1322
|
+
{
|
|
1323
|
+
toast,
|
|
1324
|
+
onClose: () => toastManager.remove(toast.id)
|
|
1325
|
+
},
|
|
1326
|
+
toast.id
|
|
1327
|
+
)) });
|
|
1328
|
+
};
|
|
1329
|
+
class AIAssistantTool {
|
|
1330
|
+
static get isInline() {
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
static get CSS() {
|
|
1334
|
+
return "ce-inline-tool--ai-assistant";
|
|
1335
|
+
}
|
|
1336
|
+
static get sanitize() {
|
|
1337
|
+
return {
|
|
1338
|
+
span: {
|
|
1339
|
+
class: "ai-suggestion",
|
|
1340
|
+
"data-original": true,
|
|
1341
|
+
"data-corrected": true,
|
|
1342
|
+
"data-type": true
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
static get title() {
|
|
1347
|
+
return "KI-Assistent";
|
|
1348
|
+
}
|
|
1349
|
+
constructor({ api, config }) {
|
|
1350
|
+
this.api = api;
|
|
1351
|
+
this.config = config || {};
|
|
1352
|
+
this.getLicenseKey = this.config.getLicenseKey || (() => window.__MAGIC_EDITOR_LICENSE_KEY__);
|
|
1353
|
+
this.button = null;
|
|
1354
|
+
this.toolbar = null;
|
|
1355
|
+
this.toolbarRoot = null;
|
|
1356
|
+
this.state = false;
|
|
1357
|
+
this.selectedRange = null;
|
|
1358
|
+
this.selectedText = "";
|
|
1359
|
+
this.currentBlockIndex = null;
|
|
1360
|
+
this.apiClient = null;
|
|
1361
|
+
this.isLoading = false;
|
|
1362
|
+
this.lastAction = null;
|
|
1363
|
+
}
|
|
1364
|
+
_initAPIClient() {
|
|
1365
|
+
const licenseKey = this.getLicenseKey();
|
|
1366
|
+
if (licenseKey && !this.apiClient) {
|
|
1367
|
+
this.apiClient = new MagicEditorAPI(licenseKey);
|
|
1368
|
+
}
|
|
1369
|
+
return this.apiClient;
|
|
1370
|
+
}
|
|
1371
|
+
render() {
|
|
1372
|
+
this.button = document.createElement("button");
|
|
1373
|
+
this.button.type = "button";
|
|
1374
|
+
this.button.classList.add(this.api.styles.inlineToolButton);
|
|
1375
|
+
this.button.classList.add(AIAssistantTool.CSS);
|
|
1376
|
+
this.button.innerHTML = this._getIcon();
|
|
1377
|
+
this.button.title = "KI-Assistent (⌘+Shift+G)";
|
|
1378
|
+
return this.button;
|
|
1379
|
+
}
|
|
1380
|
+
_getIcon() {
|
|
1381
|
+
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1382
|
+
<path d="M12 3l1.912 5.813a2 2 0 001.275 1.275L21 12l-5.813 1.912a2 2 0 00-1.275 1.275L12 21l-1.912-5.813a2 2 0 00-1.275-1.275L3 12l5.813-1.912a2 2 0 001.275-1.275L12 3z"/>
|
|
1383
|
+
<path d="M5 3v4"/>
|
|
1384
|
+
<path d="M3 5h4"/>
|
|
1385
|
+
<path d="M19 17v4"/>
|
|
1386
|
+
<path d="M17 19h4"/>
|
|
1387
|
+
</svg>`;
|
|
1388
|
+
}
|
|
1389
|
+
surround(range) {
|
|
1390
|
+
const selection = window.getSelection();
|
|
1391
|
+
let text = "";
|
|
1392
|
+
if (selection.rangeCount > 0) {
|
|
1393
|
+
this.selectedRange = selection.getRangeAt(0).cloneRange();
|
|
1394
|
+
text = selection.toString().trim();
|
|
1395
|
+
}
|
|
1396
|
+
if (!text) {
|
|
1397
|
+
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
|
1398
|
+
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
|
1399
|
+
if (currentBlock) {
|
|
1400
|
+
this.currentBlockIndex = currentBlockIndex;
|
|
1401
|
+
const blockElement = currentBlock.holder?.querySelector("[contenteditable]") || currentBlock.holder?.querySelector(".ce-paragraph") || currentBlock.holder?.querySelector(".ce-header");
|
|
1402
|
+
if (blockElement) {
|
|
1403
|
+
text = blockElement.textContent?.trim() || "";
|
|
1404
|
+
if (text) {
|
|
1405
|
+
const range2 = document.createRange();
|
|
1406
|
+
range2.selectNodeContents(blockElement);
|
|
1407
|
+
selection.removeAllRanges();
|
|
1408
|
+
selection.addRange(range2);
|
|
1409
|
+
this.selectedRange = range2.cloneRange();
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
this.selectedText = text;
|
|
1415
|
+
if (!text) {
|
|
1416
|
+
toastManager.warning("Bitte Text auswählen");
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
this._showInlineToolbar();
|
|
1420
|
+
}
|
|
1421
|
+
async _showInlineToolbar() {
|
|
1422
|
+
if (!this._initAPIClient()) {
|
|
1423
|
+
toastManager.error("Keine Lizenz verfügbar");
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const selection = window.getSelection();
|
|
1427
|
+
if (!selection.rangeCount) return;
|
|
1428
|
+
const rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
1429
|
+
const position = {
|
|
1430
|
+
left: rect.left + rect.width / 2 - 200,
|
|
1431
|
+
// Center toolbar (approximate width)
|
|
1432
|
+
top: rect.bottom + 8
|
|
1433
|
+
};
|
|
1434
|
+
if (position.left < 10) position.left = 10;
|
|
1435
|
+
if (position.left + 400 > window.innerWidth - 10) {
|
|
1436
|
+
position.left = window.innerWidth - 410;
|
|
1437
|
+
}
|
|
1438
|
+
this.toolbar = document.createElement("div");
|
|
1439
|
+
this.toolbar.classList.add("ai-inline-toolbar-container");
|
|
1440
|
+
document.body.appendChild(this.toolbar);
|
|
1441
|
+
this.toolbarRoot = createRoot(this.toolbar);
|
|
1442
|
+
this.toolbarRoot.render(
|
|
1443
|
+
React__default.createElement(AIInlineToolbar, {
|
|
1444
|
+
position,
|
|
1445
|
+
onAction: this._handleAction.bind(this),
|
|
1446
|
+
loading: this.isLoading,
|
|
1447
|
+
onClose: this._closeToolbar.bind(this)
|
|
1448
|
+
})
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
async _handleAction(action, options = {}) {
|
|
1452
|
+
if (!this.apiClient || !this.selectedText) return;
|
|
1453
|
+
this.isLoading = true;
|
|
1454
|
+
this._updateToolbar();
|
|
1455
|
+
try {
|
|
1456
|
+
let result;
|
|
1457
|
+
let originalText = this.selectedText;
|
|
1458
|
+
switch (action) {
|
|
1459
|
+
case "fix":
|
|
1460
|
+
result = await this.apiClient.correct(this.selectedText, "grammar");
|
|
1461
|
+
if (result.data.hasChanges) {
|
|
1462
|
+
this._replaceText(result.data.corrected);
|
|
1463
|
+
toastManager.success(`✓ ${result.data.changes.length} Korrekturen angewendet`, {
|
|
1464
|
+
description: `${result.usage?.remaining || 0} Credits übrig`,
|
|
1465
|
+
onUndo: () => this._replaceText(originalText),
|
|
1466
|
+
duration: 5e3
|
|
1467
|
+
});
|
|
1468
|
+
} else {
|
|
1469
|
+
toastManager.info("Text ist bereits korrekt");
|
|
1470
|
+
}
|
|
1471
|
+
break;
|
|
1472
|
+
case "rewrite":
|
|
1473
|
+
result = await this.apiClient.rewrite(this.selectedText, options);
|
|
1474
|
+
if (result.data.rewritten) {
|
|
1475
|
+
this._replaceText(result.data.rewritten);
|
|
1476
|
+
toastManager.success(`✨ Text umgeschrieben (${options.tone || "standard"})`, {
|
|
1477
|
+
onUndo: () => this._replaceText(originalText),
|
|
1478
|
+
duration: 5e3
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
break;
|
|
1482
|
+
case "expand":
|
|
1483
|
+
result = await this.apiClient.expand(this.selectedText);
|
|
1484
|
+
if (result.data.expanded) {
|
|
1485
|
+
this._replaceText(result.data.expanded);
|
|
1486
|
+
toastManager.success("📈 Text erweitert", {
|
|
1487
|
+
onUndo: () => this._replaceText(originalText),
|
|
1488
|
+
duration: 5e3
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
break;
|
|
1492
|
+
case "summarize":
|
|
1493
|
+
result = await this.apiClient.summarize(this.selectedText);
|
|
1494
|
+
if (result.data.summary) {
|
|
1495
|
+
this._replaceText(result.data.summary);
|
|
1496
|
+
toastManager.success("📉 Text zusammengefasst", {
|
|
1497
|
+
onUndo: () => this._replaceText(originalText),
|
|
1498
|
+
duration: 5e3
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
break;
|
|
1502
|
+
case "continue":
|
|
1503
|
+
result = await this.apiClient.continueWriting(this.selectedText);
|
|
1504
|
+
if (result.data.continuation) {
|
|
1505
|
+
this._appendText(" " + result.data.continuation);
|
|
1506
|
+
toastManager.success("✍️ Text fortgesetzt", {
|
|
1507
|
+
onUndo: () => this._replaceText(originalText),
|
|
1508
|
+
duration: 5e3
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
break;
|
|
1512
|
+
case "translate":
|
|
1513
|
+
const langNames = { en: "Englisch", de: "Deutsch", fr: "Französisch", es: "Spanisch" };
|
|
1514
|
+
result = await this.apiClient.translate(this.selectedText, options.language);
|
|
1515
|
+
if (result.data.translated) {
|
|
1516
|
+
this._replaceText(result.data.translated);
|
|
1517
|
+
toastManager.success(`🌍 Übersetzt zu ${langNames[options.language] || options.language}`, {
|
|
1518
|
+
onUndo: () => this._replaceText(originalText),
|
|
1519
|
+
duration: 5e3
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
break;
|
|
1523
|
+
default:
|
|
1524
|
+
console.warn("Unknown action:", action);
|
|
1525
|
+
}
|
|
1526
|
+
this._closeToolbar();
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
console.error("[AI Assistant] Action failed:", err);
|
|
1529
|
+
const isLimitError = err.code === "DAILY_LIMIT_EXCEEDED" || err.code === "MONTHLY_LIMIT_EXCEEDED";
|
|
1530
|
+
if (isLimitError) {
|
|
1531
|
+
toastManager.warning("Limit erreicht", {
|
|
1532
|
+
description: err.message,
|
|
1533
|
+
duration: 5e3
|
|
1534
|
+
});
|
|
1535
|
+
} else {
|
|
1536
|
+
toastManager.error("Fehler", {
|
|
1537
|
+
description: err.message || "Aktion fehlgeschlagen",
|
|
1538
|
+
duration: 4e3
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
} finally {
|
|
1542
|
+
this.isLoading = false;
|
|
1543
|
+
this._updateToolbar();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
_replaceText(newText) {
|
|
1547
|
+
if (!this.selectedRange) return;
|
|
1548
|
+
try {
|
|
1549
|
+
const selection = window.getSelection();
|
|
1550
|
+
selection.removeAllRanges();
|
|
1551
|
+
selection.addRange(this.selectedRange);
|
|
1552
|
+
document.execCommand("insertText", false, newText);
|
|
1553
|
+
this.api.blocks.getBlockByIndex(this.api.blocks.getCurrentBlockIndex())?.dispatchChange?.();
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
console.error("[AI Assistant] Failed to replace text:", err);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
_appendText(additionalText) {
|
|
1559
|
+
if (!this.selectedRange) return;
|
|
1560
|
+
try {
|
|
1561
|
+
const selection = window.getSelection();
|
|
1562
|
+
const range = this.selectedRange.cloneRange();
|
|
1563
|
+
range.collapse(false);
|
|
1564
|
+
selection.removeAllRanges();
|
|
1565
|
+
selection.addRange(range);
|
|
1566
|
+
document.execCommand("insertText", false, additionalText);
|
|
1567
|
+
this.api.blocks.getBlockByIndex(this.api.blocks.getCurrentBlockIndex())?.dispatchChange?.();
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
console.error("[AI Assistant] Failed to append text:", err);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
_updateToolbar() {
|
|
1573
|
+
if (this.toolbarRoot && this.toolbar) {
|
|
1574
|
+
const selection = window.getSelection();
|
|
1575
|
+
const rect = selection.rangeCount > 0 ? selection.getRangeAt(0).getBoundingClientRect() : { left: 0, bottom: 0, width: 0 };
|
|
1576
|
+
const position = {
|
|
1577
|
+
left: rect.left + rect.width / 2 - 200,
|
|
1578
|
+
top: rect.bottom + 8
|
|
1579
|
+
};
|
|
1580
|
+
this.toolbarRoot.render(
|
|
1581
|
+
React__default.createElement(AIInlineToolbar, {
|
|
1582
|
+
position,
|
|
1583
|
+
onAction: this._handleAction.bind(this),
|
|
1584
|
+
loading: this.isLoading,
|
|
1585
|
+
onClose: this._closeToolbar.bind(this)
|
|
1586
|
+
})
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
_closeToolbar() {
|
|
1591
|
+
if (this.toolbarRoot) {
|
|
1592
|
+
this.toolbarRoot.unmount();
|
|
1593
|
+
this.toolbarRoot = null;
|
|
1594
|
+
}
|
|
1595
|
+
if (this.toolbar) {
|
|
1596
|
+
this.toolbar.remove();
|
|
1597
|
+
this.toolbar = null;
|
|
1598
|
+
}
|
|
1599
|
+
this.isLoading = false;
|
|
1600
|
+
this.selectedRange = null;
|
|
1601
|
+
this.selectedText = "";
|
|
1602
|
+
this.currentBlockIndex = null;
|
|
1603
|
+
}
|
|
1604
|
+
checkState(selection) {
|
|
1605
|
+
this.state = selection && !selection.isCollapsed;
|
|
1606
|
+
return this.state;
|
|
1607
|
+
}
|
|
1608
|
+
get shortcut() {
|
|
1609
|
+
return "CMD+SHIFT+G";
|
|
1610
|
+
}
|
|
1611
|
+
destroy() {
|
|
1612
|
+
this._closeToolbar();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
class MediaLibAdapter {
|
|
1616
|
+
// Track if we're currently opening from user action vs restore
|
|
1617
|
+
static _isUserAction = false;
|
|
1618
|
+
/**
|
|
1619
|
+
* Toolbox configuration
|
|
1620
|
+
*/
|
|
1621
|
+
static get toolbox() {
|
|
1622
|
+
return {
|
|
1623
|
+
title: "Image (Media Library)",
|
|
1624
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="17" height="15" viewBox="0 0 336 276">
|
|
1625
|
+
<path d="M291 150.242V79c0-18.778-15.222-34-34-34H79c-18.778 0-34 15.222-34 34v42.264l67.179-44.192 80.398 71.614 56.686-29.14L291 150.242zm-.345 51.622l-42.3-30.246-56.3 29.884-80.773-66.925L45 174.187V197c0 18.778 15.222 34 34 34h178c17.126 0 31.295-12.663 33.655-29.136zM79 0h178c43.63 0 79 35.37 79 79v118c0 43.63-35.37 79-79 79H79c-43.63 0-79-35.37-79-79V79C0 35.37 35.37 0 79 0z"/>
|
|
1626
|
+
</svg>`
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Tool is not inline
|
|
1631
|
+
*/
|
|
1632
|
+
static get isInline() {
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Constructor
|
|
1637
|
+
* @param {object} params - Tool parameters
|
|
1638
|
+
* @param {object} params.api - EditorJS API
|
|
1639
|
+
* @param {object} params.config - Tool config
|
|
1640
|
+
* @param {object} params.data - Block data (when restoring from saved state)
|
|
1641
|
+
*/
|
|
1642
|
+
constructor({ api, config, data }) {
|
|
1643
|
+
this.api = api;
|
|
1644
|
+
this.config = config || {};
|
|
1645
|
+
this.data = data || {};
|
|
1646
|
+
this._isRestore = !!(data && data.type === "mediaLibraryStrapi");
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Render tool
|
|
1650
|
+
* Opens Media Library dialog only when user clicks the toolbox
|
|
1651
|
+
*/
|
|
1652
|
+
render() {
|
|
1653
|
+
const wrapper = document.createElement("div");
|
|
1654
|
+
wrapper.classList.add("media-lib-placeholder");
|
|
1655
|
+
if (!this._isRestore) {
|
|
1656
|
+
const currentIndex = this.api.blocks.getCurrentBlockIndex();
|
|
1657
|
+
if (this.config.mediaLibToggleFunc) {
|
|
1658
|
+
setTimeout(() => {
|
|
1659
|
+
this.config.mediaLibToggleFunc(currentIndex);
|
|
1660
|
+
}, 50);
|
|
1661
|
+
}
|
|
1662
|
+
wrapper.innerHTML = '<p style="color: #7C3AED; text-align: center; padding: 20px; font-size: 14px;">📷 Media Library wird geöffnet...</p>';
|
|
1663
|
+
} else {
|
|
1664
|
+
wrapper.innerHTML = '<p style="color: #999; text-align: center; padding: 10px; font-size: 12px;">⚠️ Ungültiger Block wird entfernt...</p>';
|
|
1665
|
+
setTimeout(() => {
|
|
1666
|
+
try {
|
|
1667
|
+
const currentIndex = this.api.blocks.getCurrentBlockIndex();
|
|
1668
|
+
this.api.blocks.delete(currentIndex);
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
console.warn("[Magic Editor X] Could not auto-delete mediaLib block:", e);
|
|
1671
|
+
}
|
|
1672
|
+
}, 100);
|
|
1673
|
+
}
|
|
1674
|
+
return wrapper;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Save data
|
|
1678
|
+
* Returns undefined to prevent saving this placeholder block
|
|
1679
|
+
*/
|
|
1680
|
+
save() {
|
|
1681
|
+
return void 0;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Validate saved data
|
|
1685
|
+
* Always return false so this block type is never persisted
|
|
1686
|
+
*/
|
|
1687
|
+
validate(savedData) {
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Sanitizer config
|
|
1692
|
+
*/
|
|
1693
|
+
static get sanitize() {
|
|
1694
|
+
return {
|
|
1695
|
+
type: false
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const getAuthToken = () => {
|
|
1700
|
+
try {
|
|
1701
|
+
const token = sessionStorage.getItem("jwtToken");
|
|
1702
|
+
if (token) {
|
|
1703
|
+
return JSON.parse(token);
|
|
1704
|
+
}
|
|
1705
|
+
const localToken = localStorage.getItem("jwtToken");
|
|
1706
|
+
if (localToken) {
|
|
1707
|
+
return JSON.parse(localToken);
|
|
1708
|
+
}
|
|
1709
|
+
return null;
|
|
1710
|
+
} catch (e) {
|
|
1711
|
+
console.warn("[Magic Editor X] Could not get auth token:", e);
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
const getTools = ({ mediaLibToggleFunc, pluginId }) => {
|
|
1716
|
+
const token = getAuthToken();
|
|
1717
|
+
const authHeader = token ? `Bearer ${token}` : "";
|
|
1718
|
+
return {
|
|
1719
|
+
// ============================================
|
|
1720
|
+
// OFFICIAL BLOCK TOOLS (16 Tools)
|
|
1721
|
+
// ============================================
|
|
1722
|
+
/**
|
|
1723
|
+
* Header Tool
|
|
1724
|
+
* @see https://github.com/editor-js/header
|
|
1725
|
+
*/
|
|
1726
|
+
header: {
|
|
1727
|
+
class: Header,
|
|
1728
|
+
inlineToolbar: true,
|
|
1729
|
+
tunes: ["alignmentTune"],
|
|
1730
|
+
config: {
|
|
1731
|
+
placeholder: "Enter a heading",
|
|
1732
|
+
levels: [1, 2, 3, 4, 5, 6],
|
|
1733
|
+
defaultLevel: 2
|
|
1734
|
+
},
|
|
1735
|
+
shortcut: "CMD+SHIFT+H"
|
|
1736
|
+
},
|
|
1737
|
+
/**
|
|
1738
|
+
* Paragraph Tool (default)
|
|
1739
|
+
* @see https://github.com/editor-js/paragraph
|
|
1740
|
+
*/
|
|
1741
|
+
paragraph: {
|
|
1742
|
+
class: Paragraph,
|
|
1743
|
+
inlineToolbar: true,
|
|
1744
|
+
tunes: ["alignmentTune", "indentTune"],
|
|
1745
|
+
config: {
|
|
1746
|
+
placeholder: "Start writing or press Tab to add a block...",
|
|
1747
|
+
preserveBlank: true
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1750
|
+
/**
|
|
1751
|
+
* Nested List Tool
|
|
1752
|
+
* @see https://github.com/editor-js/nested-list
|
|
1753
|
+
*/
|
|
1754
|
+
list: {
|
|
1755
|
+
class: NestedList,
|
|
1756
|
+
inlineToolbar: true,
|
|
1757
|
+
tunes: ["indentTune"],
|
|
1758
|
+
config: {
|
|
1759
|
+
defaultStyle: "unordered"
|
|
1760
|
+
},
|
|
1761
|
+
shortcut: "CMD+SHIFT+L"
|
|
1762
|
+
},
|
|
1763
|
+
/**
|
|
1764
|
+
* Checklist Tool
|
|
1765
|
+
* @see https://github.com/editor-js/checklist
|
|
1766
|
+
*/
|
|
1767
|
+
checklist: {
|
|
1768
|
+
class: Checklist,
|
|
1769
|
+
inlineToolbar: true,
|
|
1770
|
+
shortcut: "CMD+SHIFT+C"
|
|
1771
|
+
},
|
|
1772
|
+
/**
|
|
1773
|
+
* Quote Tool
|
|
1774
|
+
* @see https://github.com/editor-js/quote
|
|
1775
|
+
*/
|
|
1776
|
+
quote: {
|
|
1777
|
+
class: Quote,
|
|
1778
|
+
inlineToolbar: true,
|
|
1779
|
+
tunes: ["alignmentTune"],
|
|
1780
|
+
config: {
|
|
1781
|
+
quotePlaceholder: "Enter a quote",
|
|
1782
|
+
captionPlaceholder: "Quote author"
|
|
1783
|
+
},
|
|
1784
|
+
shortcut: "CMD+SHIFT+Q"
|
|
1785
|
+
},
|
|
1786
|
+
/**
|
|
1787
|
+
* Warning Tool
|
|
1788
|
+
* @see https://github.com/editor-js/warning
|
|
1789
|
+
*/
|
|
1790
|
+
warning: {
|
|
1791
|
+
class: Warning,
|
|
1792
|
+
inlineToolbar: true,
|
|
1793
|
+
config: {
|
|
1794
|
+
titlePlaceholder: "Warning title",
|
|
1795
|
+
messagePlaceholder: "Warning message"
|
|
1796
|
+
},
|
|
1797
|
+
shortcut: "CMD+SHIFT+W"
|
|
1798
|
+
},
|
|
1799
|
+
/**
|
|
1800
|
+
* Code Tool (basic)
|
|
1801
|
+
* @see https://github.com/editor-js/code
|
|
1802
|
+
*/
|
|
1803
|
+
code: {
|
|
1804
|
+
class: Code,
|
|
1805
|
+
config: {
|
|
1806
|
+
placeholder: "Enter code here..."
|
|
1807
|
+
},
|
|
1808
|
+
shortcut: "CMD+SHIFT+P"
|
|
1809
|
+
},
|
|
1810
|
+
/**
|
|
1811
|
+
* Delimiter Tool
|
|
1812
|
+
* @see https://github.com/editor-js/delimiter
|
|
1813
|
+
*/
|
|
1814
|
+
delimiter: {
|
|
1815
|
+
class: Delimiter,
|
|
1816
|
+
shortcut: "CMD+SHIFT+D"
|
|
1817
|
+
},
|
|
1818
|
+
/**
|
|
1819
|
+
* Table Tool
|
|
1820
|
+
* @see https://github.com/editor-js/table
|
|
1821
|
+
*/
|
|
1822
|
+
table: {
|
|
1823
|
+
class: Table,
|
|
1824
|
+
inlineToolbar: true,
|
|
1825
|
+
config: {
|
|
1826
|
+
rows: 2,
|
|
1827
|
+
cols: 3,
|
|
1828
|
+
withHeadings: true
|
|
1829
|
+
},
|
|
1830
|
+
shortcut: "CMD+SHIFT+T"
|
|
1831
|
+
},
|
|
1832
|
+
/**
|
|
1833
|
+
* Embed Tool
|
|
1834
|
+
* @see https://github.com/editor-js/embed
|
|
1835
|
+
*/
|
|
1836
|
+
embed: {
|
|
1837
|
+
class: Embed,
|
|
1838
|
+
config: {
|
|
1839
|
+
services: {
|
|
1840
|
+
youtube: true,
|
|
1841
|
+
vimeo: true,
|
|
1842
|
+
twitter: true,
|
|
1843
|
+
instagram: true,
|
|
1844
|
+
codepen: true,
|
|
1845
|
+
codesandbox: true,
|
|
1846
|
+
github: true,
|
|
1847
|
+
gfycat: true,
|
|
1848
|
+
imgur: true,
|
|
1849
|
+
pinterest: true,
|
|
1850
|
+
twitch: true,
|
|
1851
|
+
miro: true,
|
|
1852
|
+
figma: true,
|
|
1853
|
+
aparat: true,
|
|
1854
|
+
facebook: true
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
/**
|
|
1859
|
+
* Raw HTML Tool
|
|
1860
|
+
* @see https://github.com/editor-js/raw
|
|
1861
|
+
*/
|
|
1862
|
+
raw: {
|
|
1863
|
+
class: Raw,
|
|
1864
|
+
config: {
|
|
1865
|
+
placeholder: "Enter raw HTML..."
|
|
1866
|
+
}
|
|
1867
|
+
},
|
|
1868
|
+
/**
|
|
1869
|
+
* Link Tool
|
|
1870
|
+
* @see https://github.com/editor-js/link
|
|
1871
|
+
*/
|
|
1872
|
+
linkTool: {
|
|
1873
|
+
class: LinkTool,
|
|
1874
|
+
config: {
|
|
1875
|
+
endpoint: `/api/${pluginId}/link`,
|
|
1876
|
+
headers: {
|
|
1877
|
+
Authorization: authHeader
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1881
|
+
/**
|
|
1882
|
+
* Image Tool (with Upload)
|
|
1883
|
+
* @see https://github.com/editor-js/image
|
|
1884
|
+
*/
|
|
1885
|
+
image: {
|
|
1886
|
+
class: Image,
|
|
1887
|
+
config: {
|
|
1888
|
+
field: "files.image",
|
|
1889
|
+
additionalRequestData: {
|
|
1890
|
+
data: JSON.stringify({})
|
|
1891
|
+
},
|
|
1892
|
+
additionalRequestHeaders: {
|
|
1893
|
+
Authorization: authHeader
|
|
1894
|
+
},
|
|
1895
|
+
endpoints: {
|
|
1896
|
+
byUrl: `/api/${pluginId}/image/byUrl`
|
|
1897
|
+
},
|
|
1898
|
+
uploader: {
|
|
1899
|
+
async uploadByFile(file) {
|
|
1900
|
+
const formData = new FormData();
|
|
1901
|
+
formData.append("data", JSON.stringify({}));
|
|
1902
|
+
formData.append("files.image", file);
|
|
1903
|
+
try {
|
|
1904
|
+
const response = await fetch(`/api/${pluginId}/image/byFile`, {
|
|
1905
|
+
method: "POST",
|
|
1906
|
+
headers: {
|
|
1907
|
+
Authorization: authHeader
|
|
1908
|
+
},
|
|
1909
|
+
body: formData
|
|
1910
|
+
});
|
|
1911
|
+
return await response.json();
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
console.error("[Magic Editor X] Upload error:", error);
|
|
1914
|
+
return { success: 0, message: error.message };
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
captionPlaceholder: "Image caption",
|
|
1919
|
+
buttonContent: "Select image"
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
/**
|
|
1923
|
+
* Simple Image Tool (URL only)
|
|
1924
|
+
* @see https://github.com/editor-js/simple-image
|
|
1925
|
+
*/
|
|
1926
|
+
simpleImage: {
|
|
1927
|
+
class: SimpleImage
|
|
1928
|
+
},
|
|
1929
|
+
/**
|
|
1930
|
+
* Attaches Tool
|
|
1931
|
+
* @see https://github.com/editor-js/attaches
|
|
1932
|
+
*/
|
|
1933
|
+
attaches: {
|
|
1934
|
+
class: Attaches,
|
|
1935
|
+
config: {
|
|
1936
|
+
endpoint: `/api/${pluginId}/file/upload`,
|
|
1937
|
+
field: "file",
|
|
1938
|
+
types: "*",
|
|
1939
|
+
buttonText: "Select file to upload",
|
|
1940
|
+
errorMessage: "File upload failed",
|
|
1941
|
+
additionalRequestHeaders: {
|
|
1942
|
+
Authorization: authHeader
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
},
|
|
1946
|
+
/**
|
|
1947
|
+
* Personality Tool
|
|
1948
|
+
* @see https://github.com/editor-js/personality
|
|
1949
|
+
*/
|
|
1950
|
+
personality: {
|
|
1951
|
+
class: Personality,
|
|
1952
|
+
config: {
|
|
1953
|
+
endpoint: `/api/${pluginId}/image/byFile`,
|
|
1954
|
+
field: "files.image",
|
|
1955
|
+
additionalRequestHeaders: {
|
|
1956
|
+
Authorization: authHeader
|
|
1957
|
+
},
|
|
1958
|
+
namePlaceholder: "Name",
|
|
1959
|
+
descriptionPlaceholder: "Description / Title",
|
|
1960
|
+
linkPlaceholder: "Link (optional)"
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
/**
|
|
1964
|
+
* Media Library Tool (Custom)
|
|
1965
|
+
* Strapi Media Library integration
|
|
1966
|
+
*/
|
|
1967
|
+
mediaLib: {
|
|
1968
|
+
class: MediaLibAdapter,
|
|
1969
|
+
config: {
|
|
1970
|
+
mediaLibToggleFunc
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
// ============================================
|
|
1974
|
+
// COMMUNITY BLOCK TOOLS (4 Tools)
|
|
1975
|
+
// ============================================
|
|
1976
|
+
/**
|
|
1977
|
+
* Alert Tool
|
|
1978
|
+
* @see https://github.com/vishaltelangre/editorjs-alert
|
|
1979
|
+
* Colorful alert boxes (info, success, warning, danger)
|
|
1980
|
+
*/
|
|
1981
|
+
alert: {
|
|
1982
|
+
class: Alert,
|
|
1983
|
+
inlineToolbar: true,
|
|
1984
|
+
config: {
|
|
1985
|
+
defaultType: "info",
|
|
1986
|
+
messagePlaceholder: "Enter your message..."
|
|
1987
|
+
},
|
|
1988
|
+
shortcut: "CMD+SHIFT+A"
|
|
1989
|
+
},
|
|
1990
|
+
/**
|
|
1991
|
+
* Toggle Block Tool
|
|
1992
|
+
* @see https://github.com/kommitters/editorjs-toggle-block
|
|
1993
|
+
* Collapsible/expandable content blocks (FAQ, Accordions)
|
|
1994
|
+
*/
|
|
1995
|
+
toggle: {
|
|
1996
|
+
class: ToggleBlock,
|
|
1997
|
+
inlineToolbar: true,
|
|
1998
|
+
config: {
|
|
1999
|
+
placeholder: "Toggle title",
|
|
2000
|
+
defaultContent: "Toggle content..."
|
|
2001
|
+
}
|
|
2002
|
+
},
|
|
2003
|
+
/**
|
|
2004
|
+
* CodeFlask Tool (with Syntax Highlighting)
|
|
2005
|
+
* @see https://github.com/calumk/editorjs-codeflask
|
|
2006
|
+
* Code blocks with syntax highlighting
|
|
2007
|
+
*/
|
|
2008
|
+
codeFlask: {
|
|
2009
|
+
class: CodeFlask,
|
|
2010
|
+
config: {
|
|
2011
|
+
language: "javascript"
|
|
2012
|
+
}
|
|
2013
|
+
},
|
|
2014
|
+
/**
|
|
2015
|
+
* Button Tool (Custom Implementation)
|
|
2016
|
+
* CTA buttons with customizable text, link, and style
|
|
2017
|
+
* Secure implementation without eval()
|
|
2018
|
+
*/
|
|
2019
|
+
button: {
|
|
2020
|
+
class: ButtonTool,
|
|
2021
|
+
inlineToolbar: false
|
|
2022
|
+
},
|
|
2023
|
+
// ============================================
|
|
2024
|
+
// OFFICIAL INLINE TOOLS (3 Tools)
|
|
2025
|
+
// ============================================
|
|
2026
|
+
/**
|
|
2027
|
+
* Marker (Highlight) Tool
|
|
2028
|
+
* @see https://github.com/editor-js/marker
|
|
2029
|
+
*/
|
|
2030
|
+
marker: {
|
|
2031
|
+
class: Marker,
|
|
2032
|
+
shortcut: "CMD+SHIFT+M"
|
|
2033
|
+
},
|
|
2034
|
+
/**
|
|
2035
|
+
* Inline Code Tool
|
|
2036
|
+
* @see https://github.com/editor-js/inline-code
|
|
2037
|
+
*/
|
|
2038
|
+
inlineCode: {
|
|
2039
|
+
class: InlineCode,
|
|
2040
|
+
shortcut: "CMD+SHIFT+I"
|
|
2041
|
+
},
|
|
2042
|
+
/**
|
|
2043
|
+
* Underline Tool
|
|
2044
|
+
* @see https://github.com/editor-js/underline
|
|
2045
|
+
*/
|
|
2046
|
+
underline: {
|
|
2047
|
+
class: Underline,
|
|
2048
|
+
shortcut: "CMD+U"
|
|
2049
|
+
},
|
|
2050
|
+
// ============================================
|
|
2051
|
+
// COMMUNITY INLINE TOOLS (3 Tools)
|
|
2052
|
+
// ============================================
|
|
2053
|
+
/**
|
|
2054
|
+
* Strikethrough Tool
|
|
2055
|
+
* @see https://github.com/nicosrm/strikethrough
|
|
2056
|
+
*/
|
|
2057
|
+
strikethrough: {
|
|
2058
|
+
class: Strikethrough,
|
|
2059
|
+
shortcut: "CMD+SHIFT+S"
|
|
2060
|
+
},
|
|
2061
|
+
/**
|
|
2062
|
+
* Tooltip Tool
|
|
2063
|
+
* @see https://github.com/kommitters/editorjs-tooltip
|
|
2064
|
+
* Add tooltips to text
|
|
2065
|
+
*/
|
|
2066
|
+
tooltip: {
|
|
2067
|
+
class: Tooltip,
|
|
2068
|
+
config: {
|
|
2069
|
+
location: "top",
|
|
2070
|
+
highlightColor: "#FFEFD5",
|
|
2071
|
+
underline: true,
|
|
2072
|
+
backgroundColor: "#1e293b",
|
|
2073
|
+
textColor: "#ffffff"
|
|
2074
|
+
}
|
|
2075
|
+
},
|
|
2076
|
+
/**
|
|
2077
|
+
* Hyperlink Tool (Custom Implementation)
|
|
2078
|
+
* Links with target and rel attributes
|
|
2079
|
+
* Secure implementation without eval()
|
|
2080
|
+
*/
|
|
2081
|
+
hyperlink: {
|
|
2082
|
+
class: HyperlinkTool,
|
|
2083
|
+
config: {
|
|
2084
|
+
shortcut: "CMD+K",
|
|
2085
|
+
target: "_blank",
|
|
2086
|
+
rel: "noopener noreferrer",
|
|
2087
|
+
availableTargets: ["_blank", "_self", "_parent", "_top"],
|
|
2088
|
+
availableRels: ["nofollow", "noreferrer", "noopener", "sponsored", "ugc"]
|
|
2089
|
+
}
|
|
2090
|
+
},
|
|
2091
|
+
/**
|
|
2092
|
+
* AI Assistant Tool (Custom Implementation)
|
|
2093
|
+
* AI-powered text corrections (grammar, style, rewrite)
|
|
2094
|
+
*/
|
|
2095
|
+
aiAssistant: {
|
|
2096
|
+
class: AIAssistantTool,
|
|
2097
|
+
config: {
|
|
2098
|
+
apiBaseUrl: "https://magicapi.fitlex.me/api/magic-editor",
|
|
2099
|
+
getLicenseKey: () => window.__MAGIC_EDITOR_LICENSE_KEY__
|
|
2100
|
+
},
|
|
2101
|
+
shortcut: "CMD+SHIFT+G"
|
|
2102
|
+
},
|
|
2103
|
+
// ============================================
|
|
2104
|
+
// TUNES (3 Tunes)
|
|
2105
|
+
// ============================================
|
|
2106
|
+
/**
|
|
2107
|
+
* Text Variant Tune
|
|
2108
|
+
* @see https://github.com/editor-js/text-variant-tune
|
|
2109
|
+
*/
|
|
2110
|
+
textVariant: TextVariantTune,
|
|
2111
|
+
/**
|
|
2112
|
+
* Alignment Tune
|
|
2113
|
+
* @see https://github.com/kaaaaaaaaaaai/editorjs-alignment-blocktune
|
|
2114
|
+
* Text alignment (left, center, right, justify)
|
|
2115
|
+
*/
|
|
2116
|
+
alignmentTune: {
|
|
2117
|
+
class: AlignmentTune,
|
|
2118
|
+
config: {
|
|
2119
|
+
default: "left",
|
|
2120
|
+
blocks: {
|
|
2121
|
+
header: "center"
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
},
|
|
2125
|
+
/**
|
|
2126
|
+
* Indent Tune
|
|
2127
|
+
* @see https://github.com/kommitters/editorjs-indent-tune
|
|
2128
|
+
* Block indentation
|
|
2129
|
+
*/
|
|
2130
|
+
indentTune: {
|
|
2131
|
+
class: IndentTune,
|
|
2132
|
+
config: {
|
|
2133
|
+
maxIndent: 5,
|
|
2134
|
+
indentSize: 30,
|
|
2135
|
+
multiblock: true,
|
|
2136
|
+
tuneName: "indentTune"
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
};
|
|
2141
|
+
const initUndoRedo = (editor) => {
|
|
2142
|
+
return new Undo({ editor });
|
|
2143
|
+
};
|
|
2144
|
+
const initDragDrop = (editor) => {
|
|
2145
|
+
return new DragDrop(editor);
|
|
2146
|
+
};
|
|
2147
|
+
export {
|
|
2148
|
+
AIToast as A,
|
|
2149
|
+
MagicEditorAPI as M,
|
|
2150
|
+
initDragDrop as a,
|
|
2151
|
+
AIInlineToolbar as b,
|
|
2152
|
+
getTools as g,
|
|
2153
|
+
initUndoRedo as i,
|
|
2154
|
+
toastManager as t
|
|
2155
|
+
};
|