@panphora/clayjs 1.25.1 → 1.26.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/all.js +1 -1
- package/dist/clay-dom.js +1 -1
- package/dist/clay-events.js +1 -1
- package/dist/clay-internals.js +1 -1
- package/dist/clay-options.js +1 -1
- package/dist/clay-router.js +1 -1
- package/dist/clay-ui.js +1 -1
- package/dist/clay-utils.js +1 -1
- package/dist/clay.js +1 -1
- package/dist/clay.standalone.js +1141 -345
- package/dist/m/c-2EPHRF47.js +1 -0
- package/dist/m/{c-2GB34XL4.js → c-2SUFMUZX.js} +2 -2
- package/dist/m/{c-BHLYQGTA.js → c-3S3647J6.js} +1 -1
- package/dist/m/{c-QL4NYM2L.js → c-3S5TNRZA.js} +1 -1
- package/dist/m/{c-3Y234AUB.js → c-3XGLJAS7.js} +2 -2
- package/dist/m/{c-O4YIDULY.js → c-42WBOKFM.js} +1 -1
- package/dist/m/{c-42RLKGEY.js → c-4TP3UXBT.js} +1 -1
- package/dist/m/{c-ASGHHLFX.js → c-5ELELG2F.js} +1 -1
- package/dist/m/{c-T5IWJOMW.js → c-5ZYLTN3Q.js} +1 -1
- package/dist/m/{c-DDCYVLGG.js → c-67TBU56I.js} +1 -1
- package/dist/m/{c-WNOLMHI2.js → c-6BGJAJ67.js} +1 -1
- package/dist/m/{c-XS7G6U3Z.js → c-ABOBD7F3.js} +1 -1
- package/dist/m/{c-5IIEOPIV.js → c-ARO6CPMA.js} +1 -1
- package/dist/m/{c-XUZMRQ7O.js → c-BCS3AZQZ.js} +1 -1
- package/dist/m/c-BOS7GONO.js +1 -0
- package/dist/m/{c-NICICT4P.js → c-DEYHCYJB.js} +1 -1
- package/dist/m/{c-T7LOTEWO.js → c-DZT6W2NG.js} +1 -1
- package/dist/m/c-EHJWIGWN.js +1 -0
- package/dist/m/{c-V2V5FKXX.js → c-EIUKME3W.js} +1 -1
- package/dist/m/{c-ASMLERFA.js → c-EXZOA6XY.js} +1 -1
- package/dist/m/{c-STAPQ2X6.js → c-EYFMAR3M.js} +1 -1
- package/dist/m/{c-DVMDYRAE.js → c-FUN6AKFE.js} +1 -1
- package/dist/m/{c-XMIDHEKF.js → c-GY36NO7C.js} +1 -1
- package/dist/m/{c-TWB5ZQU4.js → c-H47C6PJZ.js} +1 -1
- package/dist/m/c-KD2ME5UN.js +2 -0
- package/dist/m/{c-GO635CRU.js → c-LAKBBJOU.js} +2 -2
- package/dist/m/{c-GNFDX6SU.js → c-LETPPG2F.js} +1 -1
- package/dist/m/{c-LS7IVQVW.js → c-MMV5JULA.js} +1 -1
- package/dist/m/{c-NEQRFC4O.js → c-MVVEIJUU.js} +1 -1
- package/dist/m/{c-XCOQNISP.js → c-N362HNGO.js} +1 -1
- package/dist/m/{c-7K4SCQB7.js → c-ND3W7D5U.js} +1 -1
- package/dist/m/{c-4NAYDOWH.js → c-NNHNXD6M.js} +1 -1
- package/dist/m/{c-DAUQ6JZ3.js → c-NYJ577NL.js} +1 -1
- package/dist/m/{c-3AAQJAKJ.js → c-PWILUN4T.js} +1 -1
- package/dist/m/{c-7Z2625LX.js → c-QA3Q3MDI.js} +1 -1
- package/dist/m/{c-2A2ATCIH.js → c-QKOKMQXA.js} +1 -1
- package/dist/m/{c-TGIMBAWE.js → c-RBWVF4N5.js} +1 -1
- package/dist/m/{c-A5IYL7JI.js → c-SPOEDIMP.js} +1 -1
- package/dist/m/{c-ZJ7726FO.js → c-SRW5OBCF.js} +1 -1
- package/dist/m/{c-A3OKSYRI.js → c-SYOA4T7V.js} +1 -1
- package/dist/m/{c-MHTO3QAD.js → c-TJ5PFOJ2.js} +1 -1
- package/dist/m/{c-NW4ZFKCY.js → c-TKV273YU.js} +1 -1
- package/dist/m/{c-D4N4UJCQ.js → c-UEKJWXCB.js} +2 -2
- package/dist/m/{c-44WITKWQ.js → c-V4DBV5ZV.js} +1 -1
- package/dist/m/c-WHMZCRQD.js +6 -0
- package/dist/m/{c-WMRJKQ2V.js → c-WHQUZM2V.js} +1 -1
- package/dist/m/{c-C27EVUQ6.js → c-XHD7XJTE.js} +1 -1
- package/dist/m/{c-PP3CUF3U.js → c-XONFK3WC.js} +1 -1
- package/dist/m/c-YYIN4NAF.js +30 -0
- package/dist/m/c-ZFAPQQIB.js +1 -0
- package/dist/m/core/{conflict-notice-Y2NTMDGJ.js → conflict-notice-UNDOD4BL.js} +1 -1
- package/dist/m/core/site-edits-C22O53GS.js +1 -0
- package/dist/m/loader-DCKCZKY6.js +1 -0
- package/dist/m/{manifest-A27A4278.js → manifest-7954CBE3.js} +1 -1
- package/dist/m/{manifest-edit-E8A61EB4.js → manifest-edit-93AA79D7.js} +1 -1
- package/dist/m/manifest.json +442 -441
- package/dist/m/page/{editor-HS2CAV3W.js → editor-UF3CIA6V.js} +2 -2
- package/dist/m/plugins/{cms-4KET6RZT.js → cms-KG6TCTR3.js} +1 -1
- package/dist/m/plugins/{live-CO4VJS46.js → live-DF5YS5AH.js} +1 -1
- package/dist/m/plugins/page-GZSU77JS.js +2 -0
- package/dist/m/plugins/{page-view-6KNTXABH.js → page-view-7MM3FEAR.js} +1 -1
- package/dist/m/plugins/{quickcrop-VNRXGRPC.js → quickcrop-UHURKA2F.js} +2 -2
- package/dist/m/plugins/richclay-MXH2GFAZ.js +549 -0
- package/dist/m/plugins/undo-KLHAGV75.js +1 -0
- package/dist/m/sync/{live-sync-DX4K5GDB.js → live-sync-RZQSDMZW.js} +1 -1
- package/dist/offline-manifest.json +360 -360
- package/package.json +1 -1
- package/src/core/site-edits.js +1 -1
- package/src/loader-logic.js +0 -13
- package/src/page/boot-pack.js +1 -0
- package/src/page/history-intent.js +111 -4
- package/src/page/ybind/bridge.js +14 -7
- package/src/page/ybind/moves.js +437 -3
- package/src/page/ybind/origins.js +1 -1
- package/src/page/ybind/recovery.js +1 -1
- package/src/plugins/page.js +48 -4
- package/src/plugins/richclay.js +136 -0
- package/src/plugins/undo.js +169 -7
- package/src/recovery/undo.js +14 -7
- package/src/sync/live-sync.js +24 -0
- package/src/ui/vendor-skin.js +9 -4
- package/dist/m/c-3XNTQQEJ.js +0 -1
- package/dist/m/c-4OZZ7ENT.js +0 -30
- package/dist/m/c-D2OPWOWO.js +0 -2
- package/dist/m/c-LUBZU4AZ.js +0 -6
- package/dist/m/c-LYNLY3MR.js +0 -1
- package/dist/m/c-MLMJUNRV.js +0 -1
- package/dist/m/c-V5L6HWVL.js +0 -1
- package/dist/m/core/site-edits-TIJDPG6H.js +0 -1
- package/dist/m/loader-7ORQVRCK.js +0 -1
- package/dist/m/plugins/page-L3VKIQU2.js +0 -2
- package/dist/m/plugins/richclay-V6WKJTUF.js +0 -549
- package/dist/m/plugins/undo-WUK2NLBA.js +0 -1
- /package/dist/m/plugins/{quickcrop-VNRXGRPC.js.LEGAL.txt → quickcrop-UHURKA2F.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-V6WKJTUF.js.LEGAL.txt → richclay-MXH2GFAZ.js.LEGAL.txt} +0 -0
package/src/plugins/richclay.js
CHANGED
|
@@ -5,9 +5,145 @@ import { installSkin } from "../ui/vendor-skin.js";
|
|
|
5
5
|
import { CSS, ROOTS } from "../ui/skins/richclay.js";
|
|
6
6
|
import { RichClay } from "../vendor/richclay.vendor.js";
|
|
7
7
|
import { startsAtImport } from "../core/is-edit-mode.js";
|
|
8
|
+
import { stripEphemeral } from "../lib/ephemeral.js";
|
|
9
|
+
import { loadedEngine, mergeEngine } from "../sync/merge-engine.js";
|
|
8
10
|
|
|
9
11
|
installSkin("richclay", CSS, { roots: ROOTS });
|
|
10
12
|
|
|
13
|
+
// A field the author kept out of page undo: its own literal no-undo (not the one RichClay
|
|
14
|
+
// adds while it runs), a clay="no-undo" token on it, or an ancestor region with no-undo.
|
|
15
|
+
const authoredNoUndo = (node) => {
|
|
16
|
+
if (node.hasAttribute("no-undo") && !node.hasAttribute("data-richclay-runtime-no-undo")) return true;
|
|
17
|
+
if ((node.getAttribute("clay") || "").split(/\s+/).includes("no-undo")) return true;
|
|
18
|
+
const parent = node.parentElement;
|
|
19
|
+
return !!parent && window.clay?.region?.resolveRegionPolicy?.(parent)?.undoable === false;
|
|
20
|
+
};
|
|
21
|
+
// The editor tidies a field's source whitespace the first time it is typed in. Whitespace
|
|
22
|
+
// that sits beside a block, or at the start or end of one, shows nothing, so it never
|
|
23
|
+
// counts as a change: a person's first letter is one step, and the tidy is not a step of
|
|
24
|
+
// its own or part of another field's.
|
|
25
|
+
const BLOCK = /^(P|DIV|UL|OL|LI|H[1-6]|BLOCKQUOTE|TABLE|THEAD|TBODY|TR|TD|TH|FIGURE|FIGCAPTION|HR|SECTION|ARTICLE|ASIDE|HEADER|FOOTER|DL|DT|DD)$/;
|
|
26
|
+
const isBlock = (node) => node?.nodeType === 1 && BLOCK.test(node.nodeName);
|
|
27
|
+
const dropLayoutWhitespace = (root) => {
|
|
28
|
+
const walker = root.ownerDocument.createTreeWalker(root, 4);
|
|
29
|
+
const blank = [];
|
|
30
|
+
for (let text = walker.nextNode(); text; text = walker.nextNode()) {
|
|
31
|
+
if (/\S/.test(text.data) || text.parentElement?.closest("pre")) continue;
|
|
32
|
+
const parent = text.parentNode;
|
|
33
|
+
const edge = !text.previousSibling || !text.nextSibling;
|
|
34
|
+
if ((edge && (parent === root || isBlock(parent))) || isBlock(text.previousSibling) || isBlock(text.nextSibling)) blank.push(text);
|
|
35
|
+
}
|
|
36
|
+
blank.forEach((text) => text.remove());
|
|
37
|
+
};
|
|
38
|
+
// Ephemeral attributes (selection marks and the like) never save, so they come off a
|
|
39
|
+
// copy before the comparison, as they do for every other node undo certifies.
|
|
40
|
+
const savedForm = (node) => {
|
|
41
|
+
const copy = node.cloneNode(true);
|
|
42
|
+
stripEphemeral(copy);
|
|
43
|
+
dropLayoutWhitespace(copy);
|
|
44
|
+
return RichClay.savedContent(copy);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Parts of a field that history does not own (editor chrome, a widget the author marked
|
|
48
|
+
// no-undo) never count as a change: the sample holds a placeholder for each, and a replay
|
|
49
|
+
// puts back what each one holds now.
|
|
50
|
+
const UNOWNED = '[editor-ui], [clay~="editor-ui"], [no-undo]:not([data-richclay-runtime-no-undo]), [clay~="no-undo"]';
|
|
51
|
+
const outermost = (root) => [...root.querySelectorAll(UNOWNED)].filter((el) => {
|
|
52
|
+
for (let at = el.parentElement; at && at !== root; at = at.parentElement) if (at.matches(UNOWNED)) return false;
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
const historyForm = (node) => {
|
|
56
|
+
const copy = node.cloneNode(true);
|
|
57
|
+
outermost(copy).forEach((el) => el.replaceWith(copy.ownerDocument.createElement("clay-kept")));
|
|
58
|
+
return savedForm(copy);
|
|
59
|
+
};
|
|
60
|
+
const withUnowned = (element, html) => {
|
|
61
|
+
if (!html.includes("<clay-kept")) return html;
|
|
62
|
+
const template = element.ownerDocument.createElement("template");
|
|
63
|
+
template.innerHTML = html;
|
|
64
|
+
const live = outermost(element);
|
|
65
|
+
[...template.content.querySelectorAll("clay-kept")].forEach((slot, at) => {
|
|
66
|
+
if (live[at]) slot.replaceWith(live[at].cloneNode(true));
|
|
67
|
+
else slot.remove();
|
|
68
|
+
});
|
|
69
|
+
return template.innerHTML;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Undo certifies a rich field by the content it would save, so editor runtime state
|
|
73
|
+
// never refuses a step and a real text change always does. A field the author marked
|
|
74
|
+
// no-undo keeps its protection.
|
|
75
|
+
const richForm = (node) => (!node.matches?.(RichClay.selector) || authoredNoUndo(node) ? undefined : savedForm(node));
|
|
76
|
+
|
|
77
|
+
// The field's own typing is a step of the page history: the engine reads the content the
|
|
78
|
+
// field would save and, on a replay, hands it back through the host write. The editor's own
|
|
79
|
+
// history is off while the page owns it, so a field's Cmd+Z and its toolbar reach the same
|
|
80
|
+
// stack as everything else.
|
|
81
|
+
// The engine rebaselines the field right after the write, so the write must land before
|
|
82
|
+
// it returns. A loaded merge engine morphs at once. The first undo asks for it, so a boot
|
|
83
|
+
// pays nothing; until it lands a plain innerHTML write is exact for saved content (checked
|
|
84
|
+
// in Chrome), except in a field holding editor chrome, which only the morph keeps.
|
|
85
|
+
const writeInner = (root, html) => { root.innerHTML = html; };
|
|
86
|
+
const morphNow = (field) => {
|
|
87
|
+
const engine = loadedEngine();
|
|
88
|
+
if (engine) return engine.morph;
|
|
89
|
+
mergeEngine().catch(() => {});
|
|
90
|
+
return field.querySelector('[editor-ui], [clay~="editor-ui"]') ? undefined : writeInner;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Rich fields belong to the page history only when the page records typing: in commands
|
|
94
|
+
// mode the page's history holds commands alone, and a field's own history is left to the
|
|
95
|
+
// editor, which is then the only history that has the typing.
|
|
96
|
+
const pageOwnsTyping = () => window.clay?.undo?.pageHistory === true && window.clay.undo.recordingMode !== "commands";
|
|
97
|
+
|
|
98
|
+
const tracked = new Map();
|
|
99
|
+
|
|
100
|
+
const track = (field) => {
|
|
101
|
+
const undo = window.clay?.undo;
|
|
102
|
+
if (!pageOwnsTyping() || authoredNoUndo(field)) return;
|
|
103
|
+
tracked.set(field, undo.trackContent(field, { read: historyForm, write: (element, html) => { RichClay.setSavedContent(element, withUnowned(element, html), { morph: morphNow(element) }); } }));
|
|
104
|
+
if (!loadedEngine() && field.querySelector('[editor-ui], [clay~="editor-ui"]') && typeof undo.hold === "function") {
|
|
105
|
+
const release = undo.hold();
|
|
106
|
+
mergeEngine().catch(() => {}).finally(release);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const untrackAll = () => {
|
|
111
|
+
for (const dispose of tracked.values()) dispose();
|
|
112
|
+
tracked.clear();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
document.addEventListener("richclay:mount", (event) => track(event.target));
|
|
116
|
+
|
|
117
|
+
// The rich form is handed to the page's undo when its session starts, rather than through
|
|
118
|
+
// a shared module, so a page without undo loads nothing for it. Each undo engine is asked
|
|
119
|
+
// once.
|
|
120
|
+
let formedFor = null;
|
|
121
|
+
const followHistory = () => {
|
|
122
|
+
const undo = window.clay?.undo;
|
|
123
|
+
if (undo?.addDurableForm && formedFor !== undo) {
|
|
124
|
+
undo.addDurableForm(richForm);
|
|
125
|
+
formedFor = undo;
|
|
126
|
+
}
|
|
127
|
+
RichClay.hostHistory = pageOwnsTyping();
|
|
128
|
+
RichClay.keepsOwnHistory = authoredNoUndo;
|
|
129
|
+
if (!RichClay.hostHistory) untrackAll();
|
|
130
|
+
else document.querySelectorAll("[data-richclay-active]").forEach(track);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
document.addEventListener("clay:undo-session", followHistory);
|
|
134
|
+
document.addEventListener("clay:undo-recording", followHistory);
|
|
135
|
+
// A boot starts the undo session while its own module is still being imported, so the
|
|
136
|
+
// session event can land before the loader has put `clay.undo` on the page: once every
|
|
137
|
+
// plugin is attached the same answer is asked for again.
|
|
138
|
+
document.addEventListener("clay:ready", followHistory);
|
|
139
|
+
|
|
140
|
+
document.addEventListener("richclay:history-exhausted", (event) => {
|
|
141
|
+
const undo = window.clay?.undo;
|
|
142
|
+
if (!undo || window.clay.modeLifecycle?.canWrite?.() === false) return;
|
|
143
|
+
if (event.detail?.direction === "redo") undo.redo();
|
|
144
|
+
else undo.undo();
|
|
145
|
+
});
|
|
146
|
+
|
|
11
147
|
let session = null;
|
|
12
148
|
|
|
13
149
|
const fault = (message, code) => Object.assign(new Error(`clay:richclay: ${message}`), { code });
|
package/src/plugins/undo.js
CHANGED
|
@@ -11,6 +11,56 @@ import { PERSIST } from '../lib/attr-aliases.js'
|
|
|
11
11
|
import { isGestureItem, isRuntimeAttribute } from '../lib/undo/capture-move.js'
|
|
12
12
|
import { gateMarkDirty } from '../lib/dirty-gate.js'
|
|
13
13
|
|
|
14
|
+
// Where the caret goes after an undo or redo: at the text that changed.
|
|
15
|
+
//
|
|
16
|
+
// The engine's receipt carries a hint for it: `{ node, offset }` for a text run, and
|
|
17
|
+
// `{ root, textOffset }` for a field whose saved content is its text. The person goes on
|
|
18
|
+
// typing from there, so the field takes focus when they were already typing in a field.
|
|
19
|
+
// When they were not (a button, the dock's Undo on a phone), the field is only scrolled
|
|
20
|
+
// into view: focusing it would open the keyboard. A structural step carries no hint, so
|
|
21
|
+
// an item coming back moves neither the caret nor the focus.
|
|
22
|
+
|
|
23
|
+
const EDITABLE = '[contenteditable]:not([contenteditable="false"])'
|
|
24
|
+
|
|
25
|
+
// The text node an offset into the field's text lands on: count the field's text nodes
|
|
26
|
+
// until the offset is inside one. Past the end is the end of the last text node, and an
|
|
27
|
+
// empty field is the field itself.
|
|
28
|
+
function resolve(root, textOffset) {
|
|
29
|
+
let left = Number(textOffset) || 0
|
|
30
|
+
if (left < 0) left = 0
|
|
31
|
+
const doc = root.ownerDocument || document
|
|
32
|
+
const walker = doc.createTreeWalker(root, 4)
|
|
33
|
+
let last = null
|
|
34
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
35
|
+
if (left <= node.data.length) return { node, offset: left }
|
|
36
|
+
left -= node.data.length
|
|
37
|
+
last = node
|
|
38
|
+
}
|
|
39
|
+
return last ? { node: last, offset: last.data.length } : { node: root, offset: root.childNodes.length }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The field the hint is inside. A field in a hidden section is neither scrolled to nor
|
|
43
|
+
// focused, and a node that is no longer in the page has nowhere to put a caret.
|
|
44
|
+
function hostOf(node) {
|
|
45
|
+
const element = node.nodeType === 1 ? node : node.parentElement
|
|
46
|
+
const host = element?.closest?.(EDITABLE)
|
|
47
|
+
if (!host || !host.isConnected) return null
|
|
48
|
+
if (typeof host.checkVisibility === 'function' && host.checkVisibility() === false) return null
|
|
49
|
+
return host
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function placeCaret(hint) {
|
|
53
|
+
if (!hint) return
|
|
54
|
+
const at = hint.root ? resolve(hint.root, hint.textOffset) : hint
|
|
55
|
+
if (!at?.node) return
|
|
56
|
+
const host = hostOf(at.node)
|
|
57
|
+
if (!host) return
|
|
58
|
+
if (typeof host.scrollIntoView === 'function') host.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
59
|
+
if (!document.activeElement?.closest?.(EDITABLE)) return
|
|
60
|
+
if (document.activeElement !== host) host.focus({ preventScroll: true })
|
|
61
|
+
document.getSelection()?.collapse(at.node, at.offset)
|
|
62
|
+
}
|
|
63
|
+
|
|
14
64
|
// Tab-local attributes (ephemeral state, route-active, root library state) are
|
|
15
65
|
// never undone, and neither is what a drag's vendor writes on the item it holds:
|
|
16
66
|
// `draggable` and `style` are the gesture's own bookkeeping, and a click that
|
|
@@ -20,6 +70,17 @@ import { gateMarkDirty } from '../lib/dirty-gate.js'
|
|
|
20
70
|
// The page's own maintenance attributes, kept outside history by name (a card number a
|
|
21
71
|
// repair rewrites, for example): saved like any attribute, never an edit to undo.
|
|
22
72
|
const pageIgnored = new Set()
|
|
73
|
+
// What a structural undo step compares for a node whose live markup is not its saved
|
|
74
|
+
// markup. An editor that owns a field registers one function (`clay.undo.addDurableForm`):
|
|
75
|
+
// it answers a string for a node it owns and undefined for every other node.
|
|
76
|
+
const durableForms = new Set()
|
|
77
|
+
const durableFormOf = (node, host = null) => {
|
|
78
|
+
for (const form of durableForms) {
|
|
79
|
+
const own = form(node, host)
|
|
80
|
+
if (typeof own === 'string') return own
|
|
81
|
+
}
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
23
84
|
const withTabLocal = (opts = {}) => {
|
|
24
85
|
const own = opts.ignoreAttribute
|
|
25
86
|
return {
|
|
@@ -27,15 +88,18 @@ const withTabLocal = (opts = {}) => {
|
|
|
27
88
|
ignoreAttribute: (name, target, host) => pageIgnored.has(name) || isTabLocalAttr(name, target) || isDerivedAttr(target, name, host) || isRuntimeAttribute(name, target) || (own ? own(name, target, host) : false),
|
|
28
89
|
isProtected: opts.isProtected || keepsOwnHistory,
|
|
29
90
|
ignoreNode: opts.ignoreNode || isGestureItem,
|
|
91
|
+
durableForm: opts.durableForm || durableFormOf,
|
|
30
92
|
}
|
|
31
93
|
}
|
|
32
94
|
|
|
33
|
-
// Saved content that undo does not record (
|
|
34
|
-
//
|
|
35
|
-
//
|
|
95
|
+
// Saved content that undo does not record (an authored no-undo block) has a history
|
|
96
|
+
// undo cannot restore, so a parent's undo or redo must not erase it. A node with a
|
|
97
|
+
// durable form (a mounted rich field) is certified by that form instead, and chrome
|
|
98
|
+
// that is never saved (editor-ui) loses nothing; neither is protected.
|
|
36
99
|
function keepsOwnHistory(node) {
|
|
37
100
|
const policy = window.clay?.region?.resolveRegionPolicy
|
|
38
101
|
if (!policy || node.nodeType !== 1) return false
|
|
102
|
+
if (durableFormOf(node) !== undefined) return false
|
|
39
103
|
const own = policy(node)
|
|
40
104
|
if (own.undoable !== false || own.persist === 'none') return false
|
|
41
105
|
const parent = node.parentElement
|
|
@@ -52,9 +116,12 @@ let recording = null
|
|
|
52
116
|
const setRecording = undo.setRecording.bind(undo)
|
|
53
117
|
undo.setRecording = (mode) => {
|
|
54
118
|
recording = mode ?? null
|
|
119
|
+
document.dispatchEvent(new CustomEvent('clay:undo-recording', { detail: { mode: recording } }))
|
|
55
120
|
return setRecording(mode)
|
|
56
121
|
}
|
|
57
122
|
|
|
123
|
+
Object.defineProperty(undo, 'recordingMode', { get: () => recording, configurable: true })
|
|
124
|
+
|
|
58
125
|
// An authored history action is a user edit, and it writes the DOM while a sync
|
|
59
126
|
// frame's apply may hold both the mutation hub and the gate paused: neither feed
|
|
60
127
|
// records it then, and the apply would take the write into its saved baseline.
|
|
@@ -73,6 +140,17 @@ undo.command = (label, fn) => {
|
|
|
73
140
|
// Undo and redo mark it in afterReplay, which the engine calls on every path that
|
|
74
141
|
// replays a step: the facade and the keyboard shortcut bound to the engine itself.
|
|
75
142
|
|
|
143
|
+
// One entry holding one step that another editor stands behind (the block editor's own
|
|
144
|
+
// Yjs stack item). It goes through the engine's own command, whose flush closes whatever
|
|
145
|
+
// the page was typing, so the order of the two histories is the order of the edits. The
|
|
146
|
+
// editor's step is not a document write of its own, so recording it never marks the page
|
|
147
|
+
// dirty; the editor's own dirty feed is the one that does.
|
|
148
|
+
undo.recordStep = (label, primitive) => command(label, () => undo.record(primitive))
|
|
149
|
+
|
|
150
|
+
// A step that stands for an editor's own undo item holds tokens, not text: its prop names
|
|
151
|
+
// it, and what a person wrote lives in the editor, not in a recovery copy.
|
|
152
|
+
const isBlockStep = (entry) => !!entry?.steps?.length && entry.steps.every((step) => step.kind === 'value' && /^blockeditor-step(?::|$)/.test(step.prop))
|
|
153
|
+
|
|
76
154
|
// A commit inside clay.effect drains the paused hub's records, so the effect's
|
|
77
155
|
// own writes reach autosave and re-trigger it. Warn once; the commit still runs.
|
|
78
156
|
const commit = undo.commit.bind(undo)
|
|
@@ -131,8 +209,9 @@ function restoreControls() {
|
|
|
131
209
|
// undo or redo never reaches the control restore.
|
|
132
210
|
const afterReplay = (receipt) => {
|
|
133
211
|
if (!receipt?.ok) return
|
|
134
|
-
gateMarkDirty()
|
|
212
|
+
if (!isBlockStep(undo.describe(receipt.entryId))) gateMarkDirty()
|
|
135
213
|
restoreControls()
|
|
214
|
+
if (receipt.caret && undo.pageHistory) placeCaret(receipt.caret)
|
|
136
215
|
}
|
|
137
216
|
|
|
138
217
|
// A refusal is announced for whoever presents it: the engine's receipt, once.
|
|
@@ -143,6 +222,17 @@ const refuse = ({ entryId, label, direction, code }) => {
|
|
|
143
222
|
keepRefused({ ok: false, code, entryId, label, direction })
|
|
144
223
|
}
|
|
145
224
|
|
|
225
|
+
// A step the engine can no longer replay is skipped instead of blocking the press: the
|
|
226
|
+
// person asked once and gets the next older step. Its recovery copy is made from the
|
|
227
|
+
// description the engine hands over, because the entry is already out of the history.
|
|
228
|
+
const skip = ({ entryId, label, direction, code, description }) => {
|
|
229
|
+
document.dispatchEvent(new CustomEvent('clay:undo-skipped', {
|
|
230
|
+
detail: { entryId, label, direction, code },
|
|
231
|
+
}))
|
|
232
|
+
if (isBlockStep(description)) return
|
|
233
|
+
keepRefused({ ok: false, code, entryId, label, direction, description })
|
|
234
|
+
}
|
|
235
|
+
|
|
146
236
|
// A refused step is kept as recovery before a reload can take it away: the refusal is
|
|
147
237
|
// announced synchronously above and the copy is made behind it, so a store that will
|
|
148
238
|
// not answer can never hold up the receipt. The module is fetched on the first refusal
|
|
@@ -185,6 +275,11 @@ const withRecovery = (call, fallback) => (...args) =>
|
|
|
185
275
|
// An app's kept copy is announced like a refused step's, so the review notice shows it
|
|
186
276
|
// in this session instead of only after a reload.
|
|
187
277
|
const keepApp = withRecovery('keep', { ok: false, code: 'unavailable' })
|
|
278
|
+
undo.addDurableForm = (form) => {
|
|
279
|
+
durableForms.add(form)
|
|
280
|
+
return () => { durableForms.delete(form) }
|
|
281
|
+
}
|
|
282
|
+
|
|
188
283
|
undo.ignoreAttribute = (name) => {
|
|
189
284
|
const key = String(name || '')
|
|
190
285
|
if (!key) return () => {}
|
|
@@ -235,9 +330,48 @@ undo.reviewActionsFor = (record) => {
|
|
|
235
330
|
// after every entry, replay, refusal, dismissal and clear, and when a session starts or
|
|
236
331
|
// ends. `undo` and `redo` are the entries the next press would replay, with `refused`
|
|
237
332
|
// set on the one that was just refused.
|
|
238
|
-
const HISTORY_EVENTS = ['commit', 'undo', 'redo', 'refused', 'discard', 'clear']
|
|
333
|
+
const HISTORY_EVENTS = ['commit', 'undo', 'redo', 'refused', 'skipped', 'discard', 'clear']
|
|
334
|
+
|
|
335
|
+
// Whatever else owns the moment (a proposal preview, a dialog) holds the page history: while
|
|
336
|
+
// any hold is live the engine refuses a replay, so nothing behind the hold can be undone by a
|
|
337
|
+
// stray Cmd+Z, and the event reports neither direction as available.
|
|
338
|
+
const holds = new Set()
|
|
339
|
+
|
|
340
|
+
// The engine's own answer for each direction: a getter on the engine object itself, so it is
|
|
341
|
+
// read once here and called through, whatever else happens to the property.
|
|
342
|
+
const engineDirection = (name) => {
|
|
343
|
+
for (let owner = undo; owner; owner = Object.getPrototypeOf(owner)) {
|
|
344
|
+
const desc = Object.getOwnPropertyDescriptor(owner, name)
|
|
345
|
+
if (!desc) continue
|
|
346
|
+
if (typeof desc.get === 'function') return () => desc.get.call(undo)
|
|
347
|
+
return () => Boolean(desc.value.call(undo))
|
|
348
|
+
}
|
|
349
|
+
return () => false
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const engineCanUndo = engineDirection('canUndo')
|
|
353
|
+
const engineCanRedo = engineDirection('canRedo')
|
|
354
|
+
|
|
355
|
+
// While a hold is live neither direction is available, so a dock button, the editor's own
|
|
356
|
+
// history and the event all answer the same thing.
|
|
357
|
+
Object.defineProperty(undo, 'canUndo', { get: () => holds.size === 0 && engineCanUndo(), configurable: true })
|
|
358
|
+
Object.defineProperty(undo, 'canRedo', { get: () => holds.size === 0 && engineCanRedo(), configurable: true })
|
|
359
|
+
|
|
360
|
+
undo.hold = () => {
|
|
361
|
+
const token = Symbol('hold')
|
|
362
|
+
holds.add(token)
|
|
363
|
+
announceChange()
|
|
364
|
+
let released = false
|
|
365
|
+
return () => {
|
|
366
|
+
if (released) return
|
|
367
|
+
released = true
|
|
368
|
+
holds.delete(token)
|
|
369
|
+
announceChange()
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
239
373
|
const announceChange = () => {
|
|
240
|
-
const live = !!session?.started
|
|
374
|
+
const live = !!session?.started && holds.size === 0
|
|
241
375
|
document.dispatchEvent(new CustomEvent('clay:undo-change', {
|
|
242
376
|
detail: {
|
|
243
377
|
canUndo: live && undo.canUndo,
|
|
@@ -254,6 +388,22 @@ const announceChange = () => {
|
|
|
254
388
|
// recovery service and outlive every session; nothing here reads, clears or rewrites them.
|
|
255
389
|
let session = null
|
|
256
390
|
|
|
391
|
+
// There is one history in Edit and it is the page's: a live session is what puts a rich
|
|
392
|
+
// field's typing on it and gives Cmd+Z in any editable field to it. No attribute decides
|
|
393
|
+
// this, and a page that never loads this plugin never starts a session, so nothing about
|
|
394
|
+
// it changes.
|
|
395
|
+
Object.defineProperty(undo, 'pageHistory', {
|
|
396
|
+
get: () => !!session?.started,
|
|
397
|
+
configurable: true,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const announceSession = () => {
|
|
401
|
+
if (typeof document === 'undefined') return
|
|
402
|
+
document.dispatchEvent(new CustomEvent('clay:undo-session', {
|
|
403
|
+
detail: { started: !!session?.started, pageHistory: undo.pageHistory },
|
|
404
|
+
}))
|
|
405
|
+
}
|
|
406
|
+
|
|
257
407
|
function ownership() {
|
|
258
408
|
return { started: false, admitted: false, subscriptions: [] }
|
|
259
409
|
}
|
|
@@ -283,18 +433,29 @@ function undoLease(record) {
|
|
|
283
433
|
if (record.started) return
|
|
284
434
|
record.started = true
|
|
285
435
|
record.admitted = true
|
|
286
|
-
undo.start(withTabLocal({
|
|
436
|
+
undo.start(withTabLocal({
|
|
437
|
+
bindKeys: true,
|
|
438
|
+
editableKeys: 'page',
|
|
439
|
+
ownKeysIn: ['.blockeditor-editor'],
|
|
440
|
+
typingBreak: 'sentence',
|
|
441
|
+
refused: 'skip',
|
|
442
|
+
holdReplay: () => holds.size > 0,
|
|
443
|
+
...(recording ? { recording } : {}),
|
|
444
|
+
}))
|
|
287
445
|
for (const type of ['undo', 'redo']) {
|
|
288
446
|
undo.on(type, afterReplay)
|
|
289
447
|
record.subscriptions.push({ type, handler: afterReplay })
|
|
290
448
|
}
|
|
291
449
|
undo.on('refused', refuse)
|
|
292
450
|
record.subscriptions.push({ type: 'refused', handler: refuse })
|
|
451
|
+
undo.on('skipped', skip)
|
|
452
|
+
record.subscriptions.push({ type: 'skipped', handler: skip })
|
|
293
453
|
for (const type of HISTORY_EVENTS) {
|
|
294
454
|
undo.on(type, announceChange)
|
|
295
455
|
record.subscriptions.push({ type, handler: announceChange })
|
|
296
456
|
}
|
|
297
457
|
announceChange()
|
|
458
|
+
announceSession()
|
|
298
459
|
},
|
|
299
460
|
quiesce() {
|
|
300
461
|
if (!record.admitted) return
|
|
@@ -314,6 +475,7 @@ function undoLease(record) {
|
|
|
314
475
|
for (const { type, handler } of record.subscriptions.splice(0)) undo.off(type, handler)
|
|
315
476
|
undo.stop()
|
|
316
477
|
announceChange()
|
|
478
|
+
announceSession()
|
|
317
479
|
},
|
|
318
480
|
}
|
|
319
481
|
}
|
package/src/recovery/undo.js
CHANGED
|
@@ -148,7 +148,9 @@ let keeping = Promise.resolve();
|
|
|
148
148
|
* keeps it — and answers with the failure.
|
|
149
149
|
*
|
|
150
150
|
* @param {Object} engine - the started undo scope (`clay.undo`)
|
|
151
|
-
* @param {{entryId: string, direction: string, code?: string}} receipt
|
|
151
|
+
* @param {{entryId: string, direction: string, code?: string, description?: ?Object}} receipt
|
|
152
|
+
* `description` is the engine's own `describe(entryId)` answer where the caller already has
|
|
153
|
+
* it, as a skipped step does: that entry is out of the history by the time it is kept.
|
|
152
154
|
* @returns {Promise<{ok: boolean, code?: string}>}
|
|
153
155
|
*/
|
|
154
156
|
export function keepRefusal(engine, receipt) {
|
|
@@ -160,11 +162,13 @@ export function keepRefusal(engine, receipt) {
|
|
|
160
162
|
async function keepRefusalNow(engine, receipt) {
|
|
161
163
|
const ch = channel();
|
|
162
164
|
if (!ch) return { ok: false, code: 'unavailable' };
|
|
163
|
-
let described = null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
let described = receipt?.description ?? null;
|
|
166
|
+
if (!described) {
|
|
167
|
+
try {
|
|
168
|
+
described = engine?.describe?.(receipt?.entryId) ?? null;
|
|
169
|
+
} catch (_) {
|
|
170
|
+
described = null;
|
|
171
|
+
}
|
|
168
172
|
}
|
|
169
173
|
if (!described) return { ok: false, code: 'unknown-entry' };
|
|
170
174
|
const refused = described.refused && typeof described.refused === 'object' ? described.refused : {};
|
|
@@ -186,7 +190,10 @@ async function keepRefusalNow(engine, receipt) {
|
|
|
186
190
|
try {
|
|
187
191
|
staged = ch.stage({ key, kind: UNDO_KIND, baseEtag: representedEtag() ?? null, base: null, payload });
|
|
188
192
|
} catch (err) {
|
|
189
|
-
|
|
193
|
+
const message = receipt?.description
|
|
194
|
+
? 'clayjs: the skipped step could not be kept for a later reload; its text is lost when this tab does'
|
|
195
|
+
: 'clayjs: the refused step could not be kept for a later reload; it is still in this tab\'s history'
|
|
196
|
+
console.error(message, err)
|
|
190
197
|
return { ok: false, code: 'error' };
|
|
191
198
|
}
|
|
192
199
|
const written = await ch.flush();
|
package/src/sync/live-sync.js
CHANGED
|
@@ -199,6 +199,27 @@ const REMOTE_WINS_SELECTOR = [
|
|
|
199
199
|
'[clay~="no-watch"]', '[no-watch]', '[mutations-ignore]',
|
|
200
200
|
].join(',');
|
|
201
201
|
|
|
202
|
+
// Fields whose editor keeps its own undo history: every attribute RichClay mounts
|
|
203
|
+
// on (the vendor's RichClay.selector). That history is copies of the field's markup,
|
|
204
|
+
// and the editor's observer takes any change to the field for the person's typing,
|
|
205
|
+
// so a frame that rewrites a field has to say so before that observer runs, or the
|
|
206
|
+
// next undo there puts the older copy back over the collaborator's text.
|
|
207
|
+
const EDITOR_FIELD_SELECTOR = '[data-richclay], [richclay], [editable], [clay-editable]';
|
|
208
|
+
|
|
209
|
+
// Called right before a frame applies; what it returns is called right after, in
|
|
210
|
+
// the same tick, so no observer callback and no keystroke lands in between. Each
|
|
211
|
+
// field the frame changed is told with `clay:external-write`, the event a refreshed
|
|
212
|
+
// shared value sends (core/site-edits.js), and starts its history from the merged
|
|
213
|
+
// content. A field the frame left alone keeps its history.
|
|
214
|
+
function editorFieldsBeforeApply() {
|
|
215
|
+
const fields = Array.from(document.querySelectorAll(EDITOR_FIELD_SELECTOR), (field) => [field, field.innerHTML]);
|
|
216
|
+
return () => {
|
|
217
|
+
for (const [field, before] of fields) {
|
|
218
|
+
if (field.isConnected && field.innerHTML !== before) field.dispatchEvent(new CustomEvent('clay:external-write'));
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
202
223
|
// Host tokens, library state, router state and ephemeral state, on every side
|
|
203
224
|
// of a merge: the live root, a snapshot clone's root, a parsed frame's root.
|
|
204
225
|
// Extension marker attributes too: snapshots and frames never carry them, so a
|
|
@@ -2011,6 +2032,7 @@ class LiveSync {
|
|
|
2011
2032
|
let lineageCapture;
|
|
2012
2033
|
const mergeDocument = (options) => {
|
|
2013
2034
|
lineageCapture = beginLineageCapture(conflicts.list());
|
|
2035
|
+
const tellRewrittenFields = editorFieldsBeforeApply();
|
|
2014
2036
|
try {
|
|
2015
2037
|
const pending = loadedEngine().HyperMorph.mergeDocument({ ...options, lineage: lineageCapture.lineage });
|
|
2016
2038
|
lineageCapture.returned();
|
|
@@ -2019,6 +2041,8 @@ class LiveSync {
|
|
|
2019
2041
|
lineageCapture.invalidate(conflicts.list());
|
|
2020
2042
|
lineageCapture.finish(conflicts.list());
|
|
2021
2043
|
throw error;
|
|
2044
|
+
} finally {
|
|
2045
|
+
tellRewrittenFields();
|
|
2022
2046
|
}
|
|
2023
2047
|
};
|
|
2024
2048
|
const store = this.identity;
|
package/src/ui/vendor-skin.js
CHANGED
|
@@ -26,9 +26,14 @@ const installed = new Map();
|
|
|
26
26
|
// A vendor that sets no color-scheme of its own inherits the page's, and a page that
|
|
27
27
|
// declares none computes `normal`, which resolves every light-dark() pin to light.
|
|
28
28
|
// ClayJS's own surfaces follow the reader's preference there (pageScheme), so a skin
|
|
29
|
-
// installed with its roots gives them the same scheme.
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// installed with its roots gives them the same scheme. The roots sit in the page's DOM,
|
|
30
|
+
// so the page's ::selection would paint its text colour over a field the skin coloured;
|
|
31
|
+
// the roots get a fixed highlight pair that follows their scheme instead (system
|
|
32
|
+
// Highlight/HighlightText can pair dark text with a dark highlight on a dark Mac).
|
|
33
|
+
function rootRules(roots) {
|
|
34
|
+
const scope = `:is(${roots.join(', ')})`;
|
|
35
|
+
return `\n@layer clay-skin{${scope}{color-scheme:${pageScheme()} !important}` +
|
|
36
|
+
`${scope}::selection,${scope} ::selection{background-color:light-dark(#B4D5FE, #2F5A8C) !important;color:light-dark(#1A1C24, #F2F4F8) !important}}`;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
/**
|
|
@@ -50,7 +55,7 @@ export function installSkin(name, css, { roots } = {}) {
|
|
|
50
55
|
el.setAttribute('clay', RUNTIME_ONLY);
|
|
51
56
|
el.setAttribute('data-clay-skin', name);
|
|
52
57
|
}
|
|
53
|
-
const text = roots ? css +
|
|
58
|
+
const text = roots ? css + rootRules(roots) : css;
|
|
54
59
|
if (el.textContent !== text) el.textContent = text;
|
|
55
60
|
if (!placed(head, el)) head.insertBefore(el, firstNonSkin(head));
|
|
56
61
|
return el;
|
package/dist/m/c-3XNTQQEJ.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as ce,b as Qe,c as ue,e as fe,f as pe,g as nt}from"./c-5IIEOPIV.js";import{a as Ze,b as et,c as de,d as tt}from"./c-LZJPNR2W.js";import{a as A,b as J}from"./c-FILLTJA7.js";import{b as Ke}from"./c-5VYOWDTI.js";import{b as oe,c as ae,d as x,f as he,g as Ve,h as Xe}from"./c-P73XPAOA.js";import{a as q}from"./c-MZCOGNZL.js";import{a as Ge}from"./c-LD6UVV5R.js";import{a as ze,f as We,g as Ye,h as Je}from"./c-PGSBXHUE.js";import{a as se,b as $e,c as P,d as re,e as le,h as je}from"./c-KU3PX7LL.js";import{e as Ue}from"./c-EQ5NBOGA.js";import{d as Pe}from"./c-IJUK2GPW.js";import{f as $,j as te}from"./c-C7SUMNMI.js";import{a as Le}from"./c-BPF2NSPV.js";import{E as Y,F as Ne,H as T,I as Oe,b as D,j as ie,o as W,t as M,z as Be}from"./c-UW6MMZ4V.js";import{a as Re,b as qe,c as Me,e as j,g as z,h as ne}from"./c-MOP5KBWZ.js";import{m as Ie,n as Te}from"./c-HA6INT6J.js";import{a as He,b as Fe,l as Ce,m as Ae}from"./c-ICMAOOGK.js";function it(s){let e=new WeakMap,t=0,i=r=>e.get(r)||null,n=r=>{let l=e.get(r);return l||(l=`${s}:${++t}`,e.set(r,l)),l};return{idOf:i,ensure:n,adopt:(r,l)=>{r&&typeof l=="string"&&l&&e.set(r,l)},exportMap:(r,l)=>{let o={},a=[],h=[],c=(d,_)=>{let g=l(d);g&&(o[_]=n(g));let E=d.children;a.push(E.length),h.push(d.tagName.toLowerCase());for(let f=0;f<E.length;f++)c(E[f],_===""?String(f):`${_}.${f}`)};return c(r,""),o["~"]=a.join(","),o["^"]=h.join(","),o}}}function st(s,e){let t=new WeakMap;if(!s||!e||typeof e!="object"||Array.isArray(e))return t;let i=e["~"],n=typeof i=="string"?i.split(",").map(Number):null,r=n&&typeof e["^"]=="string"?e["^"].split(","):null,l=d=>Number.isInteger(d)&&d>=0,o=0,a=!1,h=d=>{let _=d;for(;_>0;){let g=n[o++];if(o>n.length||!l(g))return!1;_+=g-1}return!0},c=(d,_)=>{if(a)return;if(n){let f=o++,u=n[f];if(f>=n.length||!l(u)){a=!0;return}let S=!r||r[f]===d.tagName.toLowerCase();if(!S||u!==d.children.length){let w=S&&e[_];typeof w=="string"&&w&&t.set(d,w),h(u)||(a=!0);return}}let g=e[_];typeof g=="string"&&g&&t.set(d,g);let E=d.children;for(let f=0;f<E.length;f++)c(E[f],_===""?String(f):`${_}.${f}`)};return c(s,""),t}function St(s){return e=>{for(let t of s){let i=t(e);if(typeof i=="string"&&i!=="")return i}return null}}var Bt=St([s=>s.getAttribute("data-id"),s=>s.getAttribute("id")]);var Et=/^blockeditor(\.[0-9]+)?$/,rt=s=>[...s.querySelectorAll('script[merge][clay~="live"]')].filter(e=>Et.test(e.getAttribute("merge")));function lt(s,e,t){if(!s.html.includes("data-blockeditor"))return s;let i=u=>W(new DOMParser().parseFromString(u,"text/html")),n=i(s.html),r=n.querySelector("article[data-blockeditor]");if(!r)return s;let l=i(e),o=l.querySelector("article[data-blockeditor]");if(!o)return null;let a=st(n.documentElement,s.identityMap),h=(u,S)=>{let w=n.importNode(S,!0),b=a.get(u);return b&&a.set(w,b),u.replaceWith(w),w},c=h(r,o),d=new Map(rt(n).map(u=>[u.getAttribute("merge"),u]));for(let u of rt(l)){let S=u.getAttribute("merge"),w=d.get(S);w?h(w,u):c.before(n.importNode(u,!0)),d.delete(S)}for(let u of d.values())u.remove();let _=n.querySelector("head > title"),g=l.querySelector("head > title");_&&g?h(_,g):g?n.head.append(n.importNode(g,!0)):_?.remove();let E=n.documentElement,f=t.exportMap(E,u=>{let S=a.get(u);return S?(t.adopt(u,S),u):null});return{...s,html:T(E),identityMap:f}}var wt={"merge-tokens":"tokens","merge-max":"max"};function ot(s,e){if(!e.startsWith("data-")||e==="data-id"||!s?.closest)return null;for(let[t,i]of Object.entries(wt)){let n=s.closest(`[${t}]`);if(n&&n.getAttribute(t).split(/\s+/).includes(e))return i}return null}var m=null,at=null,B=new Map;function V(){return at??=Promise.all([import("./c-O3DPFZCE.js"),import("./c-EJTGYSLN.js")]).then(([s,e])=>{m={...s,beginLineageCapture:e.beginLineageCapture};for(let t of[...B.keys()])B.set(t,s.registerContext(t));return m}),at}var ht=()=>m,I={get size(){return m?m.conflicts.size:0},list(){return m?m.conflicts.list():[]},get(s){return m?m.conflicts.get(s):null},acknowledge(s,e={}){if(m)return m.conflicts.acknowledge(s,e);if(!Array.isArray(s))throw new TypeError("clay.conflicts.acknowledge needs an array of ids");return{removedIds:[],remainingIds:[]}},claim(s,e={}){if(m)return m.conflicts.claim(s,e);if(!e.owner)throw new TypeError("clay.conflicts.claim needs an owner");return{release(){}}},registerContext(s){if(m)return m.conflicts.registerContext(s);if(typeof s!="function")throw new TypeError("clay.conflicts.registerContext needs a resolver function");return B.set(s,null),()=>{let e=B.get(s);B.delete(s),e&&e()}},contextFor(s,e){return m?m.conflicts.contextFor(s,e):null},recoveryOf(s){return m?m.conflicts.recoveryOf(s):null},hasPendingApply(){return m?m.conflicts.hasPendingApply():!1}};var y=null,_e=null;function R(){return _e??=new Promise(s=>setTimeout(s,0)).then(()=>import("./c-HYNM5U6B.js")).then(s=>y=s).catch(s=>{throw _e=null,s}),_e}var ve=null,ge=null;function kt(){return ge??=import("./c-SMLAJASY.js").then(s=>ve=s.presence).catch(s=>{throw ge=null,s}),ge}var ft=te().phase==="entering";$&&!ft&&R().catch(()=>{});$&&V().catch(()=>{});var L=null,ye=null,Q=[];function pt(s){L.presence.update(s),L.editingMarker.update(s)}function xt(){return ye??=Promise.all([import("./c-WOWGGNCJ.js"),import("./c-EEWPNUM2.js")]).then(([s,e])=>{L={presence:s.presence,editingMarker:e.editingMarker};let t=Q;Q=[];for(let i of t)pt(i);return L}).catch(s=>{throw ye=null,s}),ye}function bt(s){if(L){pt(s);return}Q.push(s),xt().catch(()=>{})}var dt=Object.freeze({bytes:null,etag:null,seq:null}),Dt=()=>({msg:"Not in edit mode",msgType:"skipped",code:null,etag:null}),_t=["[editor-ui]",'[clay~="editor-ui"]',"[save-ignore]","[snapshot-remove]","[no-snapshot]","[no-save]","[save-remove]","[freeze] *","[save-freeze] *",'[clay~="no-save"]','[clay~="no-snapshot"]','[clay~="freeze"] *',Ce,He].join(","),gt='[clay~="freeze"], [freeze], [save-freeze]';function X(s){for(let e of s.querySelectorAll(gt))e.replaceChildren();return z()&&Me(s),s}function ut(s){if(s==null)return s;let e=typeof s=="string"?new DOMParser().parseFromString(s,"text/html"):s;return ie(e),W(e),X(e),e}var It=[Ae,'[clay~="no-watch"]',"[no-watch]","[mutations-ignore]"].join(","),Tt=(s,e,t)=>Ie(s,e,t)||Fe.test(e.toLowerCase()),yt=s=>s.getAttribute("data-id")||s.getAttribute("id")||null;function Lt(s){let e=new Map,t=n=>{if(n.matches(_t))return;let r=yt(n);r&&e.set(r,(e.get(r)||0)+1);let l=n.localName==="template"&&n.content?n.content.children:n.children;for(let o of l)t(o)},i=s.nodeType===9?[s.documentElement]:s.nodeType===11?s.children:[s];for(let n of i)n&&n.nodeType===1&&t(n);return e}var Ht=s=>e=>{let t=yt(e);if(!t)return null;let i=e.getRootNode(),n=s.get(i);return n||s.set(i,n=Lt(i)),n.get(t)===1?t:null},me=s=>!!s&&typeof s=="object"&&!Array.isArray(s);function Ft(s){return typeof s!="string"||s===""?null:new DOMParser().parseFromString(s,"text/html").documentElement?.getAttribute("documentetag")||null}function Ct(s,e){e!=="live"||!s||!y||(y.enableContentEditable(s),y.enableOnClick(s),y.enableAdminInputs(s),y.enableAdminResources(s))}function At(){let{forComparison:s,forDirty:e}=Be({flushUndo:!1});return[s,e]}var Rt=1024*1024;function qt(s,e,t,i,n){let r={sender:t,identityMap:i,...n?{etag:n}:{}};return e.length<Rt?JSON.stringify({[s.snapshotKey]:e,...r}):new Blob([`{${JSON.stringify(s.snapshotKey)}:`,JSON.stringify(e),`,${JSON.stringify(r).slice(1)}`],{type:"application/json"})}var Se=class{constructor(){this.sse=null,this.currentFile=null,this.lastHtml=null,this._diskBase=null,this._diskBaseTicket=0,this._agreeSeq=0,this._saveTicket=0,this._peerBaseTicket=0,this._acceptedSaveTicket=0,this._morphTicket=0,this.clientId=this.generateClientId(),this.resumeId=null,this._savedSnapshot?.abort?.abort(),this._savedSnapshot=null,this._sendInFlight=!1,this._queuedSend=null,this._profile=null,this._profilePromise=null,this._sharedData=!1,this._sharedSync=!1,this._workerFeatures=["live-data"],this._liveData=!1,this._editing=null,this._presenceLease=null,this._presenceQuiesced=!1,this._ready=null,this._startGen=0,this.isPaused=!1,this.isDestroyed=!1,this.debug=!1,this._lane=$?"live":"saved",this._laneChosen=!1,this._snapshotHandler=null,this._hiddenHandler=null,this.lastSeenSeq=0,this._pendingHtml=null,this._pendingSeq=null,this._pendingIdentityMap=null,this._pendingEtag=null,this._pendingBy=null,this._morphInFlight=!1,this._rafHandle=null,this._represented=dt,this._handoff=null,this._handoffHeld=!1,this._pendingExternal=null,this._lastExternalSeq=0,this._saveEpoch=0,this._saveSavedHandler=null,this._saveConflictHandler=null,this._settledHandler=null,this._lastIdentityMap=null,this._applyGen=0,this._holdRetryPeer=null,this._holdRetryExt=null,this._servedFetchId=0,this._startupRetry=null,this._engineRetry=null,this._heldLive=!1,this._heldExt=!1,this.identity=it(this.clientId),this.onConnect=null,this.onDisconnect=null,this.onUpdate=null,this.onError=null,this.onNotification=null}get unresolvedConflicts(){return I.list().filter(e=>e.kind!=="apply-incomplete")}_log(e,t=null){if(!this.debug)return;let i=`[LiveSync ${new Date().toISOString()}]`;t!==null?console.log(i,e,t):console.log(i,e)}generateClientId(){return Math.random().toString(36).slice(2,11)+Date.now().toString(36)}generateResumeId(){try{let e=new Uint8Array(16);return(globalThis.crypto||window.crypto).getRandomValues(e),Array.from(e,t=>t.toString(16).padStart(2,"0")).join("")}catch{return Math.random().toString(36).slice(2)+Date.now().toString(36)}}_seedBases(){if(x())return;let e=M({flushUndo:!1});this._setPeerBase(T(e),this.identity.exportMap(e,D),this._ticket()),this._setDiskBase(Y({emitForSync:!1}).forSave,this._ticket())}get lane(){return this._lane}set lane(e){this._lane=e,this._laneChosen=!0}start(e=null,{lane:t,represented:i}={}){if(this.isDestroyed=!1,(this.sse||this._snapshotHandler)&&this.cleanup(),this.currentFile=e||this.detectCurrentFile(),!this.currentFile){console.warn("[LiveSync] No file detected");return}let n=te().mode==="edit"?"live":"saved",r=this.lane==="live";this._lane=t||(this._laneChosen?this._lane:n),this.lastHtml=null,this._lastIdentityMap=null,this._peerBaseTicket=this._ticket(),this._diskBase=null,this._diskBaseTicket=this._ticket(),this._saveTicket=0,this._savedSnapshot?.abort?.abort(),this._savedSnapshot=null,this._applyGen++,this.lastSeenSeq=0,this._lastExternalSeq=0,this._represented=i?Object.freeze({bytes:i.bytes??null,etag:i.etag??null,seq:i.seq??null}):dt,this._acquireEmptySaved=r&&this.lane==="saved"&&this._represented.bytes===null,this._pendingExternal=null,this.resumeId=this.generateResumeId(),this.lane==="live"&&this._seedBases(),this.lane==="live"&&V().catch(()=>{}),console.log(`[LiveSync] Starting for: ${this.currentFile} (lane=${this.lane})`);let l=++this._startGen;if(this.lane==="live"&&!(y&&y.baselineSettled())){let o=this._applyGen,a=this._morphTicket,h=this._saveEpoch,c=this.lastHtml,d=this._diskBaseTicket,_=this._diskBase!==null;this._settledHandler=()=>{if(this._settledHandler=null,!(this.isDestroyed||this._startGen!==l)&&!Pe()&&!x()&&this._saveTicket===0&&this._morphTicket===a){if(c!==null&&this._applyGen===o&&this._saveEpoch===h&&this.lastHtml===c){let g=M({flushUndo:!1});this._setPeerBase(T(g),this.identity.exportMap(g,D),this._ticket())}_&&this._diskBaseTicket===d&&this._setDiskBase(Y({emitForSync:!1}).forSave,this._ticket())}},document.addEventListener("clay:baseline-settled",this._settledHandler,{once:!0})}this._ready=Promise.all([this.lane==="live"?R():null,this._resolveProfile()]).then(()=>{this.isDestroyed||this._startGen!==l||this.connect()}),this.lane==="live"&&(this.listenForSnapshots(),kt().then(o=>this._startPresence(o,l),()=>{}),this._saveSavedHandler=o=>{this._saveEpoch++;let a=this._saveTicket||this._ticket();this._saveTicket=0,a>this._acceptedSaveTicket&&(this._acceptedSaveTicket=a);let h=y?y.getLastSavedBytes():null;h!=null&&this._setDiskBase(h,a),this._relayCommit(o?.detail?.captureId,h)},document.addEventListener("clay:save-saved",this._saveSavedHandler),this._saveConflictHandler=o=>{let a=o.detail&&o.detail.etag;a&&queueMicrotask(()=>{this.isDestroyed||a===se()&&(y?.conflictResolvedBySync(a),this._saveAfterMerge(x(),!1))})},document.addEventListener("clay:save-conflict",this._saveConflictHandler))}cleanup(){if(this.sse&&(this.sse.close(),this.sse=null),de(),Q=[],L&&(L.presence.clear(),L.editingMarker.clear()),this._editing&&(this._editing.stop(),this._editing=null),this._presenceLease){let e=this._presenceLease;this._presenceLease=null,typeof e.stop=="function"?e.stop():e.detach()}this._lastRoster=null,this._snapshotHandler&&(document.removeEventListener("clay:snapshot-ready",this._snapshotHandler),this._snapshotHandler=null),this._saveSavedHandler&&(document.removeEventListener("clay:save-saved",this._saveSavedHandler),this._saveSavedHandler=null),this._saveConflictHandler&&(document.removeEventListener("clay:save-conflict",this._saveConflictHandler),this._saveConflictHandler=null),this._settledHandler&&(document.removeEventListener("clay:baseline-settled",this._settledHandler),this._settledHandler=null),this._hiddenHandler&&(document.removeEventListener("visibilitychange",this._hiddenHandler),document.removeEventListener("clay:workspace-background",this._hiddenHandler),this._hiddenHandler=null),this._queuedSend=null,this._rafHandle!=null&&(this._cancelFrame(this._rafHandle),this._rafHandle=null),this._pendingHtml=null,this._pendingSeq=null,this._pendingIdentityMap=null,this._pendingBy=null,this._pendingExternal=null,this._handoff=null,this._handoffHeld=!1,clearTimeout(this._holdRetryPeer),clearTimeout(this._holdRetryExt),this._holdRetryPeer=null,this._holdRetryExt=null,clearTimeout(this._startupRetry),this._startupRetry=null,clearTimeout(this._engineRetry),this._engineRetry=null,this._servedFetchId++}detectCurrentFile(){let e=window.location.pathname;if(e==="/")return"index.html";e=e.replace(/^\//,"");let t=e.match(/^(.*?\.html(?:clay)?)/);return t?t[1]:e}_resolveProfile(){return this._profilePromise||(this._profilePromise=nt().then(e=>(this._sharedData=e.sharedData,this._sharedSync=e.sharedSync,this._liveData=e.liveData,this._workerFeatures=e.workerFeatures,this._profile=e.profile,this._log(`Wire profile: ${e.profile.name}`),e.profile))),this._profilePromise}_setHeld(e,t,i){let n=e==="live"?"_heldLive":"_heldExt";this[n]!==t&&(this[n]=t,!t&&!this._heldLive&&!this._heldExt&&je({fresh:!0,clearIfMissing:!1}),document.dispatchEvent(new CustomEvent(t?"clay:sync-held":"clay:sync-resumed",{detail:{lane:e,el:i||null}})))}_applyEtagFrame(e){return typeof e.etag!="string"||typeof e.html=="string"?!1:(this._log("Dropping a stamp that arrived with no document: nothing proves this tab holds those bytes"),!0)}connect(){if(this.isDestroyed)return;let t=(this._profile||pe.legacy).streamPath(window.location.href,this.lane,this.resumeId,this.clientId,1);this._hiddenHandler||(this._hiddenHandler=()=>{this._rafHandle==null||!ce()||(this._cancelFrame(this._rafHandle),this._rafHandle=null,this._scheduleNextFrame())},document.addEventListener("visibilitychange",this._hiddenHandler),document.addEventListener("clay:workspace-background",this._hiddenHandler)),this.sse=new Qe(new URL(t,window.location.origin).href,{shared:this._sharedSync,sharedData:this._sharedData,workerFeatures:this._workerFeatures,documentURL:window.location.href,lane:this.lane,clientId:this.clientId,keepAliveWhenHidden:this.lane!=="live"?!1:this._liveData?!0:"shared",startupCheck:!!Le}),this._acquireEmptySaved&&(this._acquireEmptySaved=!1,this._fetchServedDocument(void 0,{repair:!0})),this.sse.onopen=()=>{console.log("[LiveSync] Connected"),this._editing&&this._editing.reconnected(),ve&&ve.reconnect(),this.onConnect&&this.onConnect()},this.sse.addEventListener("open",()=>et({clientId:this.clientId,lane:this.lane})),this.sse.addEventListener("error",()=>de());for(let i of Ze)this.sse.addEventListener(i,n=>{let r;try{r=JSON.parse(n.data)}catch{return}tt(i,r)});this.sse.addEventListener("cursor",i=>{let n;try{n=JSON.parse(i.data)}catch{return}if(!n||n.resync!==!0&&n.startup!==!0)return;let r=n.resync===!0,l=n.startup===!0&&!r;console.log(r?"[LiveSync] Server could not replay everything; refetching the document":"[LiveSync] Checking the served document against the version this tab was served"),this._fetchServedDocument(typeof n.seq=="number"?n.seq:void 0,{repair:r,startup:l})}),this.sse.addEventListener("presence",i=>{let n;try{n=JSON.parse(i.data)}catch{return}bt(n),this._lastRoster=n,this._editing&&this._editing.roster(n)}),this.sse.onmessage=i=>{let n=JSON.parse(i.data),{seq:r}=n;if(typeof r=="number"&&r<=this.lastSeenSeq){this._log(`Dropping stale message: seq=${r}, lastSeen=${this.lastSeenSeq}`);return}if(typeof r=="number"&&(this.lastSeenSeq=r),n.type==="notification"){if(this._maybeAcceptExternalChange(n))return;this.handleNotification(n);return}if(n.error){console.error("[LiveSync] Server error:",n.error),this.onError&&this.onError(new Error(n.error));return}if(this._applyEtagFrame(n))return;let{html:l,sender:o,identityMap:a}=n;if(o===this.clientId){this._log("Ignoring own message (sender matches clientId)");return}if(typeof l!="string"){console.error("[LiveSync] Received invalid html, ignoring");return}let h=typeof n.etag=="string"&&n.etag?n.etag:null;if(this.lane==="live"&&!h&&$e()){this._log(`Dropping an unstamped live frame on a stamping host (seq=${r})`);return}if(this.lane==="saved"&&l===this._represented.bytes){this._represented=Object.freeze({bytes:l,etag:h||null,seq:r??null}),P(h);return}if(h&&l===this.lastHtml){this._log(`Taking the stamp from a peer save of content already applied (seq=${r})`),P(h),y&&y.isSaveConflicted()&&(y.conflictResolvedBySync(h),this._saveAfterMerge(x(),!1));return}this._log(`Received update from: ${o} (my clientId: ${this.clientId}, seq=${r})`),this.applyUpdate(l,r,a,h,n.by),this.onUpdate&&this.onUpdate({html:l,sender:o,seq:r,identityMap:a,etag:h})},this.sse.onerror=()=>{this.sse.readyState===EventSource.CONNECTING?console.log("[LiveSync] Reconnecting..."):this.sse.readyState===EventSource.CLOSED&&(console.log("[LiveSync] Connection closed"),this.onError&&this.onError(new Error("Connection closed"))),this.onDisconnect&&this.onDisconnect()}}listenForSnapshots(){this._snapshotHandler=e=>{if(!e.detail||e.detail.forSave!==!0)return;this._saveTicket=this._ticket();let{documentElement:t}=e.detail;if(!t)return;this._log("snapshot-ready received, preparing to send");let i=this.identity.exportMap(t,D);this._savedSnapshot?.abort?.abort();let n={html:null,steps:null,copy:null,abort:null,identityMap:i,ticket:this._saveTicket,captureId:e.detail.captureId??null};if(e.detail.sliced){n.copy=Oe(t),n.abort=new AbortController;let{signal:r}=n.abort;Promise.all([import("./c-L6KVXPBO.js"),import("./c-ZWBI4HYK.js")]).then(([{serializeForSyncSteps:l},{runSliced:o}])=>{if(r.aborted||typeof n.html=="string")return;let a=(function*(){let h=yield*l(n.copy);n.html=h,n.steps=null})();return n.steps=a,o(a,{signal:r})}).catch(()=>{})}else n.html=T(t);this._savedSnapshot=n},document.addEventListener("clay:snapshot-ready",this._snapshotHandler)}_enqueueSend(e,t,i=null){if(!this._profile){this._queuedSend={html:e,identityMap:t,etag:i},this._resolveProfile().then(()=>this._flushQueuedSend());return}if(this._sendInFlight){this._queuedSend={html:e,identityMap:t,etag:i};return}this._postUpdate(e,t,i)}_flushQueuedSend(){if(this.isDestroyed||this._sendInFlight)return;let e=this._queuedSend;e&&(this._queuedSend=null,this._postUpdate(e.html,e.identityMap,e.etag))}_relayCommit(e=null,t=null){if(this.isDestroyed)return;let i=se(),n=this._savedSnapshot;if(n&&(n.captureId??null)!==(e??null)){this._log("Skipping send - snapshot belongs to a different capture");return}let r=n;if(this._savedSnapshot=null,r?.copy&&typeof r.html!="string")if(r.abort?.abort(),r.steps)for(let c=r.steps.next();!c.done;c=r.steps.next());else r.html=T(r.copy);if(!r||typeof r.html!="string")return;let l=r;if(typeof t=="string"){try{l=lt(r,t,this.identity)}catch(c){this._log(`Skipping send - page relay failed: ${c&&c.message}`);return}if(!l){this._log("Skipping send - accepted capture has no page article");return}}let o=Mt(e)??t,a=typeof o=="string"?We(o):0;l={...l,html:Pt(Ye(l.html,Math.max(Je(),a)),o)};let h=!i&&l.html===this.lastHtml;if(this._setPeerBase(l.html,l.identityMap,l.ticket),h){this._log("Skipping send - HTML unchanged");return}this._enqueueSend(l.html,l.identityMap,i||null)}_postUpdate(e,t,i=null,n=0){this._log(`Sending update (HTML length: ${e.length})`),this._sendInFlight=!0;let r=!1,l=this._profile||pe.legacy;fetch(new URL(l.relayPath,window.location.origin).href,{method:"POST",headers:{"Content-Type":"application/json",[l.documentHeader]:window.location.href,[ze]:String(1)},body:qt(l,e,this.clientId,t,i)}).then(o=>{o.ok||(console.warn("[LiveSync] Save returned status:",o.status),r=o.status>=500||o.status===408||o.status===429)}).catch(o=>{console.error("[LiveSync] Save failed:",o),this.onError&&this.onError(o),r=!0}).finally(()=>{this._sendInFlight=!1;let o=this._queuedSend;if(this._queuedSend=null,o){this._postUpdate(o.html,o.identityMap,o.etag);return}!r||!i||n>=3||this.isDestroyed||(this._sendInFlight=!0,setTimeout(()=>{if(this._sendInFlight=!1,this.isDestroyed)return;let a=this._queuedSend;this._queuedSend=null,a?this._postUpdate(a.html,a.identityMap,a.etag):this._postUpdate(e,t,i,n+1)},500*2**n))})}_startPresence(e,t){return this.isDestroyed||this._startGen!==t||this._presenceLease?null:e.connect({send:i=>this._postPresence(i)}).then(i=>this.isDestroyed||this._startGen!==t?(typeof i?.stop=="function"?i.stop():i?.detach(),null):(this._presenceLease=i||null,Ue("presence-editing")),()=>!1).then(i=>{if(!(!i||this.isDestroyed||this._startGen!==t||this._editing))return R().then(n=>{this.isDestroyed||this._startGen!==t||this._editing||(this._editing=new n.EditingSender({}),this._presenceQuiesced?this._editing.stop():(this._editing.start(),this._editing.roster(this._lastRoster)))})})}_postPresence(e){let t=new URL("/_/presence",window.location.origin);return t.searchParams.set("client-id",this.clientId),fetch(t.href,{method:"POST",headers:{"Content-Type":"application/json","Document-URL":window.location.href},body:JSON.stringify(e)}).then(i=>i.ok,()=>!1)}_maybeAcceptExternalChange(e){if(this.lane!=="live")return!1;let t=e.data;return t&&t.kind==="external-change"?(typeof t.html=="string"?this._enqueueExternal(t.html,e.seq,t.etag,t.by):this._fetchExternalChange(e.seq),!0):typeof e.msg=="string"&&e.msg.endsWith("changed on disk outside this tab")?(this._fetchExternalChange(e.seq),!0):!1}_enqueueExternal(e,t,i,n){if(typeof t=="number"){if(t<=this._lastExternalSeq){this._log(`Dropping replayed external change: seq=${t}`);return}this._lastExternalSeq=t}this._pendingExternal={html:e,seq:t,saveEpoch:this._saveEpoch,etag:typeof i=="string"&&i?i:null,by:n??null},this._scheduleNextFrame()}_fetchExternalChange(e){if(typeof e=="number"){if(e<=this._lastExternalSeq)return;this._lastExternalSeq=e}this._fetchServedDocument(e)}_fetchServedDocument(e,{attempt:t=0,repair:i=!1,startup:n=!1}={}){if(n&&(this._morphInFlight||this._pendingHtml!=null||this._pendingExternal!=null)){this._retryStartup(t);return}let r=this._saveEpoch,l=this._startGen,o=this.lastSeenSeq,a=this._applyGen,h=++this._servedFetchId;i&&typeof e!="number"&&(e=this._lastExternalSeq),fetch(new URL(window.location.href),{cache:"no-store"}).then(c=>c.ok?c.text():null).then(c=>{if(this.isDestroyed||c==null||this._startGen!==l)return;let d=Ft(c),_=n||i&&!!d;if(_){if(h!==this._servedFetchId){i&&this._retryStartup(t+1,{repair:i,startup:n},typeof e=="number"?Math.max(e,this._lastExternalSeq):e);return}if(r!==this._saveEpoch||o!==this.lastSeenSeq||a!==this._applyGen||this._morphInFlight||this._pendingHtml!=null||this._pendingExternal!=null){this._retryStartup(t+1,{repair:i,startup:n},e);return}if(d){if(d===re()){(i||n)&&this.lane==="saved"&&(this._represented=Object.freeze({bytes:c,etag:d,seq:e??null}));return}}else if(!i)return}else if(typeof e=="number"&&e<this._lastExternalSeq){i&&t<3&&this._fetchServedDocument(this._lastExternalSeq,{attempt:t+1,repair:i,startup:n});return}else if(this._saveEpoch>r){t<3&&(console.log("[LiveSync] Refetching external change: own save landed mid-fetch"),this._fetchServedDocument(e,{attempt:t+1,repair:i,startup:n}));return}this._pendingExternal={html:c,seq:e,saveEpoch:r,etag:d,by:null,fetchOptions:{repair:i,startup:n,attempt:t,versionCheck:_},startGen:l,seenSeq:o,applyGen:a,fetchId:h},this._scheduleNextFrame()}).catch(c=>{this._log("External-change fetch failed",c),typeof e=="number"&&this._lastExternalSeq===e&&(this._lastExternalSeq=e-1)})}_retryStartup(e=0,t={startup:!0},i){this.isDestroyed||e>3||(clearTimeout(this._startupRetry),this._startupRetry=setTimeout(()=>{if(this._startupRetry=null,!this.isDestroyed){if(this._morphInFlight||this._pendingHtml!=null||this._pendingExternal!=null){this._retryStartup(e,t,i);return}this._fetchServedDocument(i,{...t,attempt:e})}},16))}applyUpdate(e,t,i,n,r){this.isDestroyed||(this._pendingHtml=e,this._pendingSeq=t,this._pendingIdentityMap=i,this._pendingEtag=n??null,this._pendingBy=r??null,this._scheduleNextFrame())}prepareHandoff(e={}){return this._handoff?Promise.resolve(this._handoff):this.isDestroyed?Promise.reject(new Error("LiveSync: stopped")):(this._handoffHeld=!0,this._rafHandle!=null&&(this._cancelFrame(this._rafHandle),this._rafHandle=null),import("./c-DAUQ6JZ3.js").then(({engineHandoff:t})=>t(this,e)).then(t=>this._handoff=t))}_scheduleNextFrame(){this.isDestroyed||this._handoffHeld||this._rafHandle==null&&(this._morphInFlight||(this._rafHandle=this._requestFrame(()=>this._runPending())))}async _runPending(){if(this._rafHandle=null,this.isDestroyed)return;if(!A()){this._morphInFlight=!0;try{await J()}catch(r){this._log("Merge engine failed to load",r),this._engineRetry=setTimeout(()=>this._scheduleNextFrame(),2e3);return}finally{this._morphInFlight=!1}if(this.isDestroyed)return}let e=this._pendingExternal,t=e?.fetchOptions||null,i=!!(t?.startup||t?.versionCheck),n=!1;if(e!=null&&(i&&this._pendingHtml!=null?n=!1:this._pendingHtml==null?n=!0:n=!(typeof e.seq=="number"&&typeof this._pendingSeq=="number"&&this._pendingSeq<e.seq)),n)if(this._pendingExternal=null,typeof e.seq=="number"&&e.seq<this._lastExternalSeq)i&&t?.repair?this._retryStartup((t.attempt||0)+1,t,this._lastExternalSeq):this._log("Dropping superseded external change at drain");else if(i&&e.startGen!==this._startGen)this._log("Dropping superseded startup check at drain");else if(i&&e.fetchId!==this._servedFetchId)t?.repair?this._retryStartup((t.attempt||0)+1,t,this._lastExternalSeq):this._log("Dropping superseded startup check at drain");else if(i&&(e.saveEpoch!==this._saveEpoch||e.seenSeq!==this.lastSeenSeq||e.applyGen!==this._applyGen))this._retryStartup((t.attempt||0)+1,t,e.seq);else if(i&&e.etag!=null&&e.etag===re())this._log("Startup check found the same version: nothing to apply");else if(!i&&e.saveEpoch!==this._saveEpoch)console.log("[LiveSync] Refetching external change: own save landed after queue"),this._fetchServedDocument(e.seq,t||{});else{this._morphInFlight=!0;try{await this._doApplyExternal(e.html,e.seq,e.etag,e.by,t)}catch(r){console.error("[LiveSync] applyExternal failed:",r)}finally{this._morphInFlight=!1}}else{let r=this._pendingHtml,l=this._pendingSeq,o=this._pendingIdentityMap,a=this._pendingEtag,h=this._pendingBy;if(this._pendingHtml=null,this._pendingSeq=null,this._pendingIdentityMap=null,this._pendingEtag=null,this._pendingBy=null,r==null)return;this._morphInFlight=!0;try{await this._doApplyUpdate(r,l,o,a,h)}catch(c){console.error("[LiveSync] applyUpdate failed:",c)}finally{this._morphInFlight=!1}}!this.isDestroyed&&(this._pendingHtml!=null||this._pendingExternal!=null)&&this._scheduleNextFrame()}_ticket(){return++this._agreeSeq}_setDiskBase(e,t){t<=this._diskBaseTicket||(this._diskBase=e,this._diskBaseTicket=t)}_setPeerBase(e,t,i){return i<this._peerBaseTicket?!1:(this._peerBaseTicket=i,this.lastHtml=e,this._lastIdentityMap=t&&typeof t=="object"&&!Array.isArray(t)?t:null,!0)}_requestFrame(e){if(ce()){if(typeof MessageChannel=="function"){let n=new MessageChannel,r=!0;return n.port1.onmessage=()=>{n.port1.close(),r&&e()},n.port2.postMessage(null),{cancel:()=>{r=!1,n.port1.close()}}}let i=setTimeout(e,0);return{cancel:()=>clearTimeout(i)}}if(typeof window<"u"&&typeof window.requestAnimationFrame=="function"){let i=window.requestAnimationFrame(e);return{cancel:()=>window.cancelAnimationFrame?.(i)}}let t=setTimeout(e,16);return{cancel:()=>clearTimeout(t)}}_cancelFrame(e){e?.cancel?.()}async _mergeIncoming(e,t,{base:i,baseIdentityMap:n=null,captureLocal:r,synthetic:l,source:o="peer",seq:a=null,etag:h=null,extra:c={}}){A()||await J();let{beginApply:d,completeApply:_,failApply:g,beginLineageCapture:E}=ht()||await V(),f,u=p=>{f=E(I.list());try{let v=A().HyperMorph.mergeDocument({...p,lineage:f.lineage});return f.returned(),v}catch(v){throw f.invalidate(I.list()),f.finish(I.list()),v}},S=this.identity,w=Te,b=Ht(new WeakMap),N=p=>me(p)?{first:b,map:p,then:b}:b,O=l?p=>b(p)||S.idOf(D(p)||p)||null:b,H=e,we=new Map;if(me(t)){H=new DOMParser().parseFromString(e,"text/html"),ie(H);let p=A().HyperMorph.importMap(H.documentElement,t);for(let v of H.querySelectorAll("[data-id], [id]")){let k=b(v),De=k&&p.get(v);De&&we.set(k,De)}}H=ut(H);let Z={live:document,remote:H,ignore:p=>p.matches(_t),opaque:p=>p.matches(gt)||z()&&qe(p),remoteWins:p=>p.matches(It),ignoreAttribute:(p,v,k)=>Tt(p,v,k)||z()&&Re(k||p,v),attributeMerge:ot,protectFocusedValue:!0,restoreFocus:document.activeElement?.localName!=="clay-editor-ui",keepLiveOnly:o==="peer",scripts:{mergeTags:A().mergeTagRecognizers},hooks:{beforeAttributeUpdated:w},...c},ke=this.lane==="live"&&x(),xe=he(),G,ee=null,C=null;if(i==null)G=u({...Z,base:null,local:{root:X(r()),toLive:D},identity:{local:O,remote:N(t)}});else if(ke){let p=r(),v=p.cloneNode(!0);ee=X(p),C=d({source:o,seq:a,etag:h,root:ee,recoveryRoot:v,domain:o==="disk"?"save":"sync"});try{G=u({...Z,base:ut(i),local:{root:ee,toLive:D},identity:{base:N(n),local:O,remote:N(t)}})}catch(k){throw g(C,k),k}}else{let p=X(r()),v=p.ownerDocument;v.documentElement!==p&&v.replaceChild(p,v.documentElement),G=u({...Z,base:v,local:{root:p,toLive:D},fastPath:!0,identity:{base:O,local:O,remote:N(t)}})}let U=this._ticket();this._morphTicket=U;let F;try{F=await G}catch(p){f.invalidate(I.list());try{C&&g(C,p,{ticket:U})}finally{f.finish(I.list())}throw p}let mt=he().gen!==xe.gen,be;try{f.prepareComplete(F,I.list()),be=C?_(C,F.conflicts,{ticket:U}):[]}finally{f.finish(I.list())}this.lane==="live"&&F.localDiverged?Ve():ke&&!F.localDiverged&&Xe(xe);let vt=new Set(me(t)?Object.values(t):[]);for(let[p,v]of F.identities){let k=vt.has(v)?v:we.get(v);k&&S.adopt(p,k)}return{report:F,ticket:U,typedDuringWait:mt,conflictIds:be}}_saveAfterMerge(e,t){(e||t)&&Ke()&&(y?y.savePageThrottled():Dt())}async _doApplyUpdate(e,t,i,n,r=null){if(ue(e)){await fe();return}this.lane==="live"&&!y&&await R(),this._log("applyUpdate - pausing mutations and morphing"),this.isPaused=!0;let l=window.scrollX,o=window.scrollY;q.pause(),oe();let a=!1,h=!1,c=!1;try{if(this.lane==="live"&&x()&&this.lastHtml===null){console.log("[LiveSync] Holding incoming update: unsaved local edits and no baseline to merge against"),this._setHeld("live",!0,null);let f=this._saveEpoch,u=this.lastSeenSeq;clearTimeout(this._holdRetryPeer),this._holdRetryPeer=setTimeout(()=>{this._holdRetryPeer=null,!(this.isDestroyed||this._pendingHtml!=null)&&this._saveEpoch===f&&this.lastSeenSeq===u&&(this._pendingHtml=e,this._pendingSeq=t,this._pendingIdentityMap=i,this._pendingBy=r,this._scheduleNextFrame())},3e3);return}this._setHeld("live",!1);let{report:d,ticket:_,typedDuringWait:g,conflictIds:E}=await this._mergeIncoming(e,i,{base:this.lastHtml,baseIdentityMap:this._lastIdentityMap,captureLocal:()=>M({flushUndo:!1}),synthetic:!0,source:"peer",seq:t,etag:n});if(a=d.localDiverged,h=g,ne(),window.scrollTo(l,o),this._setPeerBase(e,i,_),this._applyGen++,this._acceptedSaveTicket>_||(typeof n=="string"&&n?(P(n),c=!0):le()),this.lane==="live"&&!a&&!x()){let{forSave:f,forComparison:u,forDirty:S}=Y({emitForSync:!1,flushUndo:!1});y.setLastSavedBaselines(u,S),this._setDiskBase(f,this._ticket())}this.lane==="saved"&&(this._represented=Object.freeze({bytes:e,etag:n||null,seq:t??null})),document.dispatchEvent(new CustomEvent("clay:sync-applied",{detail:{seq:t,source:this.lane==="saved"?"disk":"peer",lane:this.lane,by:r||null,html:e,report:d,unresolved:this.unresolvedConflicts,conflictIds:E}}))}finally{this._log("applyUpdate - morph complete, resuming mutations"),q.resume(),ae(),await new Promise(d=>setTimeout(d,0)),this.isPaused=!1}c&&y?.conflictResolvedBySync(n),this._saveAfterMerge(a,h)}async _doApplyExternal(e,t,i=null,n=null,r=null){if(ue(e)){await fe();return}this.lane==="live"&&!y&&await R();let l=this._diskBase;this._log(`applyExternal - external disk change (seq=${t})`),this.isPaused=!0;let o=window.scrollX,a=window.scrollY;q.pause(),oe();let h=!1,c=!1,d=!1;try{if(this.lane==="live"&&x()&&this._diskBase===null){if(console.log("[LiveSync] Holding external change: unsaved local edits and no baseline to merge against"),this._setHeld("external",!0,null),r?.startup||r?.versionCheck){clearTimeout(this._holdRetryExt),this._holdRetryExt=setTimeout(()=>{if(this._holdRetryExt=null,this.isDestroyed)return;let S=typeof t=="number"?Math.max(t,this._lastExternalSeq):t;this._retryStartup(0,r,S)},3e3);return}let u=this._saveEpoch;clearTimeout(this._holdRetryExt),this._holdRetryExt=setTimeout(()=>{this._holdRetryExt=null,!(this.isDestroyed||this._pendingExternal!=null)&&(this._pendingExternal={html:e,seq:t,saveEpoch:u,etag:i,by:n},this._scheduleNextFrame())},3e3);return}this._setHeld("external",!1);let{report:_,ticket:g,typedDuringWait:E,conflictIds:f}=await this._mergeIncoming(e,null,{base:this._diskBase,captureLocal:()=>Ne().saveClone,synthetic:!1,source:"disk",seq:t,etag:i,extra:{beforeApply:u=>Ct(u.documentElement,this.lane)}});if(h=_.localDiverged,c=E,ne(),this._setDiskBase(e,g),window.scrollTo(o,a),this._acceptedSaveTicket>g||(typeof i=="string"&&i?(P(i),d=!0):le()),this.lane==="live"&&!h&&!x()){y.setLastSavedBaselines(...At()),y.setUnsavedChanges(!1);let u=M({flushUndo:!1});this._setPeerBase(T(u),this.identity.exportMap(u,D),g),this._applyGen++}this.lane==="saved"&&(this._represented=Object.freeze({bytes:e,etag:i||null,seq:t??null})),document.dispatchEvent(new CustomEvent("clay:sync-applied",{detail:{seq:t,source:"disk",etag:typeof i=="string"&&i?i:null,by:n||null,html:e,report:_,conflictIds:f,unresolved:this.unresolvedConflicts}}))}finally{this._log("applyExternal - morph complete, resuming mutations"),q.resume(),ae(),await new Promise(_=>setTimeout(_,0)),this.isPaused=!1}d&&y?.conflictResolvedBySync(i),this._saveAfterMerge(h,c),l!=null&&j(l)===j(e)&&y&&y.schedulePaintHeal(()=>this._diskBase==null?null:j(this._diskBase))}handleNotification({msgType:e,msg:t,action:i,persistent:n,data:r}){this._log(`Notification received: ${e} - ${t}`),e==="data-loss"||(n&&window.toastPersistent?window.toastPersistent(t,e):window.toast?window.toast(t,e):console.log(`[LiveSync] Notification: ${t}`)),document.dispatchEvent(new CustomEvent("clay:notification",{detail:{msgType:e,msg:t,action:i,persistent:n,data:r}})),this.onNotification&&this.onNotification({msgType:e,msg:t,action:i,persistent:n,data:r})}stop(){this.cleanup(),this.isDestroyed=!0,console.log("[LiveSync] Stopped")}editParticipant(){return R().then(e=>e.syncParticipant(this))}},K=null;Ge((s,e)=>{K=typeof s=="string"?{html:s,captureId:e?.captureId??null}:null});var Mt=s=>K&&(K.captureId??null)===(s??null)?K.html:null;function Pt(s,e){if(typeof e!="string"||e===s)return s;let t=i=>new DOMParser().parseFromString(i,"text/html").documentElement;return t(s).isEqualNode(t(e))?e:s}var Ee=new Se;typeof window<"u"&&!ft&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>Ee.start()):Ee.start());var yn=(s,e,t)=>J().then(({morph:i})=>i(s,e,t));var mn=Ee;export{I as a,_t as b,It as c,Se as d,Ee as e,yn as f,mn as g};
|