@avocadostudio-ai/preview-adapter 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge-functions.d.ts +22 -1
- package/dist/bridge-functions.js +84 -13
- package/package.json +2 -2
|
@@ -417,7 +417,13 @@ export type BridgeState = {
|
|
|
417
417
|
serverVersion: number;
|
|
418
418
|
liveDraftActiveBlockId: string | null;
|
|
419
419
|
liveDraftBadgeTimer: number | null;
|
|
420
|
-
liveDraftOriginals: Map<HTMLElement,
|
|
420
|
+
liveDraftOriginals: Map<HTMLElement, LiveDraftOriginal>;
|
|
421
|
+
/**
|
|
422
|
+
* React's own child nodes for fields whose streamed content was committed
|
|
423
|
+
* while foreign nodes were still on screen. Put back immediately before the
|
|
424
|
+
* next refresh, so React reconciles nodes it owns. See `LiveDraftOriginal`.
|
|
425
|
+
*/
|
|
426
|
+
liveDraftDetached: Map<HTMLElement, ChildNode[]>;
|
|
421
427
|
knownBlockIds: Set<string>;
|
|
422
428
|
expectingNewBlocks: boolean;
|
|
423
429
|
hoveredItemRoot: HTMLElement | null;
|
|
@@ -437,6 +443,21 @@ export type BridgeState = {
|
|
|
437
443
|
fixedBlockTypes: Set<string>;
|
|
438
444
|
};
|
|
439
445
|
export declare function createBridgeState(): BridgeState;
|
|
446
|
+
export type LiveDraftOriginal = {
|
|
447
|
+
mode: "text";
|
|
448
|
+
textNode: Text;
|
|
449
|
+
value: string;
|
|
450
|
+
} | {
|
|
451
|
+
mode: "replaced";
|
|
452
|
+
nodes: ChildNode[];
|
|
453
|
+
};
|
|
454
|
+
/** The text an HTML string renders, when it is only text (no elements). */
|
|
455
|
+
export declare function plainTextOfHtml(html: string): string | null;
|
|
456
|
+
/**
|
|
457
|
+
* Set an element's text without replacing React's text node when it has
|
|
458
|
+
* exactly one; `textContent =` would. Falls back to `textContent` otherwise.
|
|
459
|
+
*/
|
|
460
|
+
export declare function setTextKeepingNode(node: HTMLElement, text: string): void;
|
|
440
461
|
export type BridgeFunctions = ReturnType<typeof createBridgeFunctions>;
|
|
441
462
|
/**
|
|
442
463
|
* Creates all stateful DOM manipulation functions bound to the given state and callbacks.
|
package/dist/bridge-functions.js
CHANGED
|
@@ -905,6 +905,7 @@ export function createBridgeState() {
|
|
|
905
905
|
liveDraftActiveBlockId: null,
|
|
906
906
|
liveDraftBadgeTimer: null,
|
|
907
907
|
liveDraftOriginals: new Map(),
|
|
908
|
+
liveDraftDetached: new Map(),
|
|
908
909
|
knownBlockIds: new Set(Array.from(document.querySelectorAll("[data-block-id]"))
|
|
909
910
|
.map((node) => node.getAttribute("data-block-id"))
|
|
910
911
|
.filter((id) => Boolean(id))),
|
|
@@ -916,6 +917,27 @@ export function createBridgeState() {
|
|
|
916
917
|
fixedBlockTypes: new Set(),
|
|
917
918
|
};
|
|
918
919
|
}
|
|
920
|
+
/** The text an HTML string renders, when it is only text (no elements). */
|
|
921
|
+
export function plainTextOfHtml(html) {
|
|
922
|
+
if (/<[a-zA-Z!/]/.test(html))
|
|
923
|
+
return null;
|
|
924
|
+
const scratch = document.createElement("textarea");
|
|
925
|
+
scratch.innerHTML = html;
|
|
926
|
+
return scratch.value;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Set an element's text without replacing React's text node when it has
|
|
930
|
+
* exactly one; `textContent =` would. Falls back to `textContent` otherwise.
|
|
931
|
+
*/
|
|
932
|
+
export function setTextKeepingNode(node, text) {
|
|
933
|
+
const only = node.childNodes.length === 1 ? node.firstChild : null;
|
|
934
|
+
if (only && only.nodeType === Node.TEXT_NODE) {
|
|
935
|
+
if (only.nodeValue !== text)
|
|
936
|
+
only.nodeValue = text;
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
node.textContent = text;
|
|
940
|
+
}
|
|
919
941
|
/**
|
|
920
942
|
* Creates all stateful DOM manipulation functions bound to the given state and callbacks.
|
|
921
943
|
* Used by both PreviewBridgeCore (iframe/postMessage) and the immersive widget (direct).
|
|
@@ -955,31 +977,56 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
955
977
|
badge.append(status);
|
|
956
978
|
}, 600);
|
|
957
979
|
};
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
980
|
+
/** Put React's content back exactly as it was: its own nodes, its own text. */
|
|
981
|
+
const putBackOriginal = (node, original) => {
|
|
982
|
+
if (original.mode === "text") {
|
|
983
|
+
original.textNode.nodeValue = original.value;
|
|
984
|
+
if (original.textNode.parentNode !== node)
|
|
985
|
+
node.replaceChildren(original.textNode);
|
|
962
986
|
}
|
|
987
|
+
else {
|
|
988
|
+
node.replaceChildren(...original.nodes);
|
|
989
|
+
}
|
|
990
|
+
node.classList.remove("editor-live-typing");
|
|
991
|
+
};
|
|
992
|
+
const restoreLiveDraftOriginals = () => {
|
|
993
|
+
for (const [node, original] of state.liveDraftOriginals)
|
|
994
|
+
putBackOriginal(node, original);
|
|
963
995
|
state.liveDraftOriginals.clear();
|
|
964
996
|
};
|
|
965
|
-
/**
|
|
997
|
+
/**
|
|
998
|
+
* The streamed content was committed: keep it on screen. A field edited in
|
|
999
|
+
* place needs nothing more — React's node holds the committed text and the
|
|
1000
|
+
* refresh reconciles it. A replaced field's React nodes wait in
|
|
1001
|
+
* `liveDraftDetached` and go back in just before that refresh.
|
|
1002
|
+
*/
|
|
966
1003
|
const discardLiveDraftOriginals = () => {
|
|
967
|
-
for (const [node] of state.liveDraftOriginals) {
|
|
1004
|
+
for (const [node, original] of state.liveDraftOriginals) {
|
|
968
1005
|
node.classList.remove("editor-live-typing");
|
|
1006
|
+
if (original.mode === "replaced" && !state.liveDraftDetached.has(node)) {
|
|
1007
|
+
state.liveDraftDetached.set(node, original.nodes);
|
|
1008
|
+
}
|
|
969
1009
|
}
|
|
970
1010
|
state.liveDraftOriginals.clear();
|
|
971
1011
|
state.liveDraftActiveBlockId = null;
|
|
972
1012
|
};
|
|
1013
|
+
/** Hand every committed field's nodes back to React before it re-renders. */
|
|
1014
|
+
const reattachLiveDraftNodes = () => {
|
|
1015
|
+
for (const [node, nodes] of state.liveDraftDetached) {
|
|
1016
|
+
if (node.isConnected)
|
|
1017
|
+
node.replaceChildren(...nodes);
|
|
1018
|
+
}
|
|
1019
|
+
state.liveDraftDetached.clear();
|
|
1020
|
+
};
|
|
973
1021
|
// Write each streamed field value into its editable node (plain-text fields
|
|
974
1022
|
// only; rich fields stay under React's control — see isRichEditableNode).
|
|
975
1023
|
// Used by both the active streaming path and the final commit post.
|
|
976
1024
|
const applyLiveDraftFields = (block, fields) => {
|
|
977
1025
|
const nextPaths = new Set(Object.keys(fields));
|
|
978
|
-
for (const [node,
|
|
1026
|
+
for (const [node, original] of [...state.liveDraftOriginals.entries()]) {
|
|
979
1027
|
const path = editablePathOf(node);
|
|
980
1028
|
if (!nextPaths.has(path)) {
|
|
981
|
-
node
|
|
982
|
-
node.classList.remove("editor-live-typing");
|
|
1029
|
+
putBackOriginal(node, original);
|
|
983
1030
|
state.liveDraftOriginals.delete(node);
|
|
984
1031
|
}
|
|
985
1032
|
}
|
|
@@ -998,10 +1045,32 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
998
1045
|
node.classList.add("editor-live-typing");
|
|
999
1046
|
continue;
|
|
1000
1047
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1048
|
+
const html = markdownToHtml(value);
|
|
1049
|
+
let original = state.liveDraftOriginals.get(node);
|
|
1050
|
+
if (!original) {
|
|
1051
|
+
const only = node.childNodes.length === 1 ? node.firstChild : null;
|
|
1052
|
+
original = only && only.nodeType === Node.TEXT_NODE
|
|
1053
|
+
? { mode: "text", textNode: only, value: only.nodeValue ?? "" }
|
|
1054
|
+
: { mode: "replaced", nodes: Array.from(node.childNodes) };
|
|
1055
|
+
state.liveDraftOriginals.set(node, original);
|
|
1056
|
+
}
|
|
1057
|
+
const text = original.mode === "text" ? plainTextOfHtml(html) : null;
|
|
1058
|
+
if (original.mode === "text" && text !== null) {
|
|
1059
|
+
// React's own node, new text: nothing for React to lose track of.
|
|
1060
|
+
if (original.textNode.parentNode !== node)
|
|
1061
|
+
node.replaceChildren(original.textNode);
|
|
1062
|
+
original.textNode.nodeValue = text;
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
if (original.mode === "text") {
|
|
1066
|
+
// The stream gained markup (bold, a link): from here it replaces, and
|
|
1067
|
+
// React's text node is what goes back.
|
|
1068
|
+
original.textNode.nodeValue = original.value;
|
|
1069
|
+
original = { mode: "replaced", nodes: [original.textNode] };
|
|
1070
|
+
state.liveDraftOriginals.set(node, original);
|
|
1071
|
+
}
|
|
1072
|
+
node.innerHTML = html;
|
|
1003
1073
|
}
|
|
1004
|
-
node.innerHTML = markdownToHtml(value);
|
|
1005
1074
|
node.classList.add("editor-live-typing");
|
|
1006
1075
|
}
|
|
1007
1076
|
};
|
|
@@ -1163,7 +1232,8 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1163
1232
|
const s = state.inlineEditing;
|
|
1164
1233
|
if (!s)
|
|
1165
1234
|
return;
|
|
1166
|
-
s
|
|
1235
|
+
// Not `textContent =`: that replaces React's text node (see LiveDraftOriginal).
|
|
1236
|
+
setTextKeepingNode(s.node, s.initialValue);
|
|
1167
1237
|
s.node.setAttribute("contenteditable", "false");
|
|
1168
1238
|
s.node.classList.remove("editor-inline-editing");
|
|
1169
1239
|
s.node.append(...s.liftedControls);
|
|
@@ -1706,6 +1776,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1706
1776
|
state.pendingScrollRestore = null;
|
|
1707
1777
|
}
|
|
1708
1778
|
cancelInlineEdit();
|
|
1779
|
+
reattachLiveDraftNodes();
|
|
1709
1780
|
const restoreAndFocus = () => {
|
|
1710
1781
|
if (state.pendingScrollRestore) {
|
|
1711
1782
|
const { x, y } = state.pendingScrollRestore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/preview-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"NOTICE"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@avocadostudio-ai/shared": "^0.
|
|
42
|
+
"@avocadostudio-ai/shared": "^0.30.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"next": ">=15.0.0",
|