magic-editor-x 1.1.0 → 1.2.2
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/README.md +83 -6
- package/dist/_chunks/{App-mtrlABtd.js → App-BHNqY71z.js} +4 -4
- package/dist/_chunks/{App-B1FgOsWa.mjs → App-LgFoHtyD.mjs} +4 -4
- package/dist/_chunks/{LicensePage-BnyWSrWs.js → LicensePage-VwKQMnUO.js} +2 -2
- package/dist/_chunks/{LicensePage-CWH-AFR-.mjs → LicensePage-oxUnaZmr.mjs} +2 -2
- package/dist/_chunks/{LiveCollaborationPanel-DbDHwr2C.js → LiveCollaborationPanel-CqtkFWJs.js} +1 -1
- package/dist/_chunks/{LiveCollaborationPanel-ryjcDAA7.mjs → LiveCollaborationPanel-elejZRkh.mjs} +1 -1
- package/dist/_chunks/{Settings-Bk9bxJTy.js → Settings-4wUHMbn0.js} +1 -1
- package/dist/_chunks/{Settings-D-V2MLVm.mjs → Settings-BI9zxX3k.mjs} +1 -1
- package/dist/_chunks/{de-CSrHZWEb.mjs → de-C_0Mj-Zo.mjs} +1 -0
- package/dist/_chunks/{de-CzSo1oD2.js → de-DVNVpAt9.js} +1 -0
- package/dist/_chunks/{en-DuQun2v4.mjs → en-B7AAf1ie.mjs} +1 -0
- package/dist/_chunks/{en-DxIkVPUh.js → en-C2hv5GsA.js} +1 -0
- package/dist/_chunks/{es-DAQ_97zx.js → es-BzJqcIST.js} +1 -0
- package/dist/_chunks/{es-DEB0CA8S.mjs → es-zdP8sd-f.mjs} +1 -0
- package/dist/_chunks/{fr-Bqkhvdx2.mjs → fr-CtAgOgH1.mjs} +1 -0
- package/dist/_chunks/{fr-ChPabvNP.js → fr-D3GwqAAJ.js} +1 -0
- package/dist/_chunks/{getTranslation-C4uWR0DB.mjs → getTranslation-ChB_HlBd.mjs} +7 -6
- package/dist/_chunks/{getTranslation-D35vbDap.js → getTranslation-DxG1pB5q.js} +2 -1
- package/dist/_chunks/{index-BiLy_f7C.js → index-C_SiBh7v.js} +9 -9
- package/dist/_chunks/{index-B5MzUyo0.mjs → index-CtyxDZ0S.mjs} +9 -9
- package/dist/_chunks/{index-CQx7-dFP.js → index-DGRg45vZ.js} +890 -65
- package/dist/_chunks/{index-BRVqbnOb.mjs → index-IYdGq7Rl.mjs} +891 -66
- package/dist/_chunks/{pt-BMoYltav.mjs → pt-BmUw6YMP.mjs} +1 -0
- package/dist/_chunks/{pt-Cm74LpyZ.js → pt-CFo0nTbj.js} +1 -0
- package/dist/_chunks/{tools-CjnQJ9w2.mjs → tools-BAvbiUHr.mjs} +6 -1
- package/dist/_chunks/{tools-DNt2tioN.js → tools-Dn4jPdJs.js} +6 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +37244 -1293
- package/dist/server/index.mjs +37230 -1286
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React__default, { useState, useEffect, useCallback, useRef, useMemo, forwardRef } from "react";
|
|
3
|
-
import { u as useIntl, F as
|
|
3
|
+
import { u as useIntl, F as Flex, T as Typography, B as Button$2, D as Divider, a as Box, g as getTranslation, b as Field, L as Loader } from "./getTranslation-ChB_HlBd.mjs";
|
|
4
4
|
import styled, { createGlobalStyle, css } from "styled-components";
|
|
5
|
-
import { SparklesIcon as SparklesIcon$1, Bars3BottomLeftIcon, ListBulletIcon, CheckCircleIcon, PhotoIcon, LinkIcon, CodeBracketIcon, TableCellsIcon, ChatBubbleBottomCenterTextIcon,
|
|
5
|
+
import { ClockIcon, ExclamationTriangleIcon, SparklesIcon as SparklesIcon$1, Bars3BottomLeftIcon, ListBulletIcon, CheckCircleIcon, PhotoIcon, LinkIcon, CodeBracketIcon, TableCellsIcon, ChatBubbleBottomCenterTextIcon, MinusIcon, DocumentDuplicateIcon, TrashIcon, ArrowsPointingInIcon, ArrowsPointingOutIcon, EyeIcon, PencilSquareIcon } from "@heroicons/react/24/outline";
|
|
6
6
|
import EditorJS from "@editorjs/editorjs";
|
|
7
|
-
import { M as MagicEditorAPI, t as toastManager, g as getTools, i as initUndoRedo, a as initDragDrop, A as AIToast, b as AIInlineToolbar } from "./tools-
|
|
7
|
+
import { M as MagicEditorAPI, t as toastManager, g as getTools, i as initUndoRedo, a as initDragDrop, A as AIToast, b as AIInlineToolbar } from "./tools-BAvbiUHr.mjs";
|
|
8
8
|
import { useStrapiApp, useFetchClient, useAuth } from "@strapi/strapi/admin";
|
|
9
|
-
import { P as PLUGIN_ID } from "./index-
|
|
9
|
+
import { P as PLUGIN_ID } from "./index-CtyxDZ0S.mjs";
|
|
10
10
|
import { io } from "socket.io-client";
|
|
11
11
|
import * as Y from "yjs";
|
|
12
12
|
import { IndexeddbPersistence } from "y-indexeddb";
|
|
@@ -424,6 +424,89 @@ const getUserColor = (userId) => {
|
|
|
424
424
|
const hash = String(userId).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
425
425
|
return CURSOR_COLORS[hash % CURSOR_COLORS.length];
|
|
426
426
|
};
|
|
427
|
+
const htmlToDelta = (html) => {
|
|
428
|
+
if (!html || html === "<br>" || html === "<br/>") return [];
|
|
429
|
+
const parser = new DOMParser();
|
|
430
|
+
const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
|
|
431
|
+
const delta = [];
|
|
432
|
+
function processNode(node, attributes = {}) {
|
|
433
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
434
|
+
const text = node.textContent;
|
|
435
|
+
if (text) {
|
|
436
|
+
const attrs = Object.keys(attributes).length > 0 ? { ...attributes } : void 0;
|
|
437
|
+
delta.push({ insert: text, attributes: attrs });
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
442
|
+
const newAttrs = { ...attributes };
|
|
443
|
+
const tagName = node.tagName.toLowerCase();
|
|
444
|
+
switch (tagName) {
|
|
445
|
+
case "b":
|
|
446
|
+
case "strong":
|
|
447
|
+
newAttrs.bold = true;
|
|
448
|
+
break;
|
|
449
|
+
case "i":
|
|
450
|
+
case "em":
|
|
451
|
+
newAttrs.italic = true;
|
|
452
|
+
break;
|
|
453
|
+
case "u":
|
|
454
|
+
newAttrs.underline = true;
|
|
455
|
+
break;
|
|
456
|
+
case "code":
|
|
457
|
+
newAttrs.code = true;
|
|
458
|
+
break;
|
|
459
|
+
case "a":
|
|
460
|
+
newAttrs.a = {
|
|
461
|
+
href: node.getAttribute("href") || "",
|
|
462
|
+
target: node.getAttribute("target") || "_blank",
|
|
463
|
+
rel: node.getAttribute("rel") || "noopener noreferrer"
|
|
464
|
+
};
|
|
465
|
+
break;
|
|
466
|
+
case "mark":
|
|
467
|
+
newAttrs.mark = true;
|
|
468
|
+
break;
|
|
469
|
+
case "br":
|
|
470
|
+
delta.push({ insert: "\n" });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
for (const child of node.childNodes) {
|
|
474
|
+
processNode(child, newAttrs);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const wrapper = doc.body.firstChild;
|
|
479
|
+
if (wrapper) {
|
|
480
|
+
for (const child of wrapper.childNodes) {
|
|
481
|
+
processNode(child);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return delta;
|
|
485
|
+
};
|
|
486
|
+
const deltaToHtml = (delta) => {
|
|
487
|
+
if (!delta || delta.length === 0) return "";
|
|
488
|
+
let html = "";
|
|
489
|
+
for (const op of delta) {
|
|
490
|
+
if (typeof op.insert !== "string") continue;
|
|
491
|
+
let text = op.insert;
|
|
492
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br>");
|
|
493
|
+
const attrs = op.attributes || {};
|
|
494
|
+
let result = text;
|
|
495
|
+
if (attrs.code) result = `<code>${result}</code>`;
|
|
496
|
+
if (attrs.italic) result = `<i>${result}</i>`;
|
|
497
|
+
if (attrs.bold) result = `<b>${result}</b>`;
|
|
498
|
+
if (attrs.underline) result = `<u>${result}</u>`;
|
|
499
|
+
if (attrs.mark) result = `<mark>${result}</mark>`;
|
|
500
|
+
if (attrs.a) {
|
|
501
|
+
const href = attrs.a.href || "";
|
|
502
|
+
const target = attrs.a.target || "_blank";
|
|
503
|
+
const rel = attrs.a.rel || "noopener noreferrer";
|
|
504
|
+
result = `<a href="${href}" target="${target}" rel="${rel}">${result}</a>`;
|
|
505
|
+
}
|
|
506
|
+
html += result;
|
|
507
|
+
}
|
|
508
|
+
return html;
|
|
509
|
+
};
|
|
427
510
|
const useMagicCollaboration = ({
|
|
428
511
|
enabled,
|
|
429
512
|
roomId,
|
|
@@ -448,16 +531,47 @@ const useMagicCollaboration = ({
|
|
|
448
531
|
useEffect(() => {
|
|
449
532
|
onRemoteUpdateRef.current = onRemoteUpdate;
|
|
450
533
|
}, [onRemoteUpdate]);
|
|
451
|
-
const { doc, blocksMap, metaMap } = useMemo(() => {
|
|
534
|
+
const { doc, blocksMap, textMap, metaMap } = useMemo(() => {
|
|
452
535
|
const yDoc = new Y.Doc();
|
|
453
536
|
return {
|
|
454
537
|
doc: yDoc,
|
|
455
538
|
blocksMap: yDoc.getMap("blocks"),
|
|
456
|
-
//
|
|
539
|
+
// Block metadata (type, tunes)
|
|
540
|
+
textMap: yDoc.getMap("text"),
|
|
541
|
+
// Y.Text per block (character-level sync!)
|
|
457
542
|
metaMap: yDoc.getMap("meta")
|
|
458
|
-
//
|
|
543
|
+
// Document metadata (time, blockOrder)
|
|
459
544
|
};
|
|
460
545
|
}, [roomId]);
|
|
546
|
+
const getBlockText = useCallback((blockId) => {
|
|
547
|
+
if (!blockId) return null;
|
|
548
|
+
let ytext = textMap.get(blockId);
|
|
549
|
+
if (!ytext) {
|
|
550
|
+
ytext = new Y.Text();
|
|
551
|
+
textMap.set(blockId, ytext);
|
|
552
|
+
}
|
|
553
|
+
return ytext;
|
|
554
|
+
}, [textMap]);
|
|
555
|
+
const setBlockText = useCallback((blockId, html) => {
|
|
556
|
+
if (!blockId) return;
|
|
557
|
+
const ytext = getBlockText(blockId);
|
|
558
|
+
if (!ytext) return;
|
|
559
|
+
doc.transact(() => {
|
|
560
|
+
if (ytext.length > 0) {
|
|
561
|
+
ytext.delete(0, ytext.length);
|
|
562
|
+
}
|
|
563
|
+
const delta = htmlToDelta(html);
|
|
564
|
+
if (delta.length > 0) {
|
|
565
|
+
ytext.applyDelta(delta);
|
|
566
|
+
}
|
|
567
|
+
}, "local");
|
|
568
|
+
}, [doc, getBlockText]);
|
|
569
|
+
const getBlockTextHtml = useCallback((blockId) => {
|
|
570
|
+
if (!blockId) return "";
|
|
571
|
+
const ytext = textMap.get(blockId);
|
|
572
|
+
if (!ytext) return "";
|
|
573
|
+
return deltaToHtml(ytext.toDelta());
|
|
574
|
+
}, [textMap]);
|
|
461
575
|
useEffect(() => {
|
|
462
576
|
return () => {
|
|
463
577
|
doc.destroy();
|
|
@@ -522,9 +636,6 @@ const useMagicCollaboration = ({
|
|
|
522
636
|
persistenceRef.current = persistence;
|
|
523
637
|
persistence.on("synced", () => {
|
|
524
638
|
console.log("[Magic Collab] [CACHE] IndexedDB synced for room:", roomId);
|
|
525
|
-
const blockOrder = metaMap.get("blockOrder");
|
|
526
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:indexeddb:synced", message: "IndexedDB synced - loaded local state", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder, roomId, persistenceKey }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "H" }) }).catch(() => {
|
|
527
|
-
});
|
|
528
639
|
});
|
|
529
640
|
console.log("[Magic Collab] [CACHE] IndexedDB persistence initialized:", persistenceKey);
|
|
530
641
|
} catch (e) {
|
|
@@ -611,17 +722,11 @@ const useMagicCollaboration = ({
|
|
|
611
722
|
if (update) {
|
|
612
723
|
console.log("[Magic Collab] [SYNC] Syncing initial state, update size:", update.length, "bytes");
|
|
613
724
|
try {
|
|
614
|
-
const blockOrderBefore = metaMap.get("blockOrder");
|
|
615
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:sync:before", message: "BEFORE applying server sync", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: blockOrderBefore, roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "F" }) }).catch(() => {
|
|
616
|
-
});
|
|
617
725
|
const beforeBlockCount = blocksMap.size;
|
|
618
726
|
console.log("[Magic Collab] [DATA] Y.Map BEFORE sync - block count:", beforeBlockCount);
|
|
619
727
|
Y.applyUpdate(doc, new Uint8Array(update), "remote");
|
|
620
728
|
const afterBlockCount = blocksMap.size;
|
|
621
729
|
console.log("[Magic Collab] [DATA] Y.Map AFTER sync - block count:", afterBlockCount);
|
|
622
|
-
const blockOrderAfter = metaMap.get("blockOrder");
|
|
623
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:sync:after", message: "AFTER applying server sync", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: blockOrderAfter, roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "F" }) }).catch(() => {
|
|
624
|
-
});
|
|
625
730
|
if (onRemoteUpdateRef.current) {
|
|
626
731
|
console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback after sync");
|
|
627
732
|
setTimeout(() => {
|
|
@@ -637,15 +742,11 @@ const useMagicCollaboration = ({
|
|
|
637
742
|
if (update) {
|
|
638
743
|
console.log("[Magic Collab] [UPDATE] Received remote update:", update.length, "bytes");
|
|
639
744
|
try {
|
|
640
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:update:before", message: "BEFORE applying remote update", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: metaMap.get("blockOrder"), updateSize: update.length, roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "G" }) }).catch(() => {
|
|
641
|
-
});
|
|
642
745
|
const beforeBlockCount = blocksMap.size;
|
|
643
746
|
console.log("[Magic Collab] [DATA] Y.Map BEFORE update - blocks:", beforeBlockCount);
|
|
644
747
|
Y.applyUpdate(doc, new Uint8Array(update), "remote");
|
|
645
748
|
const afterBlockCount = blocksMap.size;
|
|
646
749
|
console.log("[Magic Collab] [DATA] Y.Map AFTER update - blocks:", afterBlockCount);
|
|
647
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:update:after", message: "AFTER applying remote update", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: metaMap.get("blockOrder"), roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "G" }) }).catch(() => {
|
|
648
|
-
});
|
|
649
750
|
if (onRemoteUpdateRef.current) {
|
|
650
751
|
console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback");
|
|
651
752
|
setTimeout(() => {
|
|
@@ -769,13 +870,30 @@ const useMagicCollaboration = ({
|
|
|
769
870
|
return user ? getUserColor(user.id) : CURSOR_COLORS[0];
|
|
770
871
|
}, [user]);
|
|
771
872
|
return {
|
|
873
|
+
// Y.js Document & Maps
|
|
772
874
|
doc,
|
|
773
875
|
blocksMap,
|
|
774
|
-
// Y.Map for block
|
|
876
|
+
// Y.Map for block metadata (type, tunes)
|
|
877
|
+
textMap,
|
|
878
|
+
// Y.Map<blockId, Y.Text> for character-level text sync
|
|
775
879
|
metaMap,
|
|
776
|
-
// Y.Map for metadata (
|
|
880
|
+
// Y.Map for document metadata (time, blockOrder)
|
|
881
|
+
// Character-level text helpers
|
|
882
|
+
getBlockText,
|
|
883
|
+
// Get Y.Text for a block (creates if not exists)
|
|
884
|
+
setBlockText,
|
|
885
|
+
// Set block text from HTML
|
|
886
|
+
getBlockTextHtml,
|
|
887
|
+
// Get block text as HTML
|
|
888
|
+
// Utility functions
|
|
889
|
+
htmlToDelta,
|
|
890
|
+
// Convert HTML to delta
|
|
891
|
+
deltaToHtml,
|
|
892
|
+
// Convert delta to HTML
|
|
893
|
+
// Connection status
|
|
777
894
|
status,
|
|
778
895
|
error,
|
|
896
|
+
// Collaboration
|
|
779
897
|
peers,
|
|
780
898
|
awareness,
|
|
781
899
|
emitAwareness,
|
|
@@ -1073,6 +1191,136 @@ const useAIActions = ({ licenseKey, editorInstanceRef, isReady, onNoCredits }) =
|
|
|
1073
1191
|
}, [replaceText, appendText, onNoCredits]);
|
|
1074
1192
|
return { handleAIAction };
|
|
1075
1193
|
};
|
|
1194
|
+
const useWebtoolsLinks = () => {
|
|
1195
|
+
const getPlugin = useStrapiApp("WebtoolsLinks", (state) => state.getPlugin);
|
|
1196
|
+
const linksPlugin = useMemo(() => {
|
|
1197
|
+
try {
|
|
1198
|
+
return getPlugin?.("webtools-addon-links");
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
}, [getPlugin]);
|
|
1203
|
+
const isAvailable = useMemo(() => {
|
|
1204
|
+
const available = !!linksPlugin?.apis?.openLinkPicker;
|
|
1205
|
+
if (typeof window !== "undefined" && !window.__WEBTOOLS_LINKS_CHECKED__) {
|
|
1206
|
+
window.__WEBTOOLS_LINKS_CHECKED__ = true;
|
|
1207
|
+
if (available) {
|
|
1208
|
+
console.log("[Magic Editor X] [SUCCESS] Webtools Links addon detected - Link Picker enabled");
|
|
1209
|
+
} else {
|
|
1210
|
+
console.log("[Magic Editor X] [INFO] Webtools Links addon not installed - Link Picker disabled");
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return available;
|
|
1214
|
+
}, [linksPlugin]);
|
|
1215
|
+
const openLinkPicker = useCallback(async ({ initialHref = "", initialText = "" } = {}) => {
|
|
1216
|
+
if (!linksPlugin?.apis?.openLinkPicker) {
|
|
1217
|
+
console.warn("[Magic Editor X] Webtools Link Picker not available");
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
console.log("[Magic Editor X] Opening Webtools Link Picker with:", {
|
|
1221
|
+
linkType: "both",
|
|
1222
|
+
initialHref: initialHref || "(empty)",
|
|
1223
|
+
initialText: initialText || "(empty)"
|
|
1224
|
+
});
|
|
1225
|
+
try {
|
|
1226
|
+
const result = await linksPlugin.apis.openLinkPicker({
|
|
1227
|
+
linkType: "both",
|
|
1228
|
+
// Allow both internal and external links
|
|
1229
|
+
initialHref: initialHref || "",
|
|
1230
|
+
initialText: initialText || ""
|
|
1231
|
+
});
|
|
1232
|
+
console.log("[Magic Editor X] Webtools picker result:", result);
|
|
1233
|
+
if (result && result.href) {
|
|
1234
|
+
console.log("[Magic Editor X] Webtools link selected:", result);
|
|
1235
|
+
return {
|
|
1236
|
+
href: result.href,
|
|
1237
|
+
label: result.label || initialText || ""
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
return null;
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
console.error("[Magic Editor X] Error opening Webtools Link Picker:", error);
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
}, [linksPlugin]);
|
|
1246
|
+
return {
|
|
1247
|
+
isAvailable,
|
|
1248
|
+
openLinkPicker
|
|
1249
|
+
};
|
|
1250
|
+
};
|
|
1251
|
+
const apiBase = "/magic-editor-x";
|
|
1252
|
+
const useVersionHistory = () => {
|
|
1253
|
+
const { get, post } = useFetchClient();
|
|
1254
|
+
const { tier } = useLicense();
|
|
1255
|
+
const [snapshots, setSnapshots] = useState([]);
|
|
1256
|
+
const [loading, setLoading] = useState(false);
|
|
1257
|
+
const [error, setError] = useState(null);
|
|
1258
|
+
const fetchSnapshots = useCallback(
|
|
1259
|
+
async (roomId) => {
|
|
1260
|
+
if (!roomId) return;
|
|
1261
|
+
setLoading(true);
|
|
1262
|
+
setError(null);
|
|
1263
|
+
try {
|
|
1264
|
+
const { data } = await get(`${apiBase}/snapshots/${roomId}`);
|
|
1265
|
+
setSnapshots(data?.data || []);
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
setError(err?.message || "Failed to load snapshots");
|
|
1268
|
+
} finally {
|
|
1269
|
+
setLoading(false);
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
[get]
|
|
1273
|
+
);
|
|
1274
|
+
const restoreSnapshot = useCallback(
|
|
1275
|
+
async (documentId, roomId) => {
|
|
1276
|
+
if (!documentId) return;
|
|
1277
|
+
setLoading(true);
|
|
1278
|
+
setError(null);
|
|
1279
|
+
try {
|
|
1280
|
+
const { data } = await post(`${apiBase}/snapshots/restore/${documentId}`, { roomId });
|
|
1281
|
+
return data?.data;
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
setError(err?.message || "Failed to restore snapshot");
|
|
1284
|
+
throw err;
|
|
1285
|
+
} finally {
|
|
1286
|
+
setLoading(false);
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
[get]
|
|
1290
|
+
);
|
|
1291
|
+
const createSnapshot = useCallback(
|
|
1292
|
+
async ({ roomId, contentType, entryId, fieldName, content }) => {
|
|
1293
|
+
if (!roomId || !contentType || !entryId || !fieldName) return;
|
|
1294
|
+
setLoading(true);
|
|
1295
|
+
setError(null);
|
|
1296
|
+
try {
|
|
1297
|
+
const { data } = await post(`${apiBase}/snapshots/${roomId}`, {
|
|
1298
|
+
contentType,
|
|
1299
|
+
entryId,
|
|
1300
|
+
fieldName,
|
|
1301
|
+
content
|
|
1302
|
+
// Include editor content as fallback
|
|
1303
|
+
});
|
|
1304
|
+
return data?.data;
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
setError(err?.message || "Failed to create snapshot");
|
|
1307
|
+
throw err;
|
|
1308
|
+
} finally {
|
|
1309
|
+
setLoading(false);
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
[post]
|
|
1313
|
+
);
|
|
1314
|
+
return {
|
|
1315
|
+
snapshots,
|
|
1316
|
+
loading,
|
|
1317
|
+
error,
|
|
1318
|
+
tier,
|
|
1319
|
+
fetchSnapshots,
|
|
1320
|
+
restoreSnapshot,
|
|
1321
|
+
createSnapshot
|
|
1322
|
+
};
|
|
1323
|
+
};
|
|
1076
1324
|
const Overlay$1 = styled.div`
|
|
1077
1325
|
position: fixed;
|
|
1078
1326
|
top: 0;
|
|
@@ -1096,7 +1344,7 @@ const PopupContainer = styled.div`
|
|
|
1096
1344
|
display: flex;
|
|
1097
1345
|
flex-direction: column;
|
|
1098
1346
|
`;
|
|
1099
|
-
const Header$
|
|
1347
|
+
const Header$2 = styled.div`
|
|
1100
1348
|
background: linear-gradient(135deg, #7C3AED 0%, #a855f7 100%);
|
|
1101
1349
|
padding: 20px 24px;
|
|
1102
1350
|
color: white;
|
|
@@ -1118,7 +1366,7 @@ const CreditsBadge = styled.div`
|
|
|
1118
1366
|
font-size: 13px;
|
|
1119
1367
|
font-weight: 500;
|
|
1120
1368
|
`;
|
|
1121
|
-
const Content$
|
|
1369
|
+
const Content$2 = styled.div`
|
|
1122
1370
|
padding: 24px;
|
|
1123
1371
|
overflow-y: auto;
|
|
1124
1372
|
flex: 1;
|
|
@@ -1383,7 +1631,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
|
|
|
1383
1631
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1384
1632
|
}, [onClose]);
|
|
1385
1633
|
return /* @__PURE__ */ jsx(Overlay$1, { onClick: handleOverlayClick, children: /* @__PURE__ */ jsxs(PopupContainer, { onClick: (e) => e.stopPropagation(), children: [
|
|
1386
|
-
/* @__PURE__ */ jsxs(Header$
|
|
1634
|
+
/* @__PURE__ */ jsxs(Header$2, { children: [
|
|
1387
1635
|
/* @__PURE__ */ jsxs(HeaderTitle, { children: [
|
|
1388
1636
|
/* @__PURE__ */ jsx(SparklesIcon, {}),
|
|
1389
1637
|
/* @__PURE__ */ jsx("span", { children: "KI-Assistent" })
|
|
@@ -1395,7 +1643,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
|
|
|
1395
1643
|
!usage?.tier && "Wird geladen..."
|
|
1396
1644
|
] })
|
|
1397
1645
|
] }),
|
|
1398
|
-
/* @__PURE__ */ jsxs(Content$
|
|
1646
|
+
/* @__PURE__ */ jsxs(Content$2, { children: [
|
|
1399
1647
|
/* @__PURE__ */ jsx(TextPreview, { children: selectedText.length > 300 ? selectedText.substring(0, 300) + "..." : selectedText }),
|
|
1400
1648
|
/* @__PURE__ */ jsxs(TypeButtons, { children: [
|
|
1401
1649
|
/* @__PURE__ */ jsxs(
|
|
@@ -1482,6 +1730,132 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
|
|
|
1482
1730
|
] })
|
|
1483
1731
|
] }) });
|
|
1484
1732
|
};
|
|
1733
|
+
const PanelWrapper = styled(Box)`
|
|
1734
|
+
width: 320px;
|
|
1735
|
+
background: ${({ theme }) => theme.colors.neutral0};
|
|
1736
|
+
border: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
1737
|
+
border-radius: 8px;
|
|
1738
|
+
box-shadow: ${({ theme }) => theme.shadows.filterShadow};
|
|
1739
|
+
display: flex;
|
|
1740
|
+
flex-direction: column;
|
|
1741
|
+
max-height: 70vh;
|
|
1742
|
+
`;
|
|
1743
|
+
const Header$1 = styled(Flex)`
|
|
1744
|
+
padding: 12px 16px;
|
|
1745
|
+
border-bottom: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
1746
|
+
`;
|
|
1747
|
+
const Content$1 = styled(Box)`
|
|
1748
|
+
padding: 12px 16px;
|
|
1749
|
+
overflow-y: auto;
|
|
1750
|
+
`;
|
|
1751
|
+
const Item = styled(Box)`
|
|
1752
|
+
padding: 10px 12px;
|
|
1753
|
+
border: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
1754
|
+
border-radius: 6px;
|
|
1755
|
+
margin-bottom: 10px;
|
|
1756
|
+
`;
|
|
1757
|
+
const Meta = styled(Typography)`
|
|
1758
|
+
color: ${({ theme }) => theme.colors.neutral500};
|
|
1759
|
+
font-size: 12px;
|
|
1760
|
+
`;
|
|
1761
|
+
const PremiumBadge = styled(Box)`
|
|
1762
|
+
background: ${({ theme }) => theme.colors.primary100};
|
|
1763
|
+
color: ${({ theme }) => theme.colors.primary600};
|
|
1764
|
+
border-radius: 6px;
|
|
1765
|
+
padding: 8px 10px;
|
|
1766
|
+
display: inline-flex;
|
|
1767
|
+
align-items: center;
|
|
1768
|
+
gap: 8px;
|
|
1769
|
+
font-weight: 600;
|
|
1770
|
+
margin-top: 8px;
|
|
1771
|
+
`;
|
|
1772
|
+
const safeDateFrom = (value) => {
|
|
1773
|
+
if (!value) return null;
|
|
1774
|
+
if (value instanceof Date) {
|
|
1775
|
+
return isNaN(value.getTime()) ? null : value;
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
const parsed = new Date(value);
|
|
1779
|
+
return isNaN(parsed.getTime()) ? null : parsed;
|
|
1780
|
+
} catch {
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
const formatDate = (dateValue) => {
|
|
1785
|
+
const date = safeDateFrom(dateValue);
|
|
1786
|
+
if (!date) return "—";
|
|
1787
|
+
try {
|
|
1788
|
+
return date.toLocaleString();
|
|
1789
|
+
} catch {
|
|
1790
|
+
return "—";
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
const VersionHistoryPanel = ({
|
|
1794
|
+
snapshots,
|
|
1795
|
+
loading,
|
|
1796
|
+
error,
|
|
1797
|
+
onRestore,
|
|
1798
|
+
onCreate,
|
|
1799
|
+
tier,
|
|
1800
|
+
onClose
|
|
1801
|
+
}) => {
|
|
1802
|
+
const { formatMessage } = useIntl();
|
|
1803
|
+
const canRestore = tier !== "free";
|
|
1804
|
+
const t = (id, defaultMessage) => formatMessage(
|
|
1805
|
+
{ id: getTranslation(id), defaultMessage },
|
|
1806
|
+
{}
|
|
1807
|
+
);
|
|
1808
|
+
return /* @__PURE__ */ jsxs(PanelWrapper, { "data-testid": "version-history-panel", children: [
|
|
1809
|
+
/* @__PURE__ */ jsxs(Header$1, { justifyContent: "space-between", alignItems: "center", children: [
|
|
1810
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 8, alignItems: "center", children: [
|
|
1811
|
+
/* @__PURE__ */ jsx(ClockIcon, { width: 18 }),
|
|
1812
|
+
/* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: t("versionHistory.title", "Version History") })
|
|
1813
|
+
] }),
|
|
1814
|
+
/* @__PURE__ */ jsx(Button$2, { size: "S", variant: "tertiary", onClick: onClose, children: t("versionHistory.close", "Close") })
|
|
1815
|
+
] }),
|
|
1816
|
+
/* @__PURE__ */ jsxs(Content$1, { children: [
|
|
1817
|
+
loading && /* @__PURE__ */ jsx(Typography, { children: t("versionHistory.loading", "Loading versions...") }),
|
|
1818
|
+
error && /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }),
|
|
1819
|
+
!loading && !error && snapshots.length === 0 && /* @__PURE__ */ jsx(Typography, { children: t("versionHistory.noSnapshots", "No versions saved yet") }),
|
|
1820
|
+
!loading && !error && snapshots.map((snap) => /* @__PURE__ */ jsxs(Item, { children: [
|
|
1821
|
+
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
|
|
1822
|
+
/* @__PURE__ */ jsxs(Typography, { fontWeight: "bold", children: [
|
|
1823
|
+
t("versionHistory.version", "Version"),
|
|
1824
|
+
" ",
|
|
1825
|
+
snap.version
|
|
1826
|
+
] }),
|
|
1827
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", children: formatDate(snap.createdAt) })
|
|
1828
|
+
] }),
|
|
1829
|
+
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
1830
|
+
t("versionHistory.createdBy", "By"),
|
|
1831
|
+
" ",
|
|
1832
|
+
snap.createdBy?.firstname ? `${snap.createdBy.firstname} ${snap.createdBy.lastname || ""}`.trim() : "—"
|
|
1833
|
+
] }),
|
|
1834
|
+
/* @__PURE__ */ jsx(Divider, { marginTop: 2, marginBottom: 2 }),
|
|
1835
|
+
canRestore ? /* @__PURE__ */ jsx(Flex, { gap: 8, children: /* @__PURE__ */ jsx(
|
|
1836
|
+
Button$2,
|
|
1837
|
+
{
|
|
1838
|
+
size: "S",
|
|
1839
|
+
variant: "secondary",
|
|
1840
|
+
onClick: () => onRestore?.(snap),
|
|
1841
|
+
children: t("versionHistory.restore", "Restore")
|
|
1842
|
+
}
|
|
1843
|
+
) }) : /* @__PURE__ */ jsxs(PremiumBadge, { children: [
|
|
1844
|
+
/* @__PURE__ */ jsx(ExclamationTriangleIcon, { width: 16 }),
|
|
1845
|
+
t("versionHistory.premiumOnly", "Premium feature")
|
|
1846
|
+
] })
|
|
1847
|
+
] }, snap.documentId || snap.id)),
|
|
1848
|
+
/* @__PURE__ */ jsx(Divider, { marginTop: 4, marginBottom: 4 }),
|
|
1849
|
+
canRestore ? /* @__PURE__ */ jsx(Button$2, { size: "S", fullWidth: true, variant: "default", onClick: onCreate, disabled: loading, children: t("versionHistory.create", "Create Snapshot") }) : /* @__PURE__ */ jsxs(Box, { children: [
|
|
1850
|
+
/* @__PURE__ */ jsx(Button$2, { size: "S", fullWidth: true, variant: "default", disabled: true, children: t("versionHistory.create", "Create Snapshot") }),
|
|
1851
|
+
/* @__PURE__ */ jsxs(PremiumBadge, { style: { marginTop: "8px", width: "100%", justifyContent: "center" }, children: [
|
|
1852
|
+
/* @__PURE__ */ jsx(ExclamationTriangleIcon, { width: 16 }),
|
|
1853
|
+
t("versionHistory.premiumOnly", "Premium feature")
|
|
1854
|
+
] })
|
|
1855
|
+
] })
|
|
1856
|
+
] })
|
|
1857
|
+
] });
|
|
1858
|
+
};
|
|
1485
1859
|
const Overlay = styled.div`
|
|
1486
1860
|
position: fixed;
|
|
1487
1861
|
top: 0;
|
|
@@ -1796,11 +2170,14 @@ const EditorJSGlobalStyles = createGlobalStyle`
|
|
|
1796
2170
|
|
|
1797
2171
|
/* ============================================
|
|
1798
2172
|
STRAPI MEDIA LIBRARY - Higher z-index for fullscreen
|
|
2173
|
+
Must be higher than fullscreen z-index (9999)
|
|
1799
2174
|
============================================ */
|
|
1800
2175
|
[data-react-portal],
|
|
1801
2176
|
.ReactModalPortal,
|
|
1802
2177
|
[role="dialog"],
|
|
1803
2178
|
[data-strapi-modal="true"],
|
|
2179
|
+
[class*="Dialog"],
|
|
2180
|
+
[class*="Modal"],
|
|
1804
2181
|
.upload-dialog,
|
|
1805
2182
|
[class*="Modal"],
|
|
1806
2183
|
[class*="modal"],
|
|
@@ -2449,15 +2826,36 @@ const EditorContent = styled.div`
|
|
|
2449
2826
|
flex: 1;
|
|
2450
2827
|
overflow: visible; /* Allow toolbars/popovers to escape */
|
|
2451
2828
|
position: relative;
|
|
2452
|
-
padding: 24px
|
|
2829
|
+
padding: 24px;
|
|
2453
2830
|
min-height: 200px;
|
|
2454
2831
|
|
|
2455
2832
|
${(props) => props.$isFullscreen && css`
|
|
2456
|
-
padding: clamp(
|
|
2833
|
+
padding: clamp(24px, 3vw, 48px);
|
|
2457
2834
|
width: 100%;
|
|
2458
2835
|
max-width: 100%;
|
|
2459
2836
|
margin: 0;
|
|
2460
2837
|
align-self: stretch;
|
|
2838
|
+
|
|
2839
|
+
/* Make blocks stretch full width in fullscreen */
|
|
2840
|
+
.codex-editor {
|
|
2841
|
+
width: 100%;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
.ce-block__content,
|
|
2845
|
+
.ce-toolbar__content {
|
|
2846
|
+
max-width: 100% !important;
|
|
2847
|
+
padding: 0 !important;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
.ce-toolbar {
|
|
2851
|
+
max-width: 100% !important;
|
|
2852
|
+
left: 0 !important;
|
|
2853
|
+
transform: none !important;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
.ce-toolbar__actions {
|
|
2857
|
+
right: 0 !important;
|
|
2858
|
+
}
|
|
2461
2859
|
`}
|
|
2462
2860
|
`;
|
|
2463
2861
|
const EditorWrapper = styled.div`
|
|
@@ -2532,18 +2930,19 @@ const EditorWrapper = styled.div`
|
|
|
2532
2930
|
TOOLBAR INSIDE EDITOR - Position Fix
|
|
2533
2931
|
============================================ */
|
|
2534
2932
|
|
|
2535
|
-
/*
|
|
2933
|
+
/* Centered content area */
|
|
2536
2934
|
.codex-editor__redactor {
|
|
2537
2935
|
padding-bottom: 100px !important;
|
|
2538
|
-
padding-left:
|
|
2539
|
-
margin
|
|
2936
|
+
padding-left: 0 !important;
|
|
2937
|
+
margin: 0 auto !important;
|
|
2938
|
+
max-width: 800px !important;
|
|
2540
2939
|
}
|
|
2541
2940
|
|
|
2542
|
-
/* Content blocks -
|
|
2941
|
+
/* Content blocks - centered */
|
|
2543
2942
|
.ce-block__content {
|
|
2544
2943
|
max-width: 100%;
|
|
2545
|
-
margin
|
|
2546
|
-
|
|
2944
|
+
margin: 0 auto;
|
|
2945
|
+
padding: 0 16px;
|
|
2547
2946
|
}
|
|
2548
2947
|
|
|
2549
2948
|
/* ============================================
|
|
@@ -2592,24 +2991,27 @@ const EditorWrapper = styled.div`
|
|
|
2592
2991
|
border-radius: 6px;
|
|
2593
2992
|
}
|
|
2594
2993
|
|
|
2595
|
-
/* Toolbar positioning -
|
|
2994
|
+
/* Toolbar positioning - centered with content */
|
|
2596
2995
|
.ce-toolbar__content {
|
|
2597
|
-
max-width:
|
|
2598
|
-
margin
|
|
2996
|
+
max-width: 800px;
|
|
2997
|
+
margin: 0 auto;
|
|
2998
|
+
padding: 0 16px;
|
|
2599
2999
|
}
|
|
2600
3000
|
|
|
2601
3001
|
.ce-toolbar {
|
|
2602
|
-
left:
|
|
3002
|
+
left: 50% !important;
|
|
3003
|
+
transform: translateX(-50%) !important;
|
|
3004
|
+
width: 100% !important;
|
|
3005
|
+
max-width: 832px !important;
|
|
2603
3006
|
}
|
|
2604
3007
|
|
|
2605
3008
|
.ce-toolbar__plus {
|
|
2606
|
-
left: 0 !important;
|
|
2607
3009
|
position: relative !important;
|
|
2608
3010
|
}
|
|
2609
3011
|
|
|
2610
3012
|
.ce-toolbar__actions {
|
|
2611
|
-
right: 0 !important;
|
|
2612
3013
|
position: absolute !important;
|
|
3014
|
+
right: 16px !important;
|
|
2613
3015
|
}
|
|
2614
3016
|
|
|
2615
3017
|
/* Settings button (⋮⋮) */
|
|
@@ -3095,6 +3497,35 @@ const FooterButton = styled.button`
|
|
|
3095
3497
|
}
|
|
3096
3498
|
}
|
|
3097
3499
|
`;
|
|
3500
|
+
const WebtoolsPromoLink = styled.a`
|
|
3501
|
+
display: inline-flex;
|
|
3502
|
+
align-items: center;
|
|
3503
|
+
gap: 6px;
|
|
3504
|
+
padding: 4px 10px;
|
|
3505
|
+
background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%);
|
|
3506
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
3507
|
+
border-radius: 6px;
|
|
3508
|
+
font-size: 11px;
|
|
3509
|
+
color: #6366f1;
|
|
3510
|
+
text-decoration: none;
|
|
3511
|
+
transition: all 0.2s ease;
|
|
3512
|
+
white-space: nowrap;
|
|
3513
|
+
|
|
3514
|
+
svg {
|
|
3515
|
+
width: 12px;
|
|
3516
|
+
height: 12px;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
&:hover {
|
|
3520
|
+
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
|
|
3521
|
+
border-color: rgba(99, 102, 241, 0.4);
|
|
3522
|
+
transform: translateY(-1px);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
@media (max-width: 768px) {
|
|
3526
|
+
display: none;
|
|
3527
|
+
}
|
|
3528
|
+
`;
|
|
3098
3529
|
const LoadingOverlay = styled.div`
|
|
3099
3530
|
position: absolute;
|
|
3100
3531
|
top: 0;
|
|
@@ -3109,6 +3540,18 @@ const LoadingOverlay = styled.div`
|
|
|
3109
3540
|
gap: 12px;
|
|
3110
3541
|
z-index: 10;
|
|
3111
3542
|
`;
|
|
3543
|
+
const VersionHistoryOverlay = styled.div`
|
|
3544
|
+
position: fixed;
|
|
3545
|
+
top: 0;
|
|
3546
|
+
left: 0;
|
|
3547
|
+
right: 0;
|
|
3548
|
+
bottom: 0;
|
|
3549
|
+
background: rgba(0, 0, 0, 0.5);
|
|
3550
|
+
display: flex;
|
|
3551
|
+
align-items: center;
|
|
3552
|
+
justify-content: center;
|
|
3553
|
+
z-index: 99999;
|
|
3554
|
+
`;
|
|
3112
3555
|
const LoadingText = styled.span`
|
|
3113
3556
|
font-size: 13px;
|
|
3114
3557
|
color: #64748b;
|
|
@@ -3324,10 +3767,12 @@ const Editor = forwardRef(({
|
|
|
3324
3767
|
}, ref) => {
|
|
3325
3768
|
const { formatMessage } = useIntl();
|
|
3326
3769
|
const t = (id, defaultMessage) => formatMessage({ id: getTranslation(id), defaultMessage });
|
|
3327
|
-
const { licenseData } = useLicense();
|
|
3770
|
+
const { licenseData, tier: licenseTier } = useLicense();
|
|
3771
|
+
const { isAvailable: isWebtoolsAvailable, openLinkPicker: webtoolsOpenLinkPicker } = useWebtoolsLinks();
|
|
3328
3772
|
const editorRef = useRef(null);
|
|
3329
3773
|
const editorInstanceRef = useRef(null);
|
|
3330
3774
|
const containerRef = useRef(null);
|
|
3775
|
+
const webtoolsSelectionRef = useRef({ text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" });
|
|
3331
3776
|
const isReadyRef = useRef(false);
|
|
3332
3777
|
const [isReady, setIsReady] = useState(false);
|
|
3333
3778
|
const [showCreditsModal, setShowCreditsModal] = useState(false);
|
|
@@ -3363,6 +3808,51 @@ const Editor = forwardRef(({
|
|
|
3363
3808
|
const [aiSelectedText, setAISelectedText] = useState("");
|
|
3364
3809
|
const aiSelectionRangeRef = useRef(null);
|
|
3365
3810
|
const [aiLoading, setAILoading] = useState(false);
|
|
3811
|
+
const [showVersionHistory, setShowVersionHistory] = useState(false);
|
|
3812
|
+
useEffect(() => {
|
|
3813
|
+
if (!isWebtoolsAvailable || !editorRef.current) return;
|
|
3814
|
+
const updateWebtoolsSelection = () => {
|
|
3815
|
+
const selection = window.getSelection();
|
|
3816
|
+
if (!selection || selection.rangeCount === 0) return;
|
|
3817
|
+
const range = selection.getRangeAt(0);
|
|
3818
|
+
if (!editorRef.current.contains(range.commonAncestorContainer)) return;
|
|
3819
|
+
const selectedText = selection.toString().trim();
|
|
3820
|
+
let existingAnchor = null;
|
|
3821
|
+
let existingHref = "";
|
|
3822
|
+
let node = range.commonAncestorContainer;
|
|
3823
|
+
while (node && node !== editorRef.current) {
|
|
3824
|
+
if (node.nodeName === "A") {
|
|
3825
|
+
existingAnchor = node;
|
|
3826
|
+
existingHref = node.href || "";
|
|
3827
|
+
break;
|
|
3828
|
+
}
|
|
3829
|
+
node = node.parentNode;
|
|
3830
|
+
}
|
|
3831
|
+
if (!existingAnchor) {
|
|
3832
|
+
node = range.startContainer;
|
|
3833
|
+
while (node && node !== editorRef.current) {
|
|
3834
|
+
if (node.nodeName === "A") {
|
|
3835
|
+
existingAnchor = node;
|
|
3836
|
+
existingHref = node.href || "";
|
|
3837
|
+
break;
|
|
3838
|
+
}
|
|
3839
|
+
node = node.parentNode;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
const blockIndex = editorInstanceRef.current?.blocks?.getCurrentBlockIndex?.() ?? -1;
|
|
3843
|
+
webtoolsSelectionRef.current = {
|
|
3844
|
+
text: existingAnchor ? existingAnchor.textContent : selectedText,
|
|
3845
|
+
range: range.cloneRange(),
|
|
3846
|
+
blockIndex,
|
|
3847
|
+
existingAnchor,
|
|
3848
|
+
existingHref
|
|
3849
|
+
};
|
|
3850
|
+
};
|
|
3851
|
+
document.addEventListener("selectionchange", updateWebtoolsSelection);
|
|
3852
|
+
return () => {
|
|
3853
|
+
document.removeEventListener("selectionchange", updateWebtoolsSelection);
|
|
3854
|
+
};
|
|
3855
|
+
}, [isWebtoolsAvailable, isReady]);
|
|
3366
3856
|
const serializedInitialValue = useMemo(() => {
|
|
3367
3857
|
if (!value) {
|
|
3368
3858
|
return "";
|
|
@@ -3390,12 +3880,25 @@ const Editor = forwardRef(({
|
|
|
3390
3880
|
const {
|
|
3391
3881
|
doc: yDoc,
|
|
3392
3882
|
blocksMap: yBlocksMap,
|
|
3883
|
+
textMap: yTextMap,
|
|
3884
|
+
// NEW: Y.Map<blockId, Y.Text> for character-level sync
|
|
3393
3885
|
metaMap: yMetaMap,
|
|
3886
|
+
// Character-level text helpers
|
|
3887
|
+
getBlockText,
|
|
3888
|
+
// NEW: Get Y.Text for a block
|
|
3889
|
+
setBlockText,
|
|
3890
|
+
// NEW: Get block text as HTML
|
|
3891
|
+
// Utility functions
|
|
3892
|
+
htmlToDelta: collabHtmlToDelta,
|
|
3893
|
+
deltaToHtml: collabDeltaToHtml,
|
|
3894
|
+
// Connection status
|
|
3394
3895
|
status: collabStatus,
|
|
3395
3896
|
error: collabError,
|
|
3897
|
+
// Collaboration
|
|
3396
3898
|
peers: collabPeers,
|
|
3397
3899
|
awareness: collabAwareness,
|
|
3398
3900
|
emitAwareness,
|
|
3901
|
+
// Role-based access control
|
|
3399
3902
|
collabRole,
|
|
3400
3903
|
canEdit: collabCanEdit
|
|
3401
3904
|
} = useMagicCollaboration({
|
|
@@ -3410,6 +3913,171 @@ const Editor = forwardRef(({
|
|
|
3410
3913
|
}
|
|
3411
3914
|
}
|
|
3412
3915
|
});
|
|
3916
|
+
const yTextBindingsRef = useRef(/* @__PURE__ */ new Map());
|
|
3917
|
+
const bindBlockToYText = useCallback((blockId, element) => {
|
|
3918
|
+
if (!collabEnabled || !blockId || !element || !yTextMap) return;
|
|
3919
|
+
if (yTextBindingsRef.current.has(blockId)) {
|
|
3920
|
+
const existing = yTextBindingsRef.current.get(blockId);
|
|
3921
|
+
if (existing.element === element) return;
|
|
3922
|
+
unbindBlockFromYText(blockId);
|
|
3923
|
+
}
|
|
3924
|
+
const ytext = getBlockText(blockId);
|
|
3925
|
+
if (!ytext) return;
|
|
3926
|
+
let isUpdating = false;
|
|
3927
|
+
const ytextObserver = (event) => {
|
|
3928
|
+
if (isUpdating) return;
|
|
3929
|
+
if (event.transaction.local) return;
|
|
3930
|
+
isUpdating = true;
|
|
3931
|
+
try {
|
|
3932
|
+
const selection = window.getSelection();
|
|
3933
|
+
let cursorOffset = 0;
|
|
3934
|
+
if (selection && selection.rangeCount > 0 && element.contains(selection.anchorNode)) {
|
|
3935
|
+
const range = selection.getRangeAt(0);
|
|
3936
|
+
const preCaretRange = document.createRange();
|
|
3937
|
+
preCaretRange.selectNodeContents(element);
|
|
3938
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
3939
|
+
cursorOffset = preCaretRange.toString().length;
|
|
3940
|
+
}
|
|
3941
|
+
const html = collabDeltaToHtml(ytext.toDelta());
|
|
3942
|
+
if (element.innerHTML !== html) {
|
|
3943
|
+
element.innerHTML = html || "";
|
|
3944
|
+
}
|
|
3945
|
+
if (document.activeElement === element && cursorOffset > 0) {
|
|
3946
|
+
try {
|
|
3947
|
+
const textNode = element.firstChild;
|
|
3948
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
3949
|
+
const newRange = document.createRange();
|
|
3950
|
+
const pos = Math.min(cursorOffset, textNode.length);
|
|
3951
|
+
newRange.setStart(textNode, pos);
|
|
3952
|
+
newRange.setEnd(textNode, pos);
|
|
3953
|
+
selection.removeAllRanges();
|
|
3954
|
+
selection.addRange(newRange);
|
|
3955
|
+
}
|
|
3956
|
+
} catch (e) {
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
} finally {
|
|
3960
|
+
isUpdating = false;
|
|
3961
|
+
}
|
|
3962
|
+
};
|
|
3963
|
+
const inputHandler = () => {
|
|
3964
|
+
if (isUpdating) return;
|
|
3965
|
+
isUpdating = true;
|
|
3966
|
+
try {
|
|
3967
|
+
const html = element.innerHTML;
|
|
3968
|
+
const newDelta = collabHtmlToDelta(html);
|
|
3969
|
+
const currentDelta = ytext.toDelta();
|
|
3970
|
+
yDoc.transact(() => {
|
|
3971
|
+
if (ytext.length > 0) {
|
|
3972
|
+
ytext.delete(0, ytext.length);
|
|
3973
|
+
}
|
|
3974
|
+
if (newDelta.length > 0) {
|
|
3975
|
+
ytext.applyDelta(newDelta);
|
|
3976
|
+
}
|
|
3977
|
+
}, "local");
|
|
3978
|
+
} finally {
|
|
3979
|
+
isUpdating = false;
|
|
3980
|
+
}
|
|
3981
|
+
};
|
|
3982
|
+
ytext.observe(ytextObserver);
|
|
3983
|
+
element.addEventListener("input", inputHandler);
|
|
3984
|
+
yTextBindingsRef.current.set(blockId, {
|
|
3985
|
+
ytext,
|
|
3986
|
+
element,
|
|
3987
|
+
ytextObserver,
|
|
3988
|
+
inputHandler
|
|
3989
|
+
});
|
|
3990
|
+
console.log("[Magic Editor X] [CHAR-SYNC] Bound block to Y.Text:", blockId);
|
|
3991
|
+
}, [collabEnabled, yTextMap, yDoc, getBlockText, collabHtmlToDelta, collabDeltaToHtml]);
|
|
3992
|
+
const unbindBlockFromYText = useCallback((blockId) => {
|
|
3993
|
+
const binding = yTextBindingsRef.current.get(blockId);
|
|
3994
|
+
if (!binding) return;
|
|
3995
|
+
binding.ytext.unobserve(binding.ytextObserver);
|
|
3996
|
+
binding.element.removeEventListener("input", binding.inputHandler);
|
|
3997
|
+
yTextBindingsRef.current.delete(blockId);
|
|
3998
|
+
console.log("[Magic Editor X] [CHAR-SYNC] Unbound block from Y.Text:", blockId);
|
|
3999
|
+
}, []);
|
|
4000
|
+
const bindAllBlocksToYText = useCallback(() => {
|
|
4001
|
+
if (!collabEnabled || !editorInstanceRef.current || !yTextMap) return;
|
|
4002
|
+
const editor = editorInstanceRef.current;
|
|
4003
|
+
const blockCount = editor.blocks.getBlocksCount();
|
|
4004
|
+
console.log("[Magic Editor X] [CHAR-SYNC] Binding", blockCount, "blocks to Y.Text");
|
|
4005
|
+
for (let i = 0; i < blockCount; i++) {
|
|
4006
|
+
try {
|
|
4007
|
+
const block = editor.blocks.getBlockByIndex(i);
|
|
4008
|
+
if (!block || !block.id) continue;
|
|
4009
|
+
const blockHolder = block.holder;
|
|
4010
|
+
if (!blockHolder) continue;
|
|
4011
|
+
const contentEditable = blockHolder.querySelector('[contenteditable="true"]');
|
|
4012
|
+
if (contentEditable) {
|
|
4013
|
+
const ytext = getBlockText(block.id);
|
|
4014
|
+
if (ytext && ytext.length === 0) {
|
|
4015
|
+
const currentHtml = contentEditable.innerHTML;
|
|
4016
|
+
if (currentHtml && currentHtml !== "<br>") {
|
|
4017
|
+
setBlockText(block.id, currentHtml);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
bindBlockToYText(block.id, contentEditable);
|
|
4021
|
+
}
|
|
4022
|
+
} catch (e) {
|
|
4023
|
+
console.warn("[Magic Editor X] [CHAR-SYNC] Error binding block:", e);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
}, [collabEnabled, yTextMap, getBlockText, setBlockText, bindBlockToYText]);
|
|
4027
|
+
const blockObserverRef = useRef(null);
|
|
4028
|
+
useEffect(() => {
|
|
4029
|
+
if (!collabEnabled || !editorRef.current || !yTextMap) return;
|
|
4030
|
+
const observer = new MutationObserver((mutations) => {
|
|
4031
|
+
let hasNewBlocks = false;
|
|
4032
|
+
for (const mutation of mutations) {
|
|
4033
|
+
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
|
4034
|
+
for (const node of mutation.addedNodes) {
|
|
4035
|
+
if (node.nodeType === Node.ELEMENT_NODE && (node.classList?.contains("ce-block") || node.querySelector?.(".ce-block"))) {
|
|
4036
|
+
hasNewBlocks = true;
|
|
4037
|
+
break;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
if (hasNewBlocks) break;
|
|
4042
|
+
}
|
|
4043
|
+
if (hasNewBlocks) {
|
|
4044
|
+
setTimeout(() => {
|
|
4045
|
+
bindAllBlocksToYText();
|
|
4046
|
+
}, 50);
|
|
4047
|
+
}
|
|
4048
|
+
});
|
|
4049
|
+
observer.observe(editorRef.current, {
|
|
4050
|
+
childList: true,
|
|
4051
|
+
subtree: true
|
|
4052
|
+
});
|
|
4053
|
+
blockObserverRef.current = observer;
|
|
4054
|
+
return () => {
|
|
4055
|
+
observer.disconnect();
|
|
4056
|
+
blockObserverRef.current = null;
|
|
4057
|
+
};
|
|
4058
|
+
}, [collabEnabled, yTextMap, bindAllBlocksToYText]);
|
|
4059
|
+
useEffect(() => {
|
|
4060
|
+
return () => {
|
|
4061
|
+
yTextBindingsRef.current.forEach((binding, blockId) => {
|
|
4062
|
+
binding.ytext.unobserve(binding.ytextObserver);
|
|
4063
|
+
binding.element.removeEventListener("input", binding.inputHandler);
|
|
4064
|
+
});
|
|
4065
|
+
yTextBindingsRef.current.clear();
|
|
4066
|
+
};
|
|
4067
|
+
}, []);
|
|
4068
|
+
const {
|
|
4069
|
+
snapshots,
|
|
4070
|
+
loading: versionHistoryLoading,
|
|
4071
|
+
error: versionHistoryError,
|
|
4072
|
+
fetchSnapshots,
|
|
4073
|
+
restoreSnapshot,
|
|
4074
|
+
createSnapshot
|
|
4075
|
+
} = useVersionHistory();
|
|
4076
|
+
useEffect(() => {
|
|
4077
|
+
if (showVersionHistory && collabRoomId) {
|
|
4078
|
+
fetchSnapshots(collabRoomId);
|
|
4079
|
+
}
|
|
4080
|
+
}, [showVersionHistory, collabRoomId, fetchSnapshots]);
|
|
3413
4081
|
useMemo(() => {
|
|
3414
4082
|
switch (collabRole) {
|
|
3415
4083
|
case "viewer":
|
|
@@ -3743,23 +4411,15 @@ const Editor = forwardRef(({
|
|
|
3743
4411
|
setWordCount(plainText.split(/\s+/).filter((w) => w.length > 0).length);
|
|
3744
4412
|
}, []);
|
|
3745
4413
|
const renderFromYDoc = useCallback(async () => {
|
|
3746
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:entry", message: "renderFromYDoc called", data: { collabEnabled, hasYBlocksMap: !!yBlocksMap, hasYDoc: !!yDoc, hasYMetaMap: !!yMetaMap }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "A" }) }).catch(() => {
|
|
3747
|
-
});
|
|
3748
4414
|
if (!collabEnabled || !yBlocksMap || !yDoc) {
|
|
3749
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:guard1", message: "EARLY EXIT: missing collab deps", data: { collabEnabled, hasYBlocksMap: !!yBlocksMap, hasYDoc: !!yDoc }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "A" }) }).catch(() => {
|
|
3750
|
-
});
|
|
3751
4415
|
return;
|
|
3752
4416
|
}
|
|
3753
4417
|
const editor = editorInstanceRef.current;
|
|
3754
4418
|
if (!editor || !isReadyRef.current) {
|
|
3755
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:guard2", message: "EARLY EXIT: editor not ready", data: { hasEditor: !!editor, isReady: isReadyRef.current }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "D" }) }).catch(() => {
|
|
3756
|
-
});
|
|
3757
4419
|
pendingRenderRef.current = pendingRenderRef.current || true;
|
|
3758
4420
|
return;
|
|
3759
4421
|
}
|
|
3760
4422
|
if (isApplyingRemoteRef.current) {
|
|
3761
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:guard3", message: "EARLY EXIT: isApplyingRemote is true (race condition)", data: { isApplyingRemote: true }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "B" }) }).catch(() => {
|
|
3762
|
-
});
|
|
3763
4423
|
return;
|
|
3764
4424
|
}
|
|
3765
4425
|
isApplyingRemoteRef.current = true;
|
|
@@ -3777,29 +4437,35 @@ const Editor = forwardRef(({
|
|
|
3777
4437
|
yOrder = Array.from(yBlocksMap.keys());
|
|
3778
4438
|
}
|
|
3779
4439
|
const yBlocks = [];
|
|
3780
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:yState", message: "Y.js state before parsing", data: { blockOrderFromMeta: !!blockOrderJson, yBlocksMapSize: yBlocksMap?.size, yOrder, yBlocksMapKeys: Array.from(yBlocksMap?.keys() || []) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "C" }) }).catch(() => {
|
|
3781
|
-
});
|
|
3782
4440
|
yOrder.forEach((id) => {
|
|
3783
4441
|
const json = yBlocksMap.get(id);
|
|
3784
4442
|
if (json) {
|
|
3785
4443
|
try {
|
|
3786
|
-
const
|
|
3787
|
-
|
|
4444
|
+
const blockData = JSON.parse(json);
|
|
4445
|
+
if (blockData.type && !blockData.data) {
|
|
4446
|
+
const ytext = yTextMap?.get(id);
|
|
4447
|
+
const textContent = ytext ? collabDeltaToHtml(ytext.toDelta()) : "";
|
|
4448
|
+
yBlocks.push({
|
|
4449
|
+
id,
|
|
4450
|
+
type: blockData.type,
|
|
4451
|
+
data: { text: textContent },
|
|
4452
|
+
tunes: blockData.tunes || {}
|
|
4453
|
+
});
|
|
4454
|
+
} else if (blockData.type && blockData.data) {
|
|
4455
|
+
yBlocks.push({
|
|
4456
|
+
id,
|
|
4457
|
+
...blockData
|
|
4458
|
+
});
|
|
4459
|
+
}
|
|
3788
4460
|
} catch (e) {
|
|
3789
4461
|
console.warn("[Magic Editor X] Invalid block JSON:", id);
|
|
3790
4462
|
}
|
|
3791
4463
|
}
|
|
3792
4464
|
});
|
|
3793
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:parsed", message: "Parsed yBlocks from Y.Map", data: { yBlocksCount: yBlocks.length, yBlockIds: yBlocks.map((b) => b.id), yBlockTypes: yBlocks.map((b) => b.type) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "A" }) }).catch(() => {
|
|
3794
|
-
});
|
|
3795
4465
|
const parsed = { blocks: yBlocks };
|
|
3796
4466
|
const normalizedParsed = serializeForCompare(parsed);
|
|
3797
4467
|
const renderFull = async () => {
|
|
3798
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFull:start", message: "FULL RENDER triggered", data: { blocksToRender: yBlocks.length, blockIds: yBlocks.map((b) => b.id) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "E" }) }).catch(() => {
|
|
3799
|
-
});
|
|
3800
4468
|
await editor.render(parsed);
|
|
3801
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFull:done", message: "FULL RENDER completed", data: { newBlockCount: editor.blocks.getBlocksCount() }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "E" }) }).catch(() => {
|
|
3802
|
-
});
|
|
3803
4469
|
lastSerializedValueRef.current = normalizedParsed;
|
|
3804
4470
|
setBlocksCount(yBlocks.length);
|
|
3805
4471
|
calculateStats(parsed);
|
|
@@ -3812,8 +4478,6 @@ const Editor = forwardRef(({
|
|
|
3812
4478
|
currentBlocks.push({ id: block.id, index: i });
|
|
3813
4479
|
}
|
|
3814
4480
|
}
|
|
3815
|
-
fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:comparison", message: "Comparing editor vs Y.js state", data: { editorBlockCount: blockCount, yBlocksCount: yBlocks.length, editorBlockIds: currentBlocks.map((b) => b.id), yBlockIds: yBlocks.map((b) => b.id) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "E" }) }).catch(() => {
|
|
3816
|
-
});
|
|
3817
4481
|
if (blockCount !== yBlocks.length) {
|
|
3818
4482
|
console.log("[Magic Editor X] [SYNC] Structural change detected (count mismatch). Falling back to full render.");
|
|
3819
4483
|
await renderFull();
|
|
@@ -4018,7 +4682,7 @@ const Editor = forwardRef(({
|
|
|
4018
4682
|
const emptyPayload = JSON.stringify({ blocks: [] });
|
|
4019
4683
|
lastSerializedValueRef.current = emptyPayload;
|
|
4020
4684
|
pushLocalToCollab(emptyPayload);
|
|
4021
|
-
onChange({ target: { name, value: null, type: "
|
|
4685
|
+
onChange({ target: { name, value: null, type: "json" } });
|
|
4022
4686
|
setBlocksCount(0);
|
|
4023
4687
|
setWordCount(0);
|
|
4024
4688
|
setCharCount(0);
|
|
@@ -4031,7 +4695,11 @@ const Editor = forwardRef(({
|
|
|
4031
4695
|
}, [isReady]);
|
|
4032
4696
|
useEffect(() => {
|
|
4033
4697
|
if (editorRef.current && !editorInstanceRef.current) {
|
|
4034
|
-
const tools = getTools({
|
|
4698
|
+
const tools = getTools({
|
|
4699
|
+
mediaLibToggleFunc,
|
|
4700
|
+
pluginId: PLUGIN_ID,
|
|
4701
|
+
openLinkPicker: isWebtoolsAvailable ? webtoolsOpenLinkPicker : null
|
|
4702
|
+
});
|
|
4035
4703
|
let initialData = void 0;
|
|
4036
4704
|
if (value) {
|
|
4037
4705
|
try {
|
|
@@ -4097,6 +4765,11 @@ const Editor = forwardRef(({
|
|
|
4097
4765
|
}
|
|
4098
4766
|
pendingRenderRef.current = null;
|
|
4099
4767
|
}
|
|
4768
|
+
if (collabEnabled && yTextMap) {
|
|
4769
|
+
setTimeout(() => {
|
|
4770
|
+
bindAllBlocksToYText();
|
|
4771
|
+
}, 100);
|
|
4772
|
+
}
|
|
4100
4773
|
},
|
|
4101
4774
|
onChange: async (api) => {
|
|
4102
4775
|
try {
|
|
@@ -4116,9 +4789,9 @@ const Editor = forwardRef(({
|
|
|
4116
4789
|
pushLocalToCollabRef.current?.(docPayload);
|
|
4117
4790
|
lastSerializedValueRef.current = normalized;
|
|
4118
4791
|
if (count === 0) {
|
|
4119
|
-
onChange({ target: { name, value: null, type: "
|
|
4792
|
+
onChange({ target: { name, value: null, type: "json" } });
|
|
4120
4793
|
} else {
|
|
4121
|
-
onChange({ target: { name, value:
|
|
4794
|
+
onChange({ target: { name, value: outputData, type: "json" } });
|
|
4122
4795
|
}
|
|
4123
4796
|
} catch (error2) {
|
|
4124
4797
|
console.error("[Magic Editor X] Error in onChange:", error2);
|
|
@@ -4241,6 +4914,96 @@ const Editor = forwardRef(({
|
|
|
4241
4914
|
children: /* @__PURE__ */ jsx(SparklesIcon$1, {})
|
|
4242
4915
|
}
|
|
4243
4916
|
),
|
|
4917
|
+
isWebtoolsAvailable && /* @__PURE__ */ jsx(
|
|
4918
|
+
ToolButton,
|
|
4919
|
+
{
|
|
4920
|
+
type: "button",
|
|
4921
|
+
"data-tooltip": "Webtools Link Picker",
|
|
4922
|
+
onClick: async () => {
|
|
4923
|
+
if (!editorInstanceRef.current || !isReady) {
|
|
4924
|
+
console.warn("[Magic Editor X] Editor not ready");
|
|
4925
|
+
return;
|
|
4926
|
+
}
|
|
4927
|
+
const editor = editorInstanceRef.current;
|
|
4928
|
+
const {
|
|
4929
|
+
text: selectedText,
|
|
4930
|
+
range: savedRange,
|
|
4931
|
+
blockIndex,
|
|
4932
|
+
existingAnchor,
|
|
4933
|
+
existingHref
|
|
4934
|
+
} = webtoolsSelectionRef.current;
|
|
4935
|
+
console.log("[Magic Editor X] Webtools button clicked with stored selection:", {
|
|
4936
|
+
text: selectedText || "(none)",
|
|
4937
|
+
existingHref: existingHref || "(new link)",
|
|
4938
|
+
hasRange: !!savedRange,
|
|
4939
|
+
blockIndex
|
|
4940
|
+
});
|
|
4941
|
+
const currentBlockIndex = blockIndex >= 0 ? blockIndex : editor.blocks.getCurrentBlockIndex();
|
|
4942
|
+
const result = await webtoolsOpenLinkPicker({
|
|
4943
|
+
initialText: selectedText || "",
|
|
4944
|
+
initialHref: existingHref || ""
|
|
4945
|
+
});
|
|
4946
|
+
if (result && result.href) {
|
|
4947
|
+
const linkText = result.label || selectedText || result.href;
|
|
4948
|
+
const linkHtml = `<a href="${result.href}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
|
|
4949
|
+
if (existingAnchor && existingAnchor.parentNode) {
|
|
4950
|
+
try {
|
|
4951
|
+
existingAnchor.href = result.href;
|
|
4952
|
+
existingAnchor.textContent = linkText;
|
|
4953
|
+
const contentEditable = existingAnchor.closest('[contenteditable="true"]');
|
|
4954
|
+
if (contentEditable) {
|
|
4955
|
+
contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4956
|
+
}
|
|
4957
|
+
console.log("[Magic Editor X] Webtools link UPDATED:", {
|
|
4958
|
+
oldHref: existingHref,
|
|
4959
|
+
newHref: result.href,
|
|
4960
|
+
text: linkText
|
|
4961
|
+
});
|
|
4962
|
+
} catch (e) {
|
|
4963
|
+
console.error("[Magic Editor X] Failed to update link:", e);
|
|
4964
|
+
}
|
|
4965
|
+
} else if (savedRange && selectedText && currentBlockIndex >= 0) {
|
|
4966
|
+
try {
|
|
4967
|
+
const blockHolder = editor.blocks.getBlockByIndex(currentBlockIndex)?.holder;
|
|
4968
|
+
const contentEditable = blockHolder?.querySelector('[contenteditable="true"]');
|
|
4969
|
+
if (contentEditable) {
|
|
4970
|
+
const selection = window.getSelection();
|
|
4971
|
+
selection.removeAllRanges();
|
|
4972
|
+
selection.addRange(savedRange);
|
|
4973
|
+
document.execCommand("insertHTML", false, linkHtml);
|
|
4974
|
+
contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4975
|
+
console.log("[Magic Editor X] Webtools link CREATED:", {
|
|
4976
|
+
text: linkText,
|
|
4977
|
+
href: result.href
|
|
4978
|
+
});
|
|
4979
|
+
} else {
|
|
4980
|
+
editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
|
|
4981
|
+
}
|
|
4982
|
+
} catch (e) {
|
|
4983
|
+
console.error("[Magic Editor X] Failed to insert link:", e);
|
|
4984
|
+
editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
|
|
4985
|
+
}
|
|
4986
|
+
} else if (currentBlockIndex >= 0) {
|
|
4987
|
+
editor.blocks.insert("paragraph", {
|
|
4988
|
+
text: linkHtml
|
|
4989
|
+
}, {}, currentBlockIndex + 1, true);
|
|
4990
|
+
editor.caret.setToBlock(currentBlockIndex + 1);
|
|
4991
|
+
console.log("[Magic Editor X] Webtools link inserted (no selection):", result);
|
|
4992
|
+
} else {
|
|
4993
|
+
editor.blocks.insert("paragraph", { text: linkHtml });
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
webtoolsSelectionRef.current = { text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" };
|
|
4997
|
+
},
|
|
4998
|
+
disabled: collabEnabled && collabCanEdit === false,
|
|
4999
|
+
style: {
|
|
5000
|
+
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
5001
|
+
color: "white",
|
|
5002
|
+
...collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {}
|
|
5003
|
+
},
|
|
5004
|
+
children: /* @__PURE__ */ jsx(LinkIcon, {})
|
|
5005
|
+
}
|
|
5006
|
+
),
|
|
4244
5007
|
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
4245
5008
|
/* @__PURE__ */ jsx(
|
|
4246
5009
|
ToolButton,
|
|
@@ -4356,9 +5119,26 @@ const Editor = forwardRef(({
|
|
|
4356
5119
|
/* @__PURE__ */ jsx("strong", { children: charCount }),
|
|
4357
5120
|
" ",
|
|
4358
5121
|
t("editor.characters", "Zeichen")
|
|
4359
|
-
] })
|
|
5122
|
+
] }),
|
|
5123
|
+
!isWebtoolsAvailable && /* @__PURE__ */ jsxs(
|
|
5124
|
+
WebtoolsPromoLink,
|
|
5125
|
+
{
|
|
5126
|
+
href: "https://www.pluginpal.io/plugin/webtools",
|
|
5127
|
+
target: "_blank",
|
|
5128
|
+
rel: "noopener noreferrer",
|
|
5129
|
+
title: "Get Webtools Links addon for internal link management",
|
|
5130
|
+
children: [
|
|
5131
|
+
/* @__PURE__ */ jsx(LinkIcon, {}),
|
|
5132
|
+
"Internal Links? Get Webtools"
|
|
5133
|
+
]
|
|
5134
|
+
}
|
|
5135
|
+
)
|
|
4360
5136
|
] }),
|
|
4361
5137
|
/* @__PURE__ */ jsxs(FooterRight, { children: [
|
|
5138
|
+
/* @__PURE__ */ jsxs(FooterButton, { type: "button", onClick: () => setShowVersionHistory(true), children: [
|
|
5139
|
+
/* @__PURE__ */ jsx(ClockIcon, {}),
|
|
5140
|
+
t("editor.versionHistory", "History")
|
|
5141
|
+
] }),
|
|
4362
5142
|
!(collabEnabled && collabCanEdit === false) && /* @__PURE__ */ jsxs(FooterButton, { type: "button", onClick: () => handleInsertBlock("mediaLib"), children: [
|
|
4363
5143
|
/* @__PURE__ */ jsx(PhotoIcon, {}),
|
|
4364
5144
|
t("editor.mediaLibrary", "Media Library")
|
|
@@ -4431,6 +5211,51 @@ const Editor = forwardRef(({
|
|
|
4431
5211
|
}
|
|
4432
5212
|
}
|
|
4433
5213
|
),
|
|
5214
|
+
showVersionHistory && /* @__PURE__ */ jsx(VersionHistoryOverlay, { onClick: () => setShowVersionHistory(false), children: /* @__PURE__ */ jsx("div", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
5215
|
+
VersionHistoryPanel,
|
|
5216
|
+
{
|
|
5217
|
+
snapshots,
|
|
5218
|
+
loading: versionHistoryLoading,
|
|
5219
|
+
error: versionHistoryError,
|
|
5220
|
+
tier: licenseTier,
|
|
5221
|
+
onClose: () => setShowVersionHistory(false),
|
|
5222
|
+
onRestore: async (snapshot) => {
|
|
5223
|
+
if (snapshot.documentId && editorInstanceRef.current && isReady) {
|
|
5224
|
+
try {
|
|
5225
|
+
const result = await restoreSnapshot(snapshot.documentId, collabRoomId);
|
|
5226
|
+
const contentToRestore = result?.jsonContent || snapshot.jsonContent;
|
|
5227
|
+
if (contentToRestore && editorInstanceRef.current) {
|
|
5228
|
+
await editorInstanceRef.current.render(contentToRestore);
|
|
5229
|
+
setShowVersionHistory(false);
|
|
5230
|
+
onChange({ target: { name, value: contentToRestore, type: "json" } });
|
|
5231
|
+
}
|
|
5232
|
+
} catch (err) {
|
|
5233
|
+
console.error("[Magic Editor X] Failed to restore snapshot:", err?.message);
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
},
|
|
5237
|
+
onCreate: async () => {
|
|
5238
|
+
if (collabRoomId && editorInstanceRef.current && isReady) {
|
|
5239
|
+
const [contentType, entryId, fieldName] = collabRoomId.split("|");
|
|
5240
|
+
if (contentType && entryId && fieldName) {
|
|
5241
|
+
try {
|
|
5242
|
+
const editorContent = await editorInstanceRef.current.save();
|
|
5243
|
+
await createSnapshot({
|
|
5244
|
+
roomId: collabRoomId,
|
|
5245
|
+
contentType,
|
|
5246
|
+
entryId,
|
|
5247
|
+
fieldName,
|
|
5248
|
+
content: editorContent
|
|
5249
|
+
});
|
|
5250
|
+
fetchSnapshots(collabRoomId);
|
|
5251
|
+
} catch (err) {
|
|
5252
|
+
console.error("[Magic Editor X] Failed to create snapshot:", err?.message);
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
) }) }),
|
|
4434
5259
|
/* @__PURE__ */ jsx(
|
|
4435
5260
|
CreditsModal,
|
|
4436
5261
|
{
|