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
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
4
|
const React = require("react");
|
|
5
|
-
const getTranslation = require("./getTranslation-
|
|
5
|
+
const getTranslation = require("./getTranslation-DxG1pB5q.js");
|
|
6
6
|
const styled = require("styled-components");
|
|
7
7
|
const outline = require("@heroicons/react/24/outline");
|
|
8
8
|
const EditorJS = require("@editorjs/editorjs");
|
|
9
|
-
const tools = require("./tools-
|
|
9
|
+
const tools = require("./tools-Dn4jPdJs.js");
|
|
10
10
|
const admin = require("@strapi/strapi/admin");
|
|
11
|
-
const index = require("./index-
|
|
11
|
+
const index = require("./index-C_SiBh7v.js");
|
|
12
12
|
const socket_ioClient = require("socket.io-client");
|
|
13
13
|
const Y = require("yjs");
|
|
14
14
|
const yIndexeddb = require("y-indexeddb");
|
|
@@ -448,6 +448,89 @@ const getUserColor = (userId) => {
|
|
|
448
448
|
const hash = String(userId).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
449
449
|
return CURSOR_COLORS[hash % CURSOR_COLORS.length];
|
|
450
450
|
};
|
|
451
|
+
const htmlToDelta = (html) => {
|
|
452
|
+
if (!html || html === "<br>" || html === "<br/>") return [];
|
|
453
|
+
const parser = new DOMParser();
|
|
454
|
+
const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
|
|
455
|
+
const delta = [];
|
|
456
|
+
function processNode(node, attributes = {}) {
|
|
457
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
458
|
+
const text = node.textContent;
|
|
459
|
+
if (text) {
|
|
460
|
+
const attrs = Object.keys(attributes).length > 0 ? { ...attributes } : void 0;
|
|
461
|
+
delta.push({ insert: text, attributes: attrs });
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
466
|
+
const newAttrs = { ...attributes };
|
|
467
|
+
const tagName = node.tagName.toLowerCase();
|
|
468
|
+
switch (tagName) {
|
|
469
|
+
case "b":
|
|
470
|
+
case "strong":
|
|
471
|
+
newAttrs.bold = true;
|
|
472
|
+
break;
|
|
473
|
+
case "i":
|
|
474
|
+
case "em":
|
|
475
|
+
newAttrs.italic = true;
|
|
476
|
+
break;
|
|
477
|
+
case "u":
|
|
478
|
+
newAttrs.underline = true;
|
|
479
|
+
break;
|
|
480
|
+
case "code":
|
|
481
|
+
newAttrs.code = true;
|
|
482
|
+
break;
|
|
483
|
+
case "a":
|
|
484
|
+
newAttrs.a = {
|
|
485
|
+
href: node.getAttribute("href") || "",
|
|
486
|
+
target: node.getAttribute("target") || "_blank",
|
|
487
|
+
rel: node.getAttribute("rel") || "noopener noreferrer"
|
|
488
|
+
};
|
|
489
|
+
break;
|
|
490
|
+
case "mark":
|
|
491
|
+
newAttrs.mark = true;
|
|
492
|
+
break;
|
|
493
|
+
case "br":
|
|
494
|
+
delta.push({ insert: "\n" });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (const child of node.childNodes) {
|
|
498
|
+
processNode(child, newAttrs);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const wrapper = doc.body.firstChild;
|
|
503
|
+
if (wrapper) {
|
|
504
|
+
for (const child of wrapper.childNodes) {
|
|
505
|
+
processNode(child);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return delta;
|
|
509
|
+
};
|
|
510
|
+
const deltaToHtml = (delta) => {
|
|
511
|
+
if (!delta || delta.length === 0) return "";
|
|
512
|
+
let html = "";
|
|
513
|
+
for (const op of delta) {
|
|
514
|
+
if (typeof op.insert !== "string") continue;
|
|
515
|
+
let text = op.insert;
|
|
516
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br>");
|
|
517
|
+
const attrs = op.attributes || {};
|
|
518
|
+
let result = text;
|
|
519
|
+
if (attrs.code) result = `<code>${result}</code>`;
|
|
520
|
+
if (attrs.italic) result = `<i>${result}</i>`;
|
|
521
|
+
if (attrs.bold) result = `<b>${result}</b>`;
|
|
522
|
+
if (attrs.underline) result = `<u>${result}</u>`;
|
|
523
|
+
if (attrs.mark) result = `<mark>${result}</mark>`;
|
|
524
|
+
if (attrs.a) {
|
|
525
|
+
const href = attrs.a.href || "";
|
|
526
|
+
const target = attrs.a.target || "_blank";
|
|
527
|
+
const rel = attrs.a.rel || "noopener noreferrer";
|
|
528
|
+
result = `<a href="${href}" target="${target}" rel="${rel}">${result}</a>`;
|
|
529
|
+
}
|
|
530
|
+
html += result;
|
|
531
|
+
}
|
|
532
|
+
return html;
|
|
533
|
+
};
|
|
451
534
|
const useMagicCollaboration = ({
|
|
452
535
|
enabled,
|
|
453
536
|
roomId,
|
|
@@ -472,16 +555,47 @@ const useMagicCollaboration = ({
|
|
|
472
555
|
React.useEffect(() => {
|
|
473
556
|
onRemoteUpdateRef.current = onRemoteUpdate;
|
|
474
557
|
}, [onRemoteUpdate]);
|
|
475
|
-
const { doc, blocksMap, metaMap } = React.useMemo(() => {
|
|
558
|
+
const { doc, blocksMap, textMap, metaMap } = React.useMemo(() => {
|
|
476
559
|
const yDoc = new Y__namespace.Doc();
|
|
477
560
|
return {
|
|
478
561
|
doc: yDoc,
|
|
479
562
|
blocksMap: yDoc.getMap("blocks"),
|
|
480
|
-
//
|
|
563
|
+
// Block metadata (type, tunes)
|
|
564
|
+
textMap: yDoc.getMap("text"),
|
|
565
|
+
// Y.Text per block (character-level sync!)
|
|
481
566
|
metaMap: yDoc.getMap("meta")
|
|
482
|
-
//
|
|
567
|
+
// Document metadata (time, blockOrder)
|
|
483
568
|
};
|
|
484
569
|
}, [roomId]);
|
|
570
|
+
const getBlockText = React.useCallback((blockId) => {
|
|
571
|
+
if (!blockId) return null;
|
|
572
|
+
let ytext = textMap.get(blockId);
|
|
573
|
+
if (!ytext) {
|
|
574
|
+
ytext = new Y__namespace.Text();
|
|
575
|
+
textMap.set(blockId, ytext);
|
|
576
|
+
}
|
|
577
|
+
return ytext;
|
|
578
|
+
}, [textMap]);
|
|
579
|
+
const setBlockText = React.useCallback((blockId, html) => {
|
|
580
|
+
if (!blockId) return;
|
|
581
|
+
const ytext = getBlockText(blockId);
|
|
582
|
+
if (!ytext) return;
|
|
583
|
+
doc.transact(() => {
|
|
584
|
+
if (ytext.length > 0) {
|
|
585
|
+
ytext.delete(0, ytext.length);
|
|
586
|
+
}
|
|
587
|
+
const delta = htmlToDelta(html);
|
|
588
|
+
if (delta.length > 0) {
|
|
589
|
+
ytext.applyDelta(delta);
|
|
590
|
+
}
|
|
591
|
+
}, "local");
|
|
592
|
+
}, [doc, getBlockText]);
|
|
593
|
+
const getBlockTextHtml = React.useCallback((blockId) => {
|
|
594
|
+
if (!blockId) return "";
|
|
595
|
+
const ytext = textMap.get(blockId);
|
|
596
|
+
if (!ytext) return "";
|
|
597
|
+
return deltaToHtml(ytext.toDelta());
|
|
598
|
+
}, [textMap]);
|
|
485
599
|
React.useEffect(() => {
|
|
486
600
|
return () => {
|
|
487
601
|
doc.destroy();
|
|
@@ -546,9 +660,6 @@ const useMagicCollaboration = ({
|
|
|
546
660
|
persistenceRef.current = persistence;
|
|
547
661
|
persistence.on("synced", () => {
|
|
548
662
|
console.log("[Magic Collab] [CACHE] IndexedDB synced for room:", roomId);
|
|
549
|
-
const blockOrder = metaMap.get("blockOrder");
|
|
550
|
-
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(() => {
|
|
551
|
-
});
|
|
552
663
|
});
|
|
553
664
|
console.log("[Magic Collab] [CACHE] IndexedDB persistence initialized:", persistenceKey);
|
|
554
665
|
} catch (e) {
|
|
@@ -635,17 +746,11 @@ const useMagicCollaboration = ({
|
|
|
635
746
|
if (update) {
|
|
636
747
|
console.log("[Magic Collab] [SYNC] Syncing initial state, update size:", update.length, "bytes");
|
|
637
748
|
try {
|
|
638
|
-
const blockOrderBefore = metaMap.get("blockOrder");
|
|
639
|
-
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(() => {
|
|
640
|
-
});
|
|
641
749
|
const beforeBlockCount = blocksMap.size;
|
|
642
750
|
console.log("[Magic Collab] [DATA] Y.Map BEFORE sync - block count:", beforeBlockCount);
|
|
643
751
|
Y__namespace.applyUpdate(doc, new Uint8Array(update), "remote");
|
|
644
752
|
const afterBlockCount = blocksMap.size;
|
|
645
753
|
console.log("[Magic Collab] [DATA] Y.Map AFTER sync - block count:", afterBlockCount);
|
|
646
|
-
const blockOrderAfter = metaMap.get("blockOrder");
|
|
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: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(() => {
|
|
648
|
-
});
|
|
649
754
|
if (onRemoteUpdateRef.current) {
|
|
650
755
|
console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback after sync");
|
|
651
756
|
setTimeout(() => {
|
|
@@ -661,15 +766,11 @@ const useMagicCollaboration = ({
|
|
|
661
766
|
if (update) {
|
|
662
767
|
console.log("[Magic Collab] [UPDATE] Received remote update:", update.length, "bytes");
|
|
663
768
|
try {
|
|
664
|
-
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(() => {
|
|
665
|
-
});
|
|
666
769
|
const beforeBlockCount = blocksMap.size;
|
|
667
770
|
console.log("[Magic Collab] [DATA] Y.Map BEFORE update - blocks:", beforeBlockCount);
|
|
668
771
|
Y__namespace.applyUpdate(doc, new Uint8Array(update), "remote");
|
|
669
772
|
const afterBlockCount = blocksMap.size;
|
|
670
773
|
console.log("[Magic Collab] [DATA] Y.Map AFTER update - blocks:", afterBlockCount);
|
|
671
|
-
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(() => {
|
|
672
|
-
});
|
|
673
774
|
if (onRemoteUpdateRef.current) {
|
|
674
775
|
console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback");
|
|
675
776
|
setTimeout(() => {
|
|
@@ -793,13 +894,30 @@ const useMagicCollaboration = ({
|
|
|
793
894
|
return user ? getUserColor(user.id) : CURSOR_COLORS[0];
|
|
794
895
|
}, [user]);
|
|
795
896
|
return {
|
|
897
|
+
// Y.js Document & Maps
|
|
796
898
|
doc,
|
|
797
899
|
blocksMap,
|
|
798
|
-
// Y.Map for block
|
|
900
|
+
// Y.Map for block metadata (type, tunes)
|
|
901
|
+
textMap,
|
|
902
|
+
// Y.Map<blockId, Y.Text> for character-level text sync
|
|
799
903
|
metaMap,
|
|
800
|
-
// Y.Map for metadata (
|
|
904
|
+
// Y.Map for document metadata (time, blockOrder)
|
|
905
|
+
// Character-level text helpers
|
|
906
|
+
getBlockText,
|
|
907
|
+
// Get Y.Text for a block (creates if not exists)
|
|
908
|
+
setBlockText,
|
|
909
|
+
// Set block text from HTML
|
|
910
|
+
getBlockTextHtml,
|
|
911
|
+
// Get block text as HTML
|
|
912
|
+
// Utility functions
|
|
913
|
+
htmlToDelta,
|
|
914
|
+
// Convert HTML to delta
|
|
915
|
+
deltaToHtml,
|
|
916
|
+
// Convert delta to HTML
|
|
917
|
+
// Connection status
|
|
801
918
|
status,
|
|
802
919
|
error,
|
|
920
|
+
// Collaboration
|
|
803
921
|
peers,
|
|
804
922
|
awareness,
|
|
805
923
|
emitAwareness,
|
|
@@ -1097,6 +1215,136 @@ const useAIActions = ({ licenseKey, editorInstanceRef, isReady, onNoCredits }) =
|
|
|
1097
1215
|
}, [replaceText, appendText, onNoCredits]);
|
|
1098
1216
|
return { handleAIAction };
|
|
1099
1217
|
};
|
|
1218
|
+
const useWebtoolsLinks = () => {
|
|
1219
|
+
const getPlugin = admin.useStrapiApp("WebtoolsLinks", (state) => state.getPlugin);
|
|
1220
|
+
const linksPlugin = React.useMemo(() => {
|
|
1221
|
+
try {
|
|
1222
|
+
return getPlugin?.("webtools-addon-links");
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
}, [getPlugin]);
|
|
1227
|
+
const isAvailable = React.useMemo(() => {
|
|
1228
|
+
const available = !!linksPlugin?.apis?.openLinkPicker;
|
|
1229
|
+
if (typeof window !== "undefined" && !window.__WEBTOOLS_LINKS_CHECKED__) {
|
|
1230
|
+
window.__WEBTOOLS_LINKS_CHECKED__ = true;
|
|
1231
|
+
if (available) {
|
|
1232
|
+
console.log("[Magic Editor X] [SUCCESS] Webtools Links addon detected - Link Picker enabled");
|
|
1233
|
+
} else {
|
|
1234
|
+
console.log("[Magic Editor X] [INFO] Webtools Links addon not installed - Link Picker disabled");
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return available;
|
|
1238
|
+
}, [linksPlugin]);
|
|
1239
|
+
const openLinkPicker = React.useCallback(async ({ initialHref = "", initialText = "" } = {}) => {
|
|
1240
|
+
if (!linksPlugin?.apis?.openLinkPicker) {
|
|
1241
|
+
console.warn("[Magic Editor X] Webtools Link Picker not available");
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
console.log("[Magic Editor X] Opening Webtools Link Picker with:", {
|
|
1245
|
+
linkType: "both",
|
|
1246
|
+
initialHref: initialHref || "(empty)",
|
|
1247
|
+
initialText: initialText || "(empty)"
|
|
1248
|
+
});
|
|
1249
|
+
try {
|
|
1250
|
+
const result = await linksPlugin.apis.openLinkPicker({
|
|
1251
|
+
linkType: "both",
|
|
1252
|
+
// Allow both internal and external links
|
|
1253
|
+
initialHref: initialHref || "",
|
|
1254
|
+
initialText: initialText || ""
|
|
1255
|
+
});
|
|
1256
|
+
console.log("[Magic Editor X] Webtools picker result:", result);
|
|
1257
|
+
if (result && result.href) {
|
|
1258
|
+
console.log("[Magic Editor X] Webtools link selected:", result);
|
|
1259
|
+
return {
|
|
1260
|
+
href: result.href,
|
|
1261
|
+
label: result.label || initialText || ""
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
return null;
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
console.error("[Magic Editor X] Error opening Webtools Link Picker:", error);
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
}, [linksPlugin]);
|
|
1270
|
+
return {
|
|
1271
|
+
isAvailable,
|
|
1272
|
+
openLinkPicker
|
|
1273
|
+
};
|
|
1274
|
+
};
|
|
1275
|
+
const apiBase = "/magic-editor-x";
|
|
1276
|
+
const useVersionHistory = () => {
|
|
1277
|
+
const { get, post } = admin.useFetchClient();
|
|
1278
|
+
const { tier } = useLicense();
|
|
1279
|
+
const [snapshots, setSnapshots] = React.useState([]);
|
|
1280
|
+
const [loading, setLoading] = React.useState(false);
|
|
1281
|
+
const [error, setError] = React.useState(null);
|
|
1282
|
+
const fetchSnapshots = React.useCallback(
|
|
1283
|
+
async (roomId) => {
|
|
1284
|
+
if (!roomId) return;
|
|
1285
|
+
setLoading(true);
|
|
1286
|
+
setError(null);
|
|
1287
|
+
try {
|
|
1288
|
+
const { data } = await get(`${apiBase}/snapshots/${roomId}`);
|
|
1289
|
+
setSnapshots(data?.data || []);
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
setError(err?.message || "Failed to load snapshots");
|
|
1292
|
+
} finally {
|
|
1293
|
+
setLoading(false);
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
[get]
|
|
1297
|
+
);
|
|
1298
|
+
const restoreSnapshot = React.useCallback(
|
|
1299
|
+
async (documentId, roomId) => {
|
|
1300
|
+
if (!documentId) return;
|
|
1301
|
+
setLoading(true);
|
|
1302
|
+
setError(null);
|
|
1303
|
+
try {
|
|
1304
|
+
const { data } = await post(`${apiBase}/snapshots/restore/${documentId}`, { roomId });
|
|
1305
|
+
return data?.data;
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
setError(err?.message || "Failed to restore snapshot");
|
|
1308
|
+
throw err;
|
|
1309
|
+
} finally {
|
|
1310
|
+
setLoading(false);
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
[get]
|
|
1314
|
+
);
|
|
1315
|
+
const createSnapshot = React.useCallback(
|
|
1316
|
+
async ({ roomId, contentType, entryId, fieldName, content }) => {
|
|
1317
|
+
if (!roomId || !contentType || !entryId || !fieldName) return;
|
|
1318
|
+
setLoading(true);
|
|
1319
|
+
setError(null);
|
|
1320
|
+
try {
|
|
1321
|
+
const { data } = await post(`${apiBase}/snapshots/${roomId}`, {
|
|
1322
|
+
contentType,
|
|
1323
|
+
entryId,
|
|
1324
|
+
fieldName,
|
|
1325
|
+
content
|
|
1326
|
+
// Include editor content as fallback
|
|
1327
|
+
});
|
|
1328
|
+
return data?.data;
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
setError(err?.message || "Failed to create snapshot");
|
|
1331
|
+
throw err;
|
|
1332
|
+
} finally {
|
|
1333
|
+
setLoading(false);
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
[post]
|
|
1337
|
+
);
|
|
1338
|
+
return {
|
|
1339
|
+
snapshots,
|
|
1340
|
+
loading,
|
|
1341
|
+
error,
|
|
1342
|
+
tier,
|
|
1343
|
+
fetchSnapshots,
|
|
1344
|
+
restoreSnapshot,
|
|
1345
|
+
createSnapshot
|
|
1346
|
+
};
|
|
1347
|
+
};
|
|
1100
1348
|
const Overlay$1 = styled__default.default.div`
|
|
1101
1349
|
position: fixed;
|
|
1102
1350
|
top: 0;
|
|
@@ -1120,7 +1368,7 @@ const PopupContainer = styled__default.default.div`
|
|
|
1120
1368
|
display: flex;
|
|
1121
1369
|
flex-direction: column;
|
|
1122
1370
|
`;
|
|
1123
|
-
const Header$
|
|
1371
|
+
const Header$2 = styled__default.default.div`
|
|
1124
1372
|
background: linear-gradient(135deg, #7C3AED 0%, #a855f7 100%);
|
|
1125
1373
|
padding: 20px 24px;
|
|
1126
1374
|
color: white;
|
|
@@ -1142,7 +1390,7 @@ const CreditsBadge = styled__default.default.div`
|
|
|
1142
1390
|
font-size: 13px;
|
|
1143
1391
|
font-weight: 500;
|
|
1144
1392
|
`;
|
|
1145
|
-
const Content$
|
|
1393
|
+
const Content$2 = styled__default.default.div`
|
|
1146
1394
|
padding: 24px;
|
|
1147
1395
|
overflow-y: auto;
|
|
1148
1396
|
flex: 1;
|
|
@@ -1407,7 +1655,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
|
|
|
1407
1655
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1408
1656
|
}, [onClose]);
|
|
1409
1657
|
return /* @__PURE__ */ jsxRuntime.jsx(Overlay$1, { onClick: handleOverlayClick, children: /* @__PURE__ */ jsxRuntime.jsxs(PopupContainer, { onClick: (e) => e.stopPropagation(), children: [
|
|
1410
|
-
/* @__PURE__ */ jsxRuntime.jsxs(Header$
|
|
1658
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Header$2, { children: [
|
|
1411
1659
|
/* @__PURE__ */ jsxRuntime.jsxs(HeaderTitle, { children: [
|
|
1412
1660
|
/* @__PURE__ */ jsxRuntime.jsx(SparklesIcon, {}),
|
|
1413
1661
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "KI-Assistent" })
|
|
@@ -1419,7 +1667,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
|
|
|
1419
1667
|
!usage?.tier && "Wird geladen..."
|
|
1420
1668
|
] })
|
|
1421
1669
|
] }),
|
|
1422
|
-
/* @__PURE__ */ jsxRuntime.jsxs(Content$
|
|
1670
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Content$2, { children: [
|
|
1423
1671
|
/* @__PURE__ */ jsxRuntime.jsx(TextPreview, { children: selectedText.length > 300 ? selectedText.substring(0, 300) + "..." : selectedText }),
|
|
1424
1672
|
/* @__PURE__ */ jsxRuntime.jsxs(TypeButtons, { children: [
|
|
1425
1673
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -1506,6 +1754,132 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
|
|
|
1506
1754
|
] })
|
|
1507
1755
|
] }) });
|
|
1508
1756
|
};
|
|
1757
|
+
const PanelWrapper = styled__default.default(getTranslation.Box)`
|
|
1758
|
+
width: 320px;
|
|
1759
|
+
background: ${({ theme }) => theme.colors.neutral0};
|
|
1760
|
+
border: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
1761
|
+
border-radius: 8px;
|
|
1762
|
+
box-shadow: ${({ theme }) => theme.shadows.filterShadow};
|
|
1763
|
+
display: flex;
|
|
1764
|
+
flex-direction: column;
|
|
1765
|
+
max-height: 70vh;
|
|
1766
|
+
`;
|
|
1767
|
+
const Header$1 = styled__default.default(getTranslation.Flex)`
|
|
1768
|
+
padding: 12px 16px;
|
|
1769
|
+
border-bottom: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
1770
|
+
`;
|
|
1771
|
+
const Content$1 = styled__default.default(getTranslation.Box)`
|
|
1772
|
+
padding: 12px 16px;
|
|
1773
|
+
overflow-y: auto;
|
|
1774
|
+
`;
|
|
1775
|
+
const Item = styled__default.default(getTranslation.Box)`
|
|
1776
|
+
padding: 10px 12px;
|
|
1777
|
+
border: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
1778
|
+
border-radius: 6px;
|
|
1779
|
+
margin-bottom: 10px;
|
|
1780
|
+
`;
|
|
1781
|
+
const Meta = styled__default.default(getTranslation.Typography)`
|
|
1782
|
+
color: ${({ theme }) => theme.colors.neutral500};
|
|
1783
|
+
font-size: 12px;
|
|
1784
|
+
`;
|
|
1785
|
+
const PremiumBadge = styled__default.default(getTranslation.Box)`
|
|
1786
|
+
background: ${({ theme }) => theme.colors.primary100};
|
|
1787
|
+
color: ${({ theme }) => theme.colors.primary600};
|
|
1788
|
+
border-radius: 6px;
|
|
1789
|
+
padding: 8px 10px;
|
|
1790
|
+
display: inline-flex;
|
|
1791
|
+
align-items: center;
|
|
1792
|
+
gap: 8px;
|
|
1793
|
+
font-weight: 600;
|
|
1794
|
+
margin-top: 8px;
|
|
1795
|
+
`;
|
|
1796
|
+
const safeDateFrom = (value) => {
|
|
1797
|
+
if (!value) return null;
|
|
1798
|
+
if (value instanceof Date) {
|
|
1799
|
+
return isNaN(value.getTime()) ? null : value;
|
|
1800
|
+
}
|
|
1801
|
+
try {
|
|
1802
|
+
const parsed = new Date(value);
|
|
1803
|
+
return isNaN(parsed.getTime()) ? null : parsed;
|
|
1804
|
+
} catch {
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
const formatDate = (dateValue) => {
|
|
1809
|
+
const date = safeDateFrom(dateValue);
|
|
1810
|
+
if (!date) return "—";
|
|
1811
|
+
try {
|
|
1812
|
+
return date.toLocaleString();
|
|
1813
|
+
} catch {
|
|
1814
|
+
return "—";
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
const VersionHistoryPanel = ({
|
|
1818
|
+
snapshots,
|
|
1819
|
+
loading,
|
|
1820
|
+
error,
|
|
1821
|
+
onRestore,
|
|
1822
|
+
onCreate,
|
|
1823
|
+
tier,
|
|
1824
|
+
onClose
|
|
1825
|
+
}) => {
|
|
1826
|
+
const { formatMessage } = getTranslation.useIntl();
|
|
1827
|
+
const canRestore = tier !== "free";
|
|
1828
|
+
const t = (id, defaultMessage) => formatMessage(
|
|
1829
|
+
{ id: getTranslation.getTranslation(id), defaultMessage },
|
|
1830
|
+
{}
|
|
1831
|
+
);
|
|
1832
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(PanelWrapper, { "data-testid": "version-history-panel", children: [
|
|
1833
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Header$1, { justifyContent: "space-between", alignItems: "center", children: [
|
|
1834
|
+
/* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Flex, { gap: 8, alignItems: "center", children: [
|
|
1835
|
+
/* @__PURE__ */ jsxRuntime.jsx(outline.ClockIcon, { width: 18 }),
|
|
1836
|
+
/* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { fontWeight: "bold", children: t("versionHistory.title", "Version History") })
|
|
1837
|
+
] }),
|
|
1838
|
+
/* @__PURE__ */ jsxRuntime.jsx(getTranslation.Button, { size: "S", variant: "tertiary", onClick: onClose, children: t("versionHistory.close", "Close") })
|
|
1839
|
+
] }),
|
|
1840
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Content$1, { children: [
|
|
1841
|
+
loading && /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { children: t("versionHistory.loading", "Loading versions...") }),
|
|
1842
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { textColor: "danger600", children: error }),
|
|
1843
|
+
!loading && !error && snapshots.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { children: t("versionHistory.noSnapshots", "No versions saved yet") }),
|
|
1844
|
+
!loading && !error && snapshots.map((snap) => /* @__PURE__ */ jsxRuntime.jsxs(Item, { children: [
|
|
1845
|
+
/* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Flex, { justifyContent: "space-between", alignItems: "center", children: [
|
|
1846
|
+
/* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Typography, { fontWeight: "bold", children: [
|
|
1847
|
+
t("versionHistory.version", "Version"),
|
|
1848
|
+
" ",
|
|
1849
|
+
snap.version
|
|
1850
|
+
] }),
|
|
1851
|
+
/* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { variant: "pi", children: formatDate(snap.createdAt) })
|
|
1852
|
+
] }),
|
|
1853
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Meta, { children: [
|
|
1854
|
+
t("versionHistory.createdBy", "By"),
|
|
1855
|
+
" ",
|
|
1856
|
+
snap.createdBy?.firstname ? `${snap.createdBy.firstname} ${snap.createdBy.lastname || ""}`.trim() : "—"
|
|
1857
|
+
] }),
|
|
1858
|
+
/* @__PURE__ */ jsxRuntime.jsx(getTranslation.Divider, { marginTop: 2, marginBottom: 2 }),
|
|
1859
|
+
canRestore ? /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Flex, { gap: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1860
|
+
getTranslation.Button,
|
|
1861
|
+
{
|
|
1862
|
+
size: "S",
|
|
1863
|
+
variant: "secondary",
|
|
1864
|
+
onClick: () => onRestore?.(snap),
|
|
1865
|
+
children: t("versionHistory.restore", "Restore")
|
|
1866
|
+
}
|
|
1867
|
+
) }) : /* @__PURE__ */ jsxRuntime.jsxs(PremiumBadge, { children: [
|
|
1868
|
+
/* @__PURE__ */ jsxRuntime.jsx(outline.ExclamationTriangleIcon, { width: 16 }),
|
|
1869
|
+
t("versionHistory.premiumOnly", "Premium feature")
|
|
1870
|
+
] })
|
|
1871
|
+
] }, snap.documentId || snap.id)),
|
|
1872
|
+
/* @__PURE__ */ jsxRuntime.jsx(getTranslation.Divider, { marginTop: 4, marginBottom: 4 }),
|
|
1873
|
+
canRestore ? /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Button, { size: "S", fullWidth: true, variant: "default", onClick: onCreate, disabled: loading, children: t("versionHistory.create", "Create Snapshot") }) : /* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Box, { children: [
|
|
1874
|
+
/* @__PURE__ */ jsxRuntime.jsx(getTranslation.Button, { size: "S", fullWidth: true, variant: "default", disabled: true, children: t("versionHistory.create", "Create Snapshot") }),
|
|
1875
|
+
/* @__PURE__ */ jsxRuntime.jsxs(PremiumBadge, { style: { marginTop: "8px", width: "100%", justifyContent: "center" }, children: [
|
|
1876
|
+
/* @__PURE__ */ jsxRuntime.jsx(outline.ExclamationTriangleIcon, { width: 16 }),
|
|
1877
|
+
t("versionHistory.premiumOnly", "Premium feature")
|
|
1878
|
+
] })
|
|
1879
|
+
] })
|
|
1880
|
+
] })
|
|
1881
|
+
] });
|
|
1882
|
+
};
|
|
1509
1883
|
const Overlay = styled__default.default.div`
|
|
1510
1884
|
position: fixed;
|
|
1511
1885
|
top: 0;
|
|
@@ -1820,11 +2194,14 @@ const EditorJSGlobalStyles = styled.createGlobalStyle`
|
|
|
1820
2194
|
|
|
1821
2195
|
/* ============================================
|
|
1822
2196
|
STRAPI MEDIA LIBRARY - Higher z-index for fullscreen
|
|
2197
|
+
Must be higher than fullscreen z-index (9999)
|
|
1823
2198
|
============================================ */
|
|
1824
2199
|
[data-react-portal],
|
|
1825
2200
|
.ReactModalPortal,
|
|
1826
2201
|
[role="dialog"],
|
|
1827
2202
|
[data-strapi-modal="true"],
|
|
2203
|
+
[class*="Dialog"],
|
|
2204
|
+
[class*="Modal"],
|
|
1828
2205
|
.upload-dialog,
|
|
1829
2206
|
[class*="Modal"],
|
|
1830
2207
|
[class*="modal"],
|
|
@@ -2473,15 +2850,36 @@ const EditorContent = styled__default.default.div`
|
|
|
2473
2850
|
flex: 1;
|
|
2474
2851
|
overflow: visible; /* Allow toolbars/popovers to escape */
|
|
2475
2852
|
position: relative;
|
|
2476
|
-
padding: 24px
|
|
2853
|
+
padding: 24px;
|
|
2477
2854
|
min-height: 200px;
|
|
2478
2855
|
|
|
2479
2856
|
${(props) => props.$isFullscreen && styled.css`
|
|
2480
|
-
padding: clamp(
|
|
2857
|
+
padding: clamp(24px, 3vw, 48px);
|
|
2481
2858
|
width: 100%;
|
|
2482
2859
|
max-width: 100%;
|
|
2483
2860
|
margin: 0;
|
|
2484
2861
|
align-self: stretch;
|
|
2862
|
+
|
|
2863
|
+
/* Make blocks stretch full width in fullscreen */
|
|
2864
|
+
.codex-editor {
|
|
2865
|
+
width: 100%;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
.ce-block__content,
|
|
2869
|
+
.ce-toolbar__content {
|
|
2870
|
+
max-width: 100% !important;
|
|
2871
|
+
padding: 0 !important;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
.ce-toolbar {
|
|
2875
|
+
max-width: 100% !important;
|
|
2876
|
+
left: 0 !important;
|
|
2877
|
+
transform: none !important;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
.ce-toolbar__actions {
|
|
2881
|
+
right: 0 !important;
|
|
2882
|
+
}
|
|
2485
2883
|
`}
|
|
2486
2884
|
`;
|
|
2487
2885
|
const EditorWrapper = styled__default.default.div`
|
|
@@ -2556,18 +2954,19 @@ const EditorWrapper = styled__default.default.div`
|
|
|
2556
2954
|
TOOLBAR INSIDE EDITOR - Position Fix
|
|
2557
2955
|
============================================ */
|
|
2558
2956
|
|
|
2559
|
-
/*
|
|
2957
|
+
/* Centered content area */
|
|
2560
2958
|
.codex-editor__redactor {
|
|
2561
2959
|
padding-bottom: 100px !important;
|
|
2562
|
-
padding-left:
|
|
2563
|
-
margin
|
|
2960
|
+
padding-left: 0 !important;
|
|
2961
|
+
margin: 0 auto !important;
|
|
2962
|
+
max-width: 800px !important;
|
|
2564
2963
|
}
|
|
2565
2964
|
|
|
2566
|
-
/* Content blocks -
|
|
2965
|
+
/* Content blocks - centered */
|
|
2567
2966
|
.ce-block__content {
|
|
2568
2967
|
max-width: 100%;
|
|
2569
|
-
margin
|
|
2570
|
-
|
|
2968
|
+
margin: 0 auto;
|
|
2969
|
+
padding: 0 16px;
|
|
2571
2970
|
}
|
|
2572
2971
|
|
|
2573
2972
|
/* ============================================
|
|
@@ -2616,24 +3015,27 @@ const EditorWrapper = styled__default.default.div`
|
|
|
2616
3015
|
border-radius: 6px;
|
|
2617
3016
|
}
|
|
2618
3017
|
|
|
2619
|
-
/* Toolbar positioning -
|
|
3018
|
+
/* Toolbar positioning - centered with content */
|
|
2620
3019
|
.ce-toolbar__content {
|
|
2621
|
-
max-width:
|
|
2622
|
-
margin
|
|
3020
|
+
max-width: 800px;
|
|
3021
|
+
margin: 0 auto;
|
|
3022
|
+
padding: 0 16px;
|
|
2623
3023
|
}
|
|
2624
3024
|
|
|
2625
3025
|
.ce-toolbar {
|
|
2626
|
-
left:
|
|
3026
|
+
left: 50% !important;
|
|
3027
|
+
transform: translateX(-50%) !important;
|
|
3028
|
+
width: 100% !important;
|
|
3029
|
+
max-width: 832px !important;
|
|
2627
3030
|
}
|
|
2628
3031
|
|
|
2629
3032
|
.ce-toolbar__plus {
|
|
2630
|
-
left: 0 !important;
|
|
2631
3033
|
position: relative !important;
|
|
2632
3034
|
}
|
|
2633
3035
|
|
|
2634
3036
|
.ce-toolbar__actions {
|
|
2635
|
-
right: 0 !important;
|
|
2636
3037
|
position: absolute !important;
|
|
3038
|
+
right: 16px !important;
|
|
2637
3039
|
}
|
|
2638
3040
|
|
|
2639
3041
|
/* Settings button (⋮⋮) */
|
|
@@ -3119,6 +3521,35 @@ const FooterButton = styled__default.default.button`
|
|
|
3119
3521
|
}
|
|
3120
3522
|
}
|
|
3121
3523
|
`;
|
|
3524
|
+
const WebtoolsPromoLink = styled__default.default.a`
|
|
3525
|
+
display: inline-flex;
|
|
3526
|
+
align-items: center;
|
|
3527
|
+
gap: 6px;
|
|
3528
|
+
padding: 4px 10px;
|
|
3529
|
+
background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%);
|
|
3530
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
3531
|
+
border-radius: 6px;
|
|
3532
|
+
font-size: 11px;
|
|
3533
|
+
color: #6366f1;
|
|
3534
|
+
text-decoration: none;
|
|
3535
|
+
transition: all 0.2s ease;
|
|
3536
|
+
white-space: nowrap;
|
|
3537
|
+
|
|
3538
|
+
svg {
|
|
3539
|
+
width: 12px;
|
|
3540
|
+
height: 12px;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
&:hover {
|
|
3544
|
+
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
|
|
3545
|
+
border-color: rgba(99, 102, 241, 0.4);
|
|
3546
|
+
transform: translateY(-1px);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
@media (max-width: 768px) {
|
|
3550
|
+
display: none;
|
|
3551
|
+
}
|
|
3552
|
+
`;
|
|
3122
3553
|
const LoadingOverlay = styled__default.default.div`
|
|
3123
3554
|
position: absolute;
|
|
3124
3555
|
top: 0;
|
|
@@ -3133,6 +3564,18 @@ const LoadingOverlay = styled__default.default.div`
|
|
|
3133
3564
|
gap: 12px;
|
|
3134
3565
|
z-index: 10;
|
|
3135
3566
|
`;
|
|
3567
|
+
const VersionHistoryOverlay = styled__default.default.div`
|
|
3568
|
+
position: fixed;
|
|
3569
|
+
top: 0;
|
|
3570
|
+
left: 0;
|
|
3571
|
+
right: 0;
|
|
3572
|
+
bottom: 0;
|
|
3573
|
+
background: rgba(0, 0, 0, 0.5);
|
|
3574
|
+
display: flex;
|
|
3575
|
+
align-items: center;
|
|
3576
|
+
justify-content: center;
|
|
3577
|
+
z-index: 99999;
|
|
3578
|
+
`;
|
|
3136
3579
|
const LoadingText = styled__default.default.span`
|
|
3137
3580
|
font-size: 13px;
|
|
3138
3581
|
color: #64748b;
|
|
@@ -3348,10 +3791,12 @@ const Editor = React.forwardRef(({
|
|
|
3348
3791
|
}, ref) => {
|
|
3349
3792
|
const { formatMessage } = getTranslation.useIntl();
|
|
3350
3793
|
const t = (id, defaultMessage) => formatMessage({ id: getTranslation.getTranslation(id), defaultMessage });
|
|
3351
|
-
const { licenseData } = useLicense();
|
|
3794
|
+
const { licenseData, tier: licenseTier } = useLicense();
|
|
3795
|
+
const { isAvailable: isWebtoolsAvailable, openLinkPicker: webtoolsOpenLinkPicker } = useWebtoolsLinks();
|
|
3352
3796
|
const editorRef = React.useRef(null);
|
|
3353
3797
|
const editorInstanceRef = React.useRef(null);
|
|
3354
3798
|
const containerRef = React.useRef(null);
|
|
3799
|
+
const webtoolsSelectionRef = React.useRef({ text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" });
|
|
3355
3800
|
const isReadyRef = React.useRef(false);
|
|
3356
3801
|
const [isReady, setIsReady] = React.useState(false);
|
|
3357
3802
|
const [showCreditsModal, setShowCreditsModal] = React.useState(false);
|
|
@@ -3387,6 +3832,51 @@ const Editor = React.forwardRef(({
|
|
|
3387
3832
|
const [aiSelectedText, setAISelectedText] = React.useState("");
|
|
3388
3833
|
const aiSelectionRangeRef = React.useRef(null);
|
|
3389
3834
|
const [aiLoading, setAILoading] = React.useState(false);
|
|
3835
|
+
const [showVersionHistory, setShowVersionHistory] = React.useState(false);
|
|
3836
|
+
React.useEffect(() => {
|
|
3837
|
+
if (!isWebtoolsAvailable || !editorRef.current) return;
|
|
3838
|
+
const updateWebtoolsSelection = () => {
|
|
3839
|
+
const selection = window.getSelection();
|
|
3840
|
+
if (!selection || selection.rangeCount === 0) return;
|
|
3841
|
+
const range = selection.getRangeAt(0);
|
|
3842
|
+
if (!editorRef.current.contains(range.commonAncestorContainer)) return;
|
|
3843
|
+
const selectedText = selection.toString().trim();
|
|
3844
|
+
let existingAnchor = null;
|
|
3845
|
+
let existingHref = "";
|
|
3846
|
+
let node = range.commonAncestorContainer;
|
|
3847
|
+
while (node && node !== editorRef.current) {
|
|
3848
|
+
if (node.nodeName === "A") {
|
|
3849
|
+
existingAnchor = node;
|
|
3850
|
+
existingHref = node.href || "";
|
|
3851
|
+
break;
|
|
3852
|
+
}
|
|
3853
|
+
node = node.parentNode;
|
|
3854
|
+
}
|
|
3855
|
+
if (!existingAnchor) {
|
|
3856
|
+
node = range.startContainer;
|
|
3857
|
+
while (node && node !== editorRef.current) {
|
|
3858
|
+
if (node.nodeName === "A") {
|
|
3859
|
+
existingAnchor = node;
|
|
3860
|
+
existingHref = node.href || "";
|
|
3861
|
+
break;
|
|
3862
|
+
}
|
|
3863
|
+
node = node.parentNode;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
const blockIndex = editorInstanceRef.current?.blocks?.getCurrentBlockIndex?.() ?? -1;
|
|
3867
|
+
webtoolsSelectionRef.current = {
|
|
3868
|
+
text: existingAnchor ? existingAnchor.textContent : selectedText,
|
|
3869
|
+
range: range.cloneRange(),
|
|
3870
|
+
blockIndex,
|
|
3871
|
+
existingAnchor,
|
|
3872
|
+
existingHref
|
|
3873
|
+
};
|
|
3874
|
+
};
|
|
3875
|
+
document.addEventListener("selectionchange", updateWebtoolsSelection);
|
|
3876
|
+
return () => {
|
|
3877
|
+
document.removeEventListener("selectionchange", updateWebtoolsSelection);
|
|
3878
|
+
};
|
|
3879
|
+
}, [isWebtoolsAvailable, isReady]);
|
|
3390
3880
|
const serializedInitialValue = React.useMemo(() => {
|
|
3391
3881
|
if (!value) {
|
|
3392
3882
|
return "";
|
|
@@ -3414,12 +3904,25 @@ const Editor = React.forwardRef(({
|
|
|
3414
3904
|
const {
|
|
3415
3905
|
doc: yDoc,
|
|
3416
3906
|
blocksMap: yBlocksMap,
|
|
3907
|
+
textMap: yTextMap,
|
|
3908
|
+
// NEW: Y.Map<blockId, Y.Text> for character-level sync
|
|
3417
3909
|
metaMap: yMetaMap,
|
|
3910
|
+
// Character-level text helpers
|
|
3911
|
+
getBlockText,
|
|
3912
|
+
// NEW: Get Y.Text for a block
|
|
3913
|
+
setBlockText,
|
|
3914
|
+
// NEW: Get block text as HTML
|
|
3915
|
+
// Utility functions
|
|
3916
|
+
htmlToDelta: collabHtmlToDelta,
|
|
3917
|
+
deltaToHtml: collabDeltaToHtml,
|
|
3918
|
+
// Connection status
|
|
3418
3919
|
status: collabStatus,
|
|
3419
3920
|
error: collabError,
|
|
3921
|
+
// Collaboration
|
|
3420
3922
|
peers: collabPeers,
|
|
3421
3923
|
awareness: collabAwareness,
|
|
3422
3924
|
emitAwareness,
|
|
3925
|
+
// Role-based access control
|
|
3423
3926
|
collabRole,
|
|
3424
3927
|
canEdit: collabCanEdit
|
|
3425
3928
|
} = useMagicCollaboration({
|
|
@@ -3434,6 +3937,171 @@ const Editor = React.forwardRef(({
|
|
|
3434
3937
|
}
|
|
3435
3938
|
}
|
|
3436
3939
|
});
|
|
3940
|
+
const yTextBindingsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
3941
|
+
const bindBlockToYText = React.useCallback((blockId, element) => {
|
|
3942
|
+
if (!collabEnabled || !blockId || !element || !yTextMap) return;
|
|
3943
|
+
if (yTextBindingsRef.current.has(blockId)) {
|
|
3944
|
+
const existing = yTextBindingsRef.current.get(blockId);
|
|
3945
|
+
if (existing.element === element) return;
|
|
3946
|
+
unbindBlockFromYText(blockId);
|
|
3947
|
+
}
|
|
3948
|
+
const ytext = getBlockText(blockId);
|
|
3949
|
+
if (!ytext) return;
|
|
3950
|
+
let isUpdating = false;
|
|
3951
|
+
const ytextObserver = (event) => {
|
|
3952
|
+
if (isUpdating) return;
|
|
3953
|
+
if (event.transaction.local) return;
|
|
3954
|
+
isUpdating = true;
|
|
3955
|
+
try {
|
|
3956
|
+
const selection = window.getSelection();
|
|
3957
|
+
let cursorOffset = 0;
|
|
3958
|
+
if (selection && selection.rangeCount > 0 && element.contains(selection.anchorNode)) {
|
|
3959
|
+
const range = selection.getRangeAt(0);
|
|
3960
|
+
const preCaretRange = document.createRange();
|
|
3961
|
+
preCaretRange.selectNodeContents(element);
|
|
3962
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
3963
|
+
cursorOffset = preCaretRange.toString().length;
|
|
3964
|
+
}
|
|
3965
|
+
const html = collabDeltaToHtml(ytext.toDelta());
|
|
3966
|
+
if (element.innerHTML !== html) {
|
|
3967
|
+
element.innerHTML = html || "";
|
|
3968
|
+
}
|
|
3969
|
+
if (document.activeElement === element && cursorOffset > 0) {
|
|
3970
|
+
try {
|
|
3971
|
+
const textNode = element.firstChild;
|
|
3972
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
3973
|
+
const newRange = document.createRange();
|
|
3974
|
+
const pos = Math.min(cursorOffset, textNode.length);
|
|
3975
|
+
newRange.setStart(textNode, pos);
|
|
3976
|
+
newRange.setEnd(textNode, pos);
|
|
3977
|
+
selection.removeAllRanges();
|
|
3978
|
+
selection.addRange(newRange);
|
|
3979
|
+
}
|
|
3980
|
+
} catch (e) {
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
} finally {
|
|
3984
|
+
isUpdating = false;
|
|
3985
|
+
}
|
|
3986
|
+
};
|
|
3987
|
+
const inputHandler = () => {
|
|
3988
|
+
if (isUpdating) return;
|
|
3989
|
+
isUpdating = true;
|
|
3990
|
+
try {
|
|
3991
|
+
const html = element.innerHTML;
|
|
3992
|
+
const newDelta = collabHtmlToDelta(html);
|
|
3993
|
+
const currentDelta = ytext.toDelta();
|
|
3994
|
+
yDoc.transact(() => {
|
|
3995
|
+
if (ytext.length > 0) {
|
|
3996
|
+
ytext.delete(0, ytext.length);
|
|
3997
|
+
}
|
|
3998
|
+
if (newDelta.length > 0) {
|
|
3999
|
+
ytext.applyDelta(newDelta);
|
|
4000
|
+
}
|
|
4001
|
+
}, "local");
|
|
4002
|
+
} finally {
|
|
4003
|
+
isUpdating = false;
|
|
4004
|
+
}
|
|
4005
|
+
};
|
|
4006
|
+
ytext.observe(ytextObserver);
|
|
4007
|
+
element.addEventListener("input", inputHandler);
|
|
4008
|
+
yTextBindingsRef.current.set(blockId, {
|
|
4009
|
+
ytext,
|
|
4010
|
+
element,
|
|
4011
|
+
ytextObserver,
|
|
4012
|
+
inputHandler
|
|
4013
|
+
});
|
|
4014
|
+
console.log("[Magic Editor X] [CHAR-SYNC] Bound block to Y.Text:", blockId);
|
|
4015
|
+
}, [collabEnabled, yTextMap, yDoc, getBlockText, collabHtmlToDelta, collabDeltaToHtml]);
|
|
4016
|
+
const unbindBlockFromYText = React.useCallback((blockId) => {
|
|
4017
|
+
const binding = yTextBindingsRef.current.get(blockId);
|
|
4018
|
+
if (!binding) return;
|
|
4019
|
+
binding.ytext.unobserve(binding.ytextObserver);
|
|
4020
|
+
binding.element.removeEventListener("input", binding.inputHandler);
|
|
4021
|
+
yTextBindingsRef.current.delete(blockId);
|
|
4022
|
+
console.log("[Magic Editor X] [CHAR-SYNC] Unbound block from Y.Text:", blockId);
|
|
4023
|
+
}, []);
|
|
4024
|
+
const bindAllBlocksToYText = React.useCallback(() => {
|
|
4025
|
+
if (!collabEnabled || !editorInstanceRef.current || !yTextMap) return;
|
|
4026
|
+
const editor = editorInstanceRef.current;
|
|
4027
|
+
const blockCount = editor.blocks.getBlocksCount();
|
|
4028
|
+
console.log("[Magic Editor X] [CHAR-SYNC] Binding", blockCount, "blocks to Y.Text");
|
|
4029
|
+
for (let i = 0; i < blockCount; i++) {
|
|
4030
|
+
try {
|
|
4031
|
+
const block = editor.blocks.getBlockByIndex(i);
|
|
4032
|
+
if (!block || !block.id) continue;
|
|
4033
|
+
const blockHolder = block.holder;
|
|
4034
|
+
if (!blockHolder) continue;
|
|
4035
|
+
const contentEditable = blockHolder.querySelector('[contenteditable="true"]');
|
|
4036
|
+
if (contentEditable) {
|
|
4037
|
+
const ytext = getBlockText(block.id);
|
|
4038
|
+
if (ytext && ytext.length === 0) {
|
|
4039
|
+
const currentHtml = contentEditable.innerHTML;
|
|
4040
|
+
if (currentHtml && currentHtml !== "<br>") {
|
|
4041
|
+
setBlockText(block.id, currentHtml);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
bindBlockToYText(block.id, contentEditable);
|
|
4045
|
+
}
|
|
4046
|
+
} catch (e) {
|
|
4047
|
+
console.warn("[Magic Editor X] [CHAR-SYNC] Error binding block:", e);
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
}, [collabEnabled, yTextMap, getBlockText, setBlockText, bindBlockToYText]);
|
|
4051
|
+
const blockObserverRef = React.useRef(null);
|
|
4052
|
+
React.useEffect(() => {
|
|
4053
|
+
if (!collabEnabled || !editorRef.current || !yTextMap) return;
|
|
4054
|
+
const observer = new MutationObserver((mutations) => {
|
|
4055
|
+
let hasNewBlocks = false;
|
|
4056
|
+
for (const mutation of mutations) {
|
|
4057
|
+
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
|
4058
|
+
for (const node of mutation.addedNodes) {
|
|
4059
|
+
if (node.nodeType === Node.ELEMENT_NODE && (node.classList?.contains("ce-block") || node.querySelector?.(".ce-block"))) {
|
|
4060
|
+
hasNewBlocks = true;
|
|
4061
|
+
break;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
if (hasNewBlocks) break;
|
|
4066
|
+
}
|
|
4067
|
+
if (hasNewBlocks) {
|
|
4068
|
+
setTimeout(() => {
|
|
4069
|
+
bindAllBlocksToYText();
|
|
4070
|
+
}, 50);
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
4073
|
+
observer.observe(editorRef.current, {
|
|
4074
|
+
childList: true,
|
|
4075
|
+
subtree: true
|
|
4076
|
+
});
|
|
4077
|
+
blockObserverRef.current = observer;
|
|
4078
|
+
return () => {
|
|
4079
|
+
observer.disconnect();
|
|
4080
|
+
blockObserverRef.current = null;
|
|
4081
|
+
};
|
|
4082
|
+
}, [collabEnabled, yTextMap, bindAllBlocksToYText]);
|
|
4083
|
+
React.useEffect(() => {
|
|
4084
|
+
return () => {
|
|
4085
|
+
yTextBindingsRef.current.forEach((binding, blockId) => {
|
|
4086
|
+
binding.ytext.unobserve(binding.ytextObserver);
|
|
4087
|
+
binding.element.removeEventListener("input", binding.inputHandler);
|
|
4088
|
+
});
|
|
4089
|
+
yTextBindingsRef.current.clear();
|
|
4090
|
+
};
|
|
4091
|
+
}, []);
|
|
4092
|
+
const {
|
|
4093
|
+
snapshots,
|
|
4094
|
+
loading: versionHistoryLoading,
|
|
4095
|
+
error: versionHistoryError,
|
|
4096
|
+
fetchSnapshots,
|
|
4097
|
+
restoreSnapshot,
|
|
4098
|
+
createSnapshot
|
|
4099
|
+
} = useVersionHistory();
|
|
4100
|
+
React.useEffect(() => {
|
|
4101
|
+
if (showVersionHistory && collabRoomId) {
|
|
4102
|
+
fetchSnapshots(collabRoomId);
|
|
4103
|
+
}
|
|
4104
|
+
}, [showVersionHistory, collabRoomId, fetchSnapshots]);
|
|
3437
4105
|
React.useMemo(() => {
|
|
3438
4106
|
switch (collabRole) {
|
|
3439
4107
|
case "viewer":
|
|
@@ -3767,23 +4435,15 @@ const Editor = React.forwardRef(({
|
|
|
3767
4435
|
setWordCount(plainText.split(/\s+/).filter((w) => w.length > 0).length);
|
|
3768
4436
|
}, []);
|
|
3769
4437
|
const renderFromYDoc = React.useCallback(async () => {
|
|
3770
|
-
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(() => {
|
|
3771
|
-
});
|
|
3772
4438
|
if (!collabEnabled || !yBlocksMap || !yDoc) {
|
|
3773
|
-
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(() => {
|
|
3774
|
-
});
|
|
3775
4439
|
return;
|
|
3776
4440
|
}
|
|
3777
4441
|
const editor = editorInstanceRef.current;
|
|
3778
4442
|
if (!editor || !isReadyRef.current) {
|
|
3779
|
-
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(() => {
|
|
3780
|
-
});
|
|
3781
4443
|
pendingRenderRef.current = pendingRenderRef.current || true;
|
|
3782
4444
|
return;
|
|
3783
4445
|
}
|
|
3784
4446
|
if (isApplyingRemoteRef.current) {
|
|
3785
|
-
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(() => {
|
|
3786
|
-
});
|
|
3787
4447
|
return;
|
|
3788
4448
|
}
|
|
3789
4449
|
isApplyingRemoteRef.current = true;
|
|
@@ -3801,29 +4461,35 @@ const Editor = React.forwardRef(({
|
|
|
3801
4461
|
yOrder = Array.from(yBlocksMap.keys());
|
|
3802
4462
|
}
|
|
3803
4463
|
const yBlocks = [];
|
|
3804
|
-
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(() => {
|
|
3805
|
-
});
|
|
3806
4464
|
yOrder.forEach((id) => {
|
|
3807
4465
|
const json = yBlocksMap.get(id);
|
|
3808
4466
|
if (json) {
|
|
3809
4467
|
try {
|
|
3810
|
-
const
|
|
3811
|
-
|
|
4468
|
+
const blockData = JSON.parse(json);
|
|
4469
|
+
if (blockData.type && !blockData.data) {
|
|
4470
|
+
const ytext = yTextMap?.get(id);
|
|
4471
|
+
const textContent = ytext ? collabDeltaToHtml(ytext.toDelta()) : "";
|
|
4472
|
+
yBlocks.push({
|
|
4473
|
+
id,
|
|
4474
|
+
type: blockData.type,
|
|
4475
|
+
data: { text: textContent },
|
|
4476
|
+
tunes: blockData.tunes || {}
|
|
4477
|
+
});
|
|
4478
|
+
} else if (blockData.type && blockData.data) {
|
|
4479
|
+
yBlocks.push({
|
|
4480
|
+
id,
|
|
4481
|
+
...blockData
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
3812
4484
|
} catch (e) {
|
|
3813
4485
|
console.warn("[Magic Editor X] Invalid block JSON:", id);
|
|
3814
4486
|
}
|
|
3815
4487
|
}
|
|
3816
4488
|
});
|
|
3817
|
-
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(() => {
|
|
3818
|
-
});
|
|
3819
4489
|
const parsed = { blocks: yBlocks };
|
|
3820
4490
|
const normalizedParsed = serializeForCompare(parsed);
|
|
3821
4491
|
const renderFull = async () => {
|
|
3822
|
-
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(() => {
|
|
3823
|
-
});
|
|
3824
4492
|
await editor.render(parsed);
|
|
3825
|
-
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(() => {
|
|
3826
|
-
});
|
|
3827
4493
|
lastSerializedValueRef.current = normalizedParsed;
|
|
3828
4494
|
setBlocksCount(yBlocks.length);
|
|
3829
4495
|
calculateStats(parsed);
|
|
@@ -3836,8 +4502,6 @@ const Editor = React.forwardRef(({
|
|
|
3836
4502
|
currentBlocks.push({ id: block.id, index: i });
|
|
3837
4503
|
}
|
|
3838
4504
|
}
|
|
3839
|
-
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(() => {
|
|
3840
|
-
});
|
|
3841
4505
|
if (blockCount !== yBlocks.length) {
|
|
3842
4506
|
console.log("[Magic Editor X] [SYNC] Structural change detected (count mismatch). Falling back to full render.");
|
|
3843
4507
|
await renderFull();
|
|
@@ -4042,7 +4706,7 @@ const Editor = React.forwardRef(({
|
|
|
4042
4706
|
const emptyPayload = JSON.stringify({ blocks: [] });
|
|
4043
4707
|
lastSerializedValueRef.current = emptyPayload;
|
|
4044
4708
|
pushLocalToCollab(emptyPayload);
|
|
4045
|
-
onChange({ target: { name, value: null, type: "
|
|
4709
|
+
onChange({ target: { name, value: null, type: "json" } });
|
|
4046
4710
|
setBlocksCount(0);
|
|
4047
4711
|
setWordCount(0);
|
|
4048
4712
|
setCharCount(0);
|
|
@@ -4055,7 +4719,11 @@ const Editor = React.forwardRef(({
|
|
|
4055
4719
|
}, [isReady]);
|
|
4056
4720
|
React.useEffect(() => {
|
|
4057
4721
|
if (editorRef.current && !editorInstanceRef.current) {
|
|
4058
|
-
const tools$1 = tools.getTools({
|
|
4722
|
+
const tools$1 = tools.getTools({
|
|
4723
|
+
mediaLibToggleFunc,
|
|
4724
|
+
pluginId: index.PLUGIN_ID,
|
|
4725
|
+
openLinkPicker: isWebtoolsAvailable ? webtoolsOpenLinkPicker : null
|
|
4726
|
+
});
|
|
4059
4727
|
let initialData = void 0;
|
|
4060
4728
|
if (value) {
|
|
4061
4729
|
try {
|
|
@@ -4121,6 +4789,11 @@ const Editor = React.forwardRef(({
|
|
|
4121
4789
|
}
|
|
4122
4790
|
pendingRenderRef.current = null;
|
|
4123
4791
|
}
|
|
4792
|
+
if (collabEnabled && yTextMap) {
|
|
4793
|
+
setTimeout(() => {
|
|
4794
|
+
bindAllBlocksToYText();
|
|
4795
|
+
}, 100);
|
|
4796
|
+
}
|
|
4124
4797
|
},
|
|
4125
4798
|
onChange: async (api) => {
|
|
4126
4799
|
try {
|
|
@@ -4140,9 +4813,9 @@ const Editor = React.forwardRef(({
|
|
|
4140
4813
|
pushLocalToCollabRef.current?.(docPayload);
|
|
4141
4814
|
lastSerializedValueRef.current = normalized;
|
|
4142
4815
|
if (count === 0) {
|
|
4143
|
-
onChange({ target: { name, value: null, type: "
|
|
4816
|
+
onChange({ target: { name, value: null, type: "json" } });
|
|
4144
4817
|
} else {
|
|
4145
|
-
onChange({ target: { name, value:
|
|
4818
|
+
onChange({ target: { name, value: outputData, type: "json" } });
|
|
4146
4819
|
}
|
|
4147
4820
|
} catch (error2) {
|
|
4148
4821
|
console.error("[Magic Editor X] Error in onChange:", error2);
|
|
@@ -4265,6 +4938,96 @@ const Editor = React.forwardRef(({
|
|
|
4265
4938
|
children: /* @__PURE__ */ jsxRuntime.jsx(outline.SparklesIcon, {})
|
|
4266
4939
|
}
|
|
4267
4940
|
),
|
|
4941
|
+
isWebtoolsAvailable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4942
|
+
ToolButton,
|
|
4943
|
+
{
|
|
4944
|
+
type: "button",
|
|
4945
|
+
"data-tooltip": "Webtools Link Picker",
|
|
4946
|
+
onClick: async () => {
|
|
4947
|
+
if (!editorInstanceRef.current || !isReady) {
|
|
4948
|
+
console.warn("[Magic Editor X] Editor not ready");
|
|
4949
|
+
return;
|
|
4950
|
+
}
|
|
4951
|
+
const editor = editorInstanceRef.current;
|
|
4952
|
+
const {
|
|
4953
|
+
text: selectedText,
|
|
4954
|
+
range: savedRange,
|
|
4955
|
+
blockIndex,
|
|
4956
|
+
existingAnchor,
|
|
4957
|
+
existingHref
|
|
4958
|
+
} = webtoolsSelectionRef.current;
|
|
4959
|
+
console.log("[Magic Editor X] Webtools button clicked with stored selection:", {
|
|
4960
|
+
text: selectedText || "(none)",
|
|
4961
|
+
existingHref: existingHref || "(new link)",
|
|
4962
|
+
hasRange: !!savedRange,
|
|
4963
|
+
blockIndex
|
|
4964
|
+
});
|
|
4965
|
+
const currentBlockIndex = blockIndex >= 0 ? blockIndex : editor.blocks.getCurrentBlockIndex();
|
|
4966
|
+
const result = await webtoolsOpenLinkPicker({
|
|
4967
|
+
initialText: selectedText || "",
|
|
4968
|
+
initialHref: existingHref || ""
|
|
4969
|
+
});
|
|
4970
|
+
if (result && result.href) {
|
|
4971
|
+
const linkText = result.label || selectedText || result.href;
|
|
4972
|
+
const linkHtml = `<a href="${result.href}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
|
|
4973
|
+
if (existingAnchor && existingAnchor.parentNode) {
|
|
4974
|
+
try {
|
|
4975
|
+
existingAnchor.href = result.href;
|
|
4976
|
+
existingAnchor.textContent = linkText;
|
|
4977
|
+
const contentEditable = existingAnchor.closest('[contenteditable="true"]');
|
|
4978
|
+
if (contentEditable) {
|
|
4979
|
+
contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4980
|
+
}
|
|
4981
|
+
console.log("[Magic Editor X] Webtools link UPDATED:", {
|
|
4982
|
+
oldHref: existingHref,
|
|
4983
|
+
newHref: result.href,
|
|
4984
|
+
text: linkText
|
|
4985
|
+
});
|
|
4986
|
+
} catch (e) {
|
|
4987
|
+
console.error("[Magic Editor X] Failed to update link:", e);
|
|
4988
|
+
}
|
|
4989
|
+
} else if (savedRange && selectedText && currentBlockIndex >= 0) {
|
|
4990
|
+
try {
|
|
4991
|
+
const blockHolder = editor.blocks.getBlockByIndex(currentBlockIndex)?.holder;
|
|
4992
|
+
const contentEditable = blockHolder?.querySelector('[contenteditable="true"]');
|
|
4993
|
+
if (contentEditable) {
|
|
4994
|
+
const selection = window.getSelection();
|
|
4995
|
+
selection.removeAllRanges();
|
|
4996
|
+
selection.addRange(savedRange);
|
|
4997
|
+
document.execCommand("insertHTML", false, linkHtml);
|
|
4998
|
+
contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4999
|
+
console.log("[Magic Editor X] Webtools link CREATED:", {
|
|
5000
|
+
text: linkText,
|
|
5001
|
+
href: result.href
|
|
5002
|
+
});
|
|
5003
|
+
} else {
|
|
5004
|
+
editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
|
|
5005
|
+
}
|
|
5006
|
+
} catch (e) {
|
|
5007
|
+
console.error("[Magic Editor X] Failed to insert link:", e);
|
|
5008
|
+
editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
|
|
5009
|
+
}
|
|
5010
|
+
} else if (currentBlockIndex >= 0) {
|
|
5011
|
+
editor.blocks.insert("paragraph", {
|
|
5012
|
+
text: linkHtml
|
|
5013
|
+
}, {}, currentBlockIndex + 1, true);
|
|
5014
|
+
editor.caret.setToBlock(currentBlockIndex + 1);
|
|
5015
|
+
console.log("[Magic Editor X] Webtools link inserted (no selection):", result);
|
|
5016
|
+
} else {
|
|
5017
|
+
editor.blocks.insert("paragraph", { text: linkHtml });
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
webtoolsSelectionRef.current = { text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" };
|
|
5021
|
+
},
|
|
5022
|
+
disabled: collabEnabled && collabCanEdit === false,
|
|
5023
|
+
style: {
|
|
5024
|
+
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
5025
|
+
color: "white",
|
|
5026
|
+
...collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {}
|
|
5027
|
+
},
|
|
5028
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(outline.LinkIcon, {})
|
|
5029
|
+
}
|
|
5030
|
+
),
|
|
4268
5031
|
/* @__PURE__ */ jsxRuntime.jsx(ToolbarDivider, {}),
|
|
4269
5032
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4270
5033
|
ToolButton,
|
|
@@ -4380,9 +5143,26 @@ const Editor = React.forwardRef(({
|
|
|
4380
5143
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: charCount }),
|
|
4381
5144
|
" ",
|
|
4382
5145
|
t("editor.characters", "Zeichen")
|
|
4383
|
-
] })
|
|
5146
|
+
] }),
|
|
5147
|
+
!isWebtoolsAvailable && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5148
|
+
WebtoolsPromoLink,
|
|
5149
|
+
{
|
|
5150
|
+
href: "https://www.pluginpal.io/plugin/webtools",
|
|
5151
|
+
target: "_blank",
|
|
5152
|
+
rel: "noopener noreferrer",
|
|
5153
|
+
title: "Get Webtools Links addon for internal link management",
|
|
5154
|
+
children: [
|
|
5155
|
+
/* @__PURE__ */ jsxRuntime.jsx(outline.LinkIcon, {}),
|
|
5156
|
+
"Internal Links? Get Webtools"
|
|
5157
|
+
]
|
|
5158
|
+
}
|
|
5159
|
+
)
|
|
4384
5160
|
] }),
|
|
4385
5161
|
/* @__PURE__ */ jsxRuntime.jsxs(FooterRight, { children: [
|
|
5162
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FooterButton, { type: "button", onClick: () => setShowVersionHistory(true), children: [
|
|
5163
|
+
/* @__PURE__ */ jsxRuntime.jsx(outline.ClockIcon, {}),
|
|
5164
|
+
t("editor.versionHistory", "History")
|
|
5165
|
+
] }),
|
|
4386
5166
|
!(collabEnabled && collabCanEdit === false) && /* @__PURE__ */ jsxRuntime.jsxs(FooterButton, { type: "button", onClick: () => handleInsertBlock("mediaLib"), children: [
|
|
4387
5167
|
/* @__PURE__ */ jsxRuntime.jsx(outline.PhotoIcon, {}),
|
|
4388
5168
|
t("editor.mediaLibrary", "Media Library")
|
|
@@ -4455,6 +5235,51 @@ const Editor = React.forwardRef(({
|
|
|
4455
5235
|
}
|
|
4456
5236
|
}
|
|
4457
5237
|
),
|
|
5238
|
+
showVersionHistory && /* @__PURE__ */ jsxRuntime.jsx(VersionHistoryOverlay, { onClick: () => setShowVersionHistory(false), children: /* @__PURE__ */ jsxRuntime.jsx("div", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5239
|
+
VersionHistoryPanel,
|
|
5240
|
+
{
|
|
5241
|
+
snapshots,
|
|
5242
|
+
loading: versionHistoryLoading,
|
|
5243
|
+
error: versionHistoryError,
|
|
5244
|
+
tier: licenseTier,
|
|
5245
|
+
onClose: () => setShowVersionHistory(false),
|
|
5246
|
+
onRestore: async (snapshot) => {
|
|
5247
|
+
if (snapshot.documentId && editorInstanceRef.current && isReady) {
|
|
5248
|
+
try {
|
|
5249
|
+
const result = await restoreSnapshot(snapshot.documentId, collabRoomId);
|
|
5250
|
+
const contentToRestore = result?.jsonContent || snapshot.jsonContent;
|
|
5251
|
+
if (contentToRestore && editorInstanceRef.current) {
|
|
5252
|
+
await editorInstanceRef.current.render(contentToRestore);
|
|
5253
|
+
setShowVersionHistory(false);
|
|
5254
|
+
onChange({ target: { name, value: contentToRestore, type: "json" } });
|
|
5255
|
+
}
|
|
5256
|
+
} catch (err) {
|
|
5257
|
+
console.error("[Magic Editor X] Failed to restore snapshot:", err?.message);
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
},
|
|
5261
|
+
onCreate: async () => {
|
|
5262
|
+
if (collabRoomId && editorInstanceRef.current && isReady) {
|
|
5263
|
+
const [contentType, entryId, fieldName] = collabRoomId.split("|");
|
|
5264
|
+
if (contentType && entryId && fieldName) {
|
|
5265
|
+
try {
|
|
5266
|
+
const editorContent = await editorInstanceRef.current.save();
|
|
5267
|
+
await createSnapshot({
|
|
5268
|
+
roomId: collabRoomId,
|
|
5269
|
+
contentType,
|
|
5270
|
+
entryId,
|
|
5271
|
+
fieldName,
|
|
5272
|
+
content: editorContent
|
|
5273
|
+
});
|
|
5274
|
+
fetchSnapshots(collabRoomId);
|
|
5275
|
+
} catch (err) {
|
|
5276
|
+
console.error("[Magic Editor X] Failed to create snapshot:", err?.message);
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
) }) }),
|
|
4458
5283
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4459
5284
|
CreditsModal,
|
|
4460
5285
|
{
|