@panphora/clayjs 0.2.0 → 0.4.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/README.md +99 -7
- package/clay-internals.js +19 -0
- package/clay.js +18 -2
- package/package.json +19 -3
- package/sap.js +5 -5
- package/src/attrs/onaftersave.js +12 -1
- package/src/attrs/refetch-on-save.js +4 -4
- package/src/attrs/save-freeze.js +47 -19
- package/src/core/admin-contenteditable.js +2 -2
- package/src/core/admin-inputs.js +2 -2
- package/src/core/admin-onclick.js +2 -2
- package/src/core/admin-resources.js +23 -14
- package/src/core/autosave.js +11 -1
- package/src/core/edit-mode.js +2 -2
- package/src/core/host-attrs.js +46 -0
- package/src/core/is-edit-mode.js +14 -6
- package/src/core/persist.js +8 -1
- package/src/core/save-core.js +199 -290
- package/src/core/save.js +124 -84
- package/src/core/snapshot.js +99 -43
- package/src/core/unsaved-warning.js +8 -2
- package/src/dom/all.js +4 -29
- package/src/dom/form-data.js +1 -1
- package/src/dom/nearest.js +11 -9
- package/src/internals/index.js +64 -0
- package/src/lib/cache-bust.js +10 -0
- package/src/lib/cookie.js +50 -15
- package/src/lib/extension-noise.js +5 -1
- package/src/lib/mutation.js +65 -18
- package/src/lib/region-policy.js +17 -2
- package/src/lib/root-attrs.js +45 -0
- package/src/lib/throttle.js +25 -13
- package/src/loader.js +18 -24
- package/src/options/options.js +1 -3
- package/src/plugins/demo.js +7 -1
- package/src/sync/live-sync.js +121 -89
- package/src/ui/dialogs.js +10 -1
- package/src/ui/index.js +2 -11
- package/src/ui/toast.js +8 -2
- package/src/utils/debounce.js +24 -7
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +29 -16
- package/src/vendor/richclay.vendor.js +29 -22
package/src/loader.js
CHANGED
|
@@ -28,16 +28,27 @@ export async function boot(base, params, readyResolve) {
|
|
|
28
28
|
loaded[path] = await import(base + "/src/" + path); // sequential: order is load-bearing
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
assembleCore(loaded, { isEditMode, isOwner }, regionPolicy); // window.clay
|
|
31
|
+
assembleCore(loaded, { isEditMode, isOwner }, regionPolicy); // window.clay MUST be assembled
|
|
32
32
|
// before any plugin import
|
|
33
33
|
|
|
34
34
|
for (const path of plan.plugins) {
|
|
35
|
-
|
|
35
|
+
// A plugin that throws on evaluation degrades to absent. Unguarded, its
|
|
36
|
+
// rejection propagated out of boot() and left clay.ready pending forever, so
|
|
37
|
+
// every `await clay.ready` on the page hung: one broken optional module took
|
|
38
|
+
// the whole client with it. (The allSettled below covers the plugins' async
|
|
39
|
+
// `ready` exports, never their evaluation.)
|
|
40
|
+
let mod;
|
|
41
|
+
try {
|
|
42
|
+
mod = await import(base + "/src/" + path);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(`clayjs: plugin "${path}" failed to load, continuing without it:`, err);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
36
47
|
loaded[path] = mod;
|
|
37
48
|
attachPluginMember(path, mod); // immediately, not after the loop: hypercms's ?cms=true
|
|
38
49
|
// auto-open runs as a microtask queued during ITS evaluation
|
|
39
|
-
// (before boot resumes) and reads
|
|
40
|
-
//
|
|
50
|
+
// (before boot resumes) and reads clay.RichClay and clay.undo,
|
|
51
|
+
// which earlier plugins must have attached by then
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
// Plugins with async setup (sortable's vendor fetch) export `ready`; hold
|
|
@@ -60,6 +71,7 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
|
|
|
60
71
|
isEditMode,
|
|
61
72
|
isOwner,
|
|
62
73
|
Mutation: mutation.default,
|
|
74
|
+
region: regionPolicy.windowRegionShape,
|
|
63
75
|
});
|
|
64
76
|
|
|
65
77
|
const snapshot = loaded["core/snapshot.js"];
|
|
@@ -73,28 +85,12 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
|
|
|
73
85
|
}
|
|
74
86
|
if (snapshot) {
|
|
75
87
|
clay.getHTML = snapshot.getPageContents;
|
|
76
|
-
clay.
|
|
88
|
+
clay.addDocumentTransform = snapshot.addDocumentTransform;
|
|
77
89
|
clay.onSnapshot = snapshot.onSnapshot;
|
|
78
90
|
}
|
|
79
91
|
if (cacheBustMod) {
|
|
80
92
|
clay.cacheBust = cacheBustMod.default;
|
|
81
93
|
}
|
|
82
|
-
|
|
83
|
-
// vendor-compat shim: richclay, hypercms, and hyper-undo read window.hyperclay;
|
|
84
|
-
// remove when the vendors migrate to window.clay.
|
|
85
|
-
window.hyperclay = window.hyperclay || {};
|
|
86
|
-
Object.assign(window.hyperclay, {
|
|
87
|
-
Mutation: clay.Mutation,
|
|
88
|
-
isEditMode: clay.isEditMode,
|
|
89
|
-
region: regionPolicy.windowRegionShape,
|
|
90
|
-
});
|
|
91
|
-
if (clay.beforeSave) {
|
|
92
|
-
window.hyperclay.beforeSave = clay.beforeSave; // richclay's save-cleanup hook
|
|
93
|
-
window.hyperclay.onPrepareForSave = clay.beforeSave; // hypercms uses this name
|
|
94
|
-
}
|
|
95
|
-
if (clay.save) {
|
|
96
|
-
window.hyperclay.savePage = clay.save;
|
|
97
|
-
}
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
function attachPluginMember(path, mod) {
|
|
@@ -102,16 +98,14 @@ function attachPluginMember(path, mod) {
|
|
|
102
98
|
|
|
103
99
|
if (path === "plugins/undo.js") {
|
|
104
100
|
clay.undo = mod.undo || mod.default;
|
|
105
|
-
window.hyperclay.undo = clay.undo;
|
|
106
101
|
} else if (path === "sync/live-sync.js") {
|
|
107
102
|
clay.morph = mod.morph;
|
|
108
103
|
} else if (path === "vendor/hypercms.vendor.js") {
|
|
109
104
|
clay.cms = mod.cms || mod.default;
|
|
110
|
-
window.hyperclay.hypercms = clay.cms;
|
|
111
105
|
} else if (path === "plugins/demo.js") {
|
|
112
106
|
clay.demo = mod.demo;
|
|
113
107
|
} else if (path === "vendor/richclay.vendor.js") {
|
|
114
|
-
|
|
108
|
+
clay.RichClay = mod.RichClay || mod.default;
|
|
115
109
|
}
|
|
116
110
|
}
|
|
117
111
|
|
package/src/options/options.js
CHANGED
|
@@ -240,9 +240,7 @@ const optionVisibility = {
|
|
|
240
240
|
}
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
window.hyperclay = window.hyperclay || {};
|
|
245
|
-
window.hyperclay.optionVisibility = optionVisibility;
|
|
243
|
+
window.clay.optionVisibility = optionVisibility;
|
|
246
244
|
|
|
247
245
|
export default optionVisibility;
|
|
248
246
|
|
package/src/plugins/demo.js
CHANGED
|
@@ -20,12 +20,18 @@
|
|
|
20
20
|
// returning visitors' old saved bodies morph over the new page.
|
|
21
21
|
|
|
22
22
|
import { morph } from "../vendor/hyper-morph.vendor.js";
|
|
23
|
+
import { STRIP_FROM_SAVE } from "../lib/region-policy.js";
|
|
23
24
|
|
|
24
25
|
const KEY = "clay:demo:" +
|
|
25
26
|
(document.documentElement.getAttribute("demo-key") || window.location.pathname);
|
|
26
27
|
const LATENCY_MS = 800;
|
|
28
|
+
// What the demo's fake server must not persist. The region half comes from
|
|
29
|
+
// region-policy's STRIP_FROM_SAVE rather than being re-listed here: this used to
|
|
30
|
+
// name "[save-remove]" alone and so missed the canonical clay="no-save" spelling,
|
|
31
|
+
// which meant the demo saved the very regions no-save promises it never will. The
|
|
32
|
+
// rest is editor chrome the real save lane strips by other means.
|
|
27
33
|
const CHROME_SELECTOR = [
|
|
28
|
-
|
|
34
|
+
STRIP_FROM_SAVE,
|
|
29
35
|
"[data-richclay-toolbar]",
|
|
30
36
|
"[data-richclay-menu]",
|
|
31
37
|
"[data-richclay-dialog]",
|
package/src/sync/live-sync.js
CHANGED
|
@@ -40,6 +40,8 @@ import Mutation from "../lib/mutation.js";
|
|
|
40
40
|
import { isSnapshotRemoved } from "../lib/region-policy.js";
|
|
41
41
|
import { isEditMode } from "../core/is-edit-mode.js";
|
|
42
42
|
import { mergeTagRecognizers } from "./merge-tags.js";
|
|
43
|
+
import { serializeForSync } from '../core/snapshot.js';
|
|
44
|
+
import { isTabLocalRootAttr } from '../lib/root-attrs.js';
|
|
43
45
|
|
|
44
46
|
class LiveSync {
|
|
45
47
|
constructor() {
|
|
@@ -47,8 +49,19 @@ class LiveSync {
|
|
|
47
49
|
this.currentFile = null;
|
|
48
50
|
this.lastHtml = null;
|
|
49
51
|
this.clientId = this.generateClientId();
|
|
52
|
+
|
|
53
|
+
// Per-stream resume id for the htmlclay replay server's wire contract.
|
|
54
|
+
// Regenerated on every start() (see below) so an explicit stop/start is a
|
|
55
|
+
// brand-new stream that never inherits the old baseline. Native
|
|
56
|
+
// EventSource reuses the same URL — and thus this id — on its own
|
|
57
|
+
// automatic reconnect. Deliberately NOT the sender clientId, which is the
|
|
58
|
+
// durable per-tab identity used for echo suppression.
|
|
59
|
+
this.resumeId = null;
|
|
60
|
+
|
|
50
61
|
this.debounceMs = 150;
|
|
51
62
|
this.debounceTimer = null;
|
|
63
|
+
this._sendInFlight = false;
|
|
64
|
+
this._queuedSend = null;
|
|
52
65
|
this.isPaused = false;
|
|
53
66
|
this.isDestroyed = false;
|
|
54
67
|
this.debug = false;
|
|
@@ -87,7 +100,7 @@ class LiveSync {
|
|
|
87
100
|
// against the live elements so that the next save can produce the same
|
|
88
101
|
// identityMap, and afterNodeMorphed transfers IDs from incoming parsed
|
|
89
102
|
// elements onto the live elements they morphed into.
|
|
90
|
-
this.idCounter =
|
|
103
|
+
this.idCounter = 0;
|
|
91
104
|
this.liveWeakMap = new WeakMap();
|
|
92
105
|
|
|
93
106
|
// Callbacks
|
|
@@ -109,28 +122,30 @@ class LiveSync {
|
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
125
|
+
* Mint the per-tab sender identity used for echo suppression. Lives on the
|
|
126
|
+
* instance, never in storage: sessionStorage is COPIED into a duplicated tab,
|
|
127
|
+
* so a persisted id made the duplicate treat the original's edits as its own
|
|
128
|
+
* echoes and the two tabs silently stopped syncing. Instance lifetime is the
|
|
129
|
+
* durability that matters, since EventSource auto-reconnect reuses this same
|
|
130
|
+
* instance.
|
|
114
131
|
*/
|
|
115
132
|
generateClientId() {
|
|
116
|
-
|
|
133
|
+
return Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
|
|
134
|
+
}
|
|
117
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Generate a fresh per-stream resume id (URL-safe, well under the 128-byte
|
|
138
|
+
* wire limit). Minted on every start(), never persisted: an explicit
|
|
139
|
+
* stop/start must not reuse the previous stream's baseline.
|
|
140
|
+
*/
|
|
141
|
+
generateResumeId() {
|
|
118
142
|
try {
|
|
119
|
-
|
|
143
|
+
const bytes = new Uint8Array(16);
|
|
144
|
+
(globalThis.crypto || window.crypto).getRandomValues(bytes);
|
|
145
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
120
146
|
} catch (e) {
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!id) {
|
|
125
|
-
id = Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
|
|
126
|
-
try {
|
|
127
|
-
sessionStorage.setItem('livesync-client-id', id);
|
|
128
|
-
} catch (e) {
|
|
129
|
-
// That's okay
|
|
130
|
-
}
|
|
147
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
131
148
|
}
|
|
132
|
-
|
|
133
|
-
return id;
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
/**
|
|
@@ -153,9 +168,11 @@ class LiveSync {
|
|
|
153
168
|
return;
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
// Reset state for new connection
|
|
171
|
+
// Reset state for new connection. Resetting lastSeenSeq is a new baseline,
|
|
172
|
+
// so mint a fresh resume id — this stream must not resume the previous one.
|
|
157
173
|
this.lastHtml = null;
|
|
158
174
|
this.lastSeenSeq = 0;
|
|
175
|
+
this.resumeId = this.generateResumeId();
|
|
159
176
|
|
|
160
177
|
console.log(`[LiveSync] Starting for: ${this.currentFile} (lane=${this.lane})`);
|
|
161
178
|
this.connect();
|
|
@@ -182,6 +199,7 @@ class LiveSync {
|
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
clearTimeout(this.debounceTimer);
|
|
202
|
+
this._queuedSend = null;
|
|
185
203
|
|
|
186
204
|
// Cancel any pending frame and clear the queue. A morph already in
|
|
187
205
|
// flight cannot be aborted; the isDestroyed check in _runPending guards
|
|
@@ -195,27 +213,8 @@ class LiveSync {
|
|
|
195
213
|
this._pendingIdentityMap = null;
|
|
196
214
|
}
|
|
197
215
|
|
|
198
|
-
_loadIdCounter() {
|
|
199
|
-
try {
|
|
200
|
-
const raw = sessionStorage.getItem('livesync-id-counter');
|
|
201
|
-
const n = parseInt(raw || '0', 10);
|
|
202
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
203
|
-
} catch (e) {
|
|
204
|
-
return 0;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
_persistIdCounter() {
|
|
209
|
-
try {
|
|
210
|
-
sessionStorage.setItem('livesync-id-counter', String(this.idCounter));
|
|
211
|
-
} catch (e) {
|
|
212
|
-
// Storage full / sandboxed — fall back to in-memory only.
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
216
|
_mintId() {
|
|
217
217
|
this.idCounter++;
|
|
218
|
-
this._persistIdCounter();
|
|
219
218
|
return `${this.clientId}:${this.idCounter}`;
|
|
220
219
|
}
|
|
221
220
|
|
|
@@ -361,8 +360,10 @@ class LiveSync {
|
|
|
361
360
|
if (this.isDestroyed) return;
|
|
362
361
|
|
|
363
362
|
const pageUrl = encodeURIComponent(window.location.href);
|
|
364
|
-
const
|
|
365
|
-
|
|
363
|
+
const path = `/_/live-sync/stream?page-url=${pageUrl}&lane=${this.lane}&resume-id=${this.resumeId}`;
|
|
364
|
+
// Resolved against the real origin: a <base href> in the authored document
|
|
365
|
+
// would otherwise point the sync stream at an origin the document chose.
|
|
366
|
+
this.sse = new EventSource(new URL(path, window.location.origin).href);
|
|
366
367
|
|
|
367
368
|
this.sse.onopen = () => {
|
|
368
369
|
console.log('[LiveSync] Connected');
|
|
@@ -372,6 +373,26 @@ class LiveSync {
|
|
|
372
373
|
this.sse.onmessage = (event) => {
|
|
373
374
|
const data = JSON.parse(event.data);
|
|
374
375
|
|
|
376
|
+
// Staleness check + watermark advance run FIRST — before the notification
|
|
377
|
+
// and error branches — against the high-water mark of seqs we've seen
|
|
378
|
+
// (own echoes count too, see below). Notification frames now carry a seq,
|
|
379
|
+
// so a conservative-cursor replay after reconnect must not duplicate a
|
|
380
|
+
// toast or let a later stale data frame apply an older snapshot. `seq` is
|
|
381
|
+
// optional for back-compat with older servers that don't stamp it.
|
|
382
|
+
const { seq } = data;
|
|
383
|
+
if (typeof seq === 'number' && seq <= this.lastSeenSeq) {
|
|
384
|
+
this._log(`Dropping stale message: seq=${seq}, lastSeen=${this.lastSeenSeq}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Advance the watermark before the notification/sender branches so that
|
|
389
|
+
// our own save echoes count toward "seen". Without this, a later
|
|
390
|
+
// buffered/replayed message with a smaller seq could rewind us past our
|
|
391
|
+
// local edit.
|
|
392
|
+
if (typeof seq === 'number') {
|
|
393
|
+
this.lastSeenSeq = seq;
|
|
394
|
+
}
|
|
395
|
+
|
|
375
396
|
// Handle notifications (show toast, don't morph)
|
|
376
397
|
if (data.type === "notification") {
|
|
377
398
|
this.handleNotification(data);
|
|
@@ -385,22 +406,7 @@ class LiveSync {
|
|
|
385
406
|
return;
|
|
386
407
|
}
|
|
387
408
|
|
|
388
|
-
const { html, sender,
|
|
389
|
-
|
|
390
|
-
// Staleness check runs FIRST — compared against the high-water mark of
|
|
391
|
-
// seqs we've seen (own echoes count too, see below). `seq` is optional
|
|
392
|
-
// for back-compat with older server builds that don't stamp it.
|
|
393
|
-
if (typeof seq === 'number' && seq <= this.lastSeenSeq) {
|
|
394
|
-
this._log(`Dropping stale message: seq=${seq}, lastSeen=${this.lastSeenSeq}`);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Advance the watermark before the sender filter so that our own save
|
|
399
|
-
// echoes count toward "seen". Without this, a later buffered/replayed
|
|
400
|
-
// peer message with a smaller seq could rewind us past our local edit.
|
|
401
|
-
if (typeof seq === 'number') {
|
|
402
|
-
this.lastSeenSeq = seq;
|
|
403
|
-
}
|
|
409
|
+
const { html, sender, identityMap } = data;
|
|
404
410
|
|
|
405
411
|
// Ignore own changes — already reflected in the DOM, nothing to morph
|
|
406
412
|
if (sender === this.clientId) {
|
|
@@ -451,7 +457,7 @@ class LiveSync {
|
|
|
451
457
|
// after dispatchEvent returns, so reading outerHTML and walking children
|
|
452
458
|
// must happen now.
|
|
453
459
|
this._log('snapshot-ready received, preparing to send');
|
|
454
|
-
const html = clone
|
|
460
|
+
const html = serializeForSync(clone);
|
|
455
461
|
const identityMap = this._buildIdentityMap(document.documentElement, clone);
|
|
456
462
|
this.sendUpdate(html, identityMap);
|
|
457
463
|
};
|
|
@@ -460,42 +466,67 @@ class LiveSync {
|
|
|
460
466
|
}
|
|
461
467
|
|
|
462
468
|
/**
|
|
463
|
-
* Send full HTML to the server (debounced)
|
|
464
|
-
*
|
|
469
|
+
* Send full HTML to the server (debounced, single-flight).
|
|
470
|
+
*
|
|
471
|
+
* One POST on the wire at a time, with at most one newer payload queued; a
|
|
472
|
+
* fresher snapshot replaces the queued one, because peers only ever need the
|
|
473
|
+
* latest state. Two concurrent fetches could reach the server in either order,
|
|
474
|
+
* and last-write-wins then stored the OLDER snapshot for every peer.
|
|
465
475
|
*/
|
|
466
476
|
sendUpdate(html, identityMap) {
|
|
467
477
|
clearTimeout(this.debounceTimer);
|
|
468
478
|
|
|
469
479
|
this.debounceTimer = setTimeout(() => {
|
|
470
|
-
|
|
471
|
-
if (html === this.lastHtml) {
|
|
472
|
-
this._log('Skipping send - HTML unchanged');
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
this._log(`Sending update (HTML length: ${html.length}, lastHtml length: ${this.lastHtml?.length || 0})`);
|
|
477
|
-
|
|
478
|
-
fetch('/_/live-sync/save', {
|
|
479
|
-
method: 'POST',
|
|
480
|
-
headers: { 'Content-Type': 'application/json', 'Page-URL': window.location.href },
|
|
481
|
-
body: JSON.stringify({
|
|
482
|
-
html: html,
|
|
483
|
-
sender: this.clientId,
|
|
484
|
-
identityMap: identityMap
|
|
485
|
-
})
|
|
486
|
-
}).then(response => {
|
|
487
|
-
if (response.ok) {
|
|
488
|
-
this.lastHtml = html;
|
|
489
|
-
} else {
|
|
490
|
-
console.warn('[LiveSync] Save returned status:', response.status);
|
|
491
|
-
}
|
|
492
|
-
}).catch(err => {
|
|
493
|
-
console.error('[LiveSync] Save failed:', err);
|
|
494
|
-
if (this.onError) this.onError(err);
|
|
495
|
-
});
|
|
480
|
+
this._enqueueSend(html, identityMap);
|
|
496
481
|
}, this.debounceMs);
|
|
497
482
|
}
|
|
498
483
|
|
|
484
|
+
_enqueueSend(html, identityMap) {
|
|
485
|
+
if (this._sendInFlight) {
|
|
486
|
+
this._queuedSend = { html, identityMap };
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this._postUpdate(html, identityMap);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_postUpdate(html, identityMap) {
|
|
493
|
+
// Skip if unchanged
|
|
494
|
+
if (html === this.lastHtml) {
|
|
495
|
+
this._log('Skipping send - HTML unchanged');
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this._log(`Sending update (HTML length: ${html.length}, lastHtml length: ${this.lastHtml?.length || 0})`);
|
|
500
|
+
|
|
501
|
+
this._sendInFlight = true;
|
|
502
|
+
|
|
503
|
+
// Absolute against the real origin, so a <base href> in the page cannot
|
|
504
|
+
// redirect the whole document to an origin the author picked.
|
|
505
|
+
fetch(new URL('/_/live-sync/save', window.location.origin).href, {
|
|
506
|
+
method: 'POST',
|
|
507
|
+
headers: { 'Content-Type': 'application/json', 'Page-URL': window.location.href },
|
|
508
|
+
body: JSON.stringify({
|
|
509
|
+
html: html,
|
|
510
|
+
sender: this.clientId,
|
|
511
|
+
identityMap: identityMap
|
|
512
|
+
})
|
|
513
|
+
}).then(response => {
|
|
514
|
+
if (response.ok) {
|
|
515
|
+
this.lastHtml = html;
|
|
516
|
+
} else {
|
|
517
|
+
console.warn('[LiveSync] Save returned status:', response.status);
|
|
518
|
+
}
|
|
519
|
+
}).catch(err => {
|
|
520
|
+
console.error('[LiveSync] Save failed:', err);
|
|
521
|
+
if (this.onError) this.onError(err);
|
|
522
|
+
}).finally(() => {
|
|
523
|
+
this._sendInFlight = false;
|
|
524
|
+
const queued = this._queuedSend;
|
|
525
|
+
this._queuedSend = null;
|
|
526
|
+
if (queued) this._postUpdate(queued.html, queued.identityMap);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
499
530
|
/**
|
|
500
531
|
* Apply an update received from the server. Morphs the entire document.
|
|
501
532
|
*
|
|
@@ -629,6 +660,11 @@ class LiveSync {
|
|
|
629
660
|
const id = parsedWeakMap.get(newEl);
|
|
630
661
|
if (id) liveWeakMap.set(oldEl, id);
|
|
631
662
|
};
|
|
663
|
+
// A peer may neither write these onto our root nor, by not carrying them,
|
|
664
|
+
// take ours away. Returning false is hyper-morph's veto for both directions
|
|
665
|
+
// (it calls this for updateType 'update' and 'remove' alike).
|
|
666
|
+
const beforeAttributeUpdated = (name, element) =>
|
|
667
|
+
isTabLocalRootAttr(name, element) ? false : undefined;
|
|
632
668
|
|
|
633
669
|
try {
|
|
634
670
|
// Morph entire document. We MUST await — HyperMorph.morph returns a
|
|
@@ -652,7 +688,7 @@ class LiveSync {
|
|
|
652
688
|
mergeTags: mergeTagRecognizers
|
|
653
689
|
},
|
|
654
690
|
key,
|
|
655
|
-
callbacks: { afterNodeMorphed }
|
|
691
|
+
callbacks: { afterNodeMorphed, beforeAttributeUpdated }
|
|
656
692
|
});
|
|
657
693
|
|
|
658
694
|
// Restore viewport. Done after morph so layout has settled.
|
|
@@ -685,10 +721,6 @@ class LiveSync {
|
|
|
685
721
|
document.dispatchEvent(new CustomEvent('clay:sync-applied', {
|
|
686
722
|
detail: { seq }
|
|
687
723
|
}));
|
|
688
|
-
// vendor-compat: hypercms's form panel listens for the legacy name.
|
|
689
|
-
document.dispatchEvent(new CustomEvent('hyperclay:livesync-applied', {
|
|
690
|
-
detail: { seq }
|
|
691
|
-
}));
|
|
692
724
|
} finally {
|
|
693
725
|
this._log('applyUpdate - morph complete, resuming mutations');
|
|
694
726
|
Mutation.resume();
|
package/src/ui/dialogs.js
CHANGED
|
@@ -6,9 +6,18 @@ import copyToClipboard from "../utils/copy-to-clipboard.js";
|
|
|
6
6
|
const CLOSE_BUTTON_SVG = `<svg viewBox="0 0 134 134" fill="none" xmlns="http://www.w3.org/2000/svg"><path class="micromodal__close-bg" d="M132 132.5 1 1.5h131v131Z" /><path class="micromodal__close-x" fill-rule="evenodd" clip-rule="evenodd" d="M0 0h3v1.5h1.5V3H6v1.5h1.5V6H9v1.5h1.5V9H12v1.5h1.5V12H15v1.5h1.5V15H18v1.5h1.5V18H21v1.5h1.5V21H24v1.5h1.5V24H27v1.5h1.5V27H30v1.5h1.5V30H33v1.5h1.5V33H36v1.5h1.5V36H39v1.5h1.5V39H42v1.5h1.5V42H45v1.5h1.5V45H48v1.5h1.5V48H51v1.5h1.5V51H54v1.5h1.5V54H57v1.5h1.5V57H60v1.5h1.5V60H63v1.5h1.5V63H66v1.5h1.5V66H69v1.5h1.5V69H72v1.5h1.5V72H75v1.5h1.5V75H78v1.5h1.5V78H81v1.5h1.5V81H84v1.5h1.5V84H87v1.5h1.5V87H90v1.5h1.5V90H93v1.5h1.5V93H96v1.5h1.5V96H99v1.5h1.5V99h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v3h-3V132H129v-1.5h-1.5V129H126v-1.5h-1.5V126H123v-1.5h-1.5V123H120v-1.5h-1.5V120H117v-1.5h-1.5V117H114v-1.5h-1.5V114H111v-1.5h-1.5V111H108v-1.5h-1.5V108H105v-1.5h-1.5V105H102v-1.5h-1.5V102H99v-1.5h-1.5V99H96v-1.5h-1.5V96H93v-1.5h-1.5V93H90v-1.5h-1.5V90H87v-1.5h-1.5V87H84v-1.5h-1.5V84H81v-1.5h-1.5V81H78v-1.5h-1.5V78H75v-1.5h-1.5V75H72v-1.5h-1.5V72H69v-1.5h-1.5V69H66v-1.5h-1.5V66H63v-1.5h-1.5V63H60v-1.5h-1.5V60H57v-1.5h-1.5V57H54v-1.5h-1.5V54H51v-1.5h-1.5V51H48v-1.5h-1.5V48H45v-1.5h-1.5V45H42v-1.5h-1.5V42H39v-1.5h-1.5V39H36v-1.5h-1.5V36H33v-1.5h-1.5V33H30v-1.5h-1.5V30H27v-1.5h-1.5V27H24v-1.5h-1.5V24H21v-1.5h-1.5V21H18v-1.5h-1.5V18H15v-1.5h-1.5V15H12v-1.5h-1.5V12H9v-1.5H7.5V9H6V7.5H4.5V6H3V4.5H1.5V3H0V0ZM108.8 22h5.2v5.1h-2.6v2.6H109v2.6h-2.6v2.6h-2.6v2.5h-2.6v5.2h2.6V45h2.6v2.6h2.6v2.6h2.5v2.6h2.6V58h-5.1v-2.6h-2.6V53h-2.6v-2.6h-2.6v-2.6h-2.5v-2.6h-5.2v2.6H91v2.6h-2.6v2.6h-2.6v2.5h-2.6V58H78v-5.1h2.6v-2.6H83v-2.6h2.6v-2.6h2.6v-2.5h2.6v-5.2h-2.6V35h-2.6v-2.6h-2.6v-2.6h-2.5v-2.6H78V22h5.2v2.6h2.5V27h2.6v2.6h2.6v2.6h2.5v2.6h5.2v-2.6h2.5v-2.6h2.6v-2.6h2.6v-2.5h2.5V22Z" /></svg>`;
|
|
7
7
|
const CONFIRM_BUTTON_SVG = `<div style="width: 28px;"><svg viewBox="0 0 60 33" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M34.5 0.75H43.5V5.25H49V9.75H54.5V14.25H60V18.75H54.5V23.25H49V27.75H43.5V32.25H34.5V27.75H40V23.25H45.5V18.75H0V14.25H45.5V9.75H40V5.25H34.5V0.75Z" fill="white"/></svg></div>`;
|
|
8
8
|
|
|
9
|
+
// Only for the prompt's default value. promptText and extraContent are markup by
|
|
10
|
+
// design, and callers pass elements through them.
|
|
11
|
+
function escapeAttribute(value) {
|
|
12
|
+
return String(value)
|
|
13
|
+
.replaceAll("&", "&")
|
|
14
|
+
.replaceAll('"', """)
|
|
15
|
+
.replaceAll("<", "<");
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
function createModal(promptText, yesCallback, extraContent = "", includeInput = false, defaultValue = "") {
|
|
10
19
|
const inputHtml = includeInput
|
|
11
|
-
? `<div><input class="micromodal__input" type="text" value="${defaultValue}" required></div>`
|
|
20
|
+
? `<div><input class="micromodal__input" type="text" value="${escapeAttribute(defaultValue)}" required></div>`
|
|
12
21
|
: "";
|
|
13
22
|
|
|
14
23
|
themodal.html = `<div>
|
package/src/ui/index.js
CHANGED
|
@@ -18,19 +18,10 @@ clay.modal = themodal;
|
|
|
18
18
|
if (typeof window.toast === "undefined") window.toast = toast;
|
|
19
19
|
if (typeof window.toastPersistent === "undefined") window.toastPersistent = toastPersistent;
|
|
20
20
|
|
|
21
|
-
// vendor-compat mirror: sap reads window.hyperclay.consent (the legacy name for
|
|
22
|
-
// clay.confirm). Never CREATE window.hyperclay here: sap's mutation bridge treats
|
|
23
|
-
// any truthy window.hyperclay as "a hub is coming" and waits forever for it. Core
|
|
24
|
-
// creates the object (with Mutation) and fires clay:mutation-ready; attach then.
|
|
25
|
-
function mirrorConsent() {
|
|
26
|
-
if (window.hyperclay && !window.hyperclay.consent) window.hyperclay.consent = consent;
|
|
27
|
-
}
|
|
28
|
-
if (window.hyperclay) mirrorConsent();
|
|
29
|
-
else document.addEventListener("clay:mutation-ready", mirrorConsent, { once: true });
|
|
30
|
-
|
|
31
21
|
// Automatic save feedback. clay:save-saving stays deliberately silent (a toast for
|
|
32
22
|
// a sub-second transient is noise). Keep your own toast lib? The events are public.
|
|
33
|
-
document.addEventListener("clay:save-saved", () =>
|
|
23
|
+
document.addEventListener("clay:save-saved", (e) =>
|
|
24
|
+
toast(e.detail?.msg || "Saved", e.detail?.msgType === "warning" ? "warning" : "success"));
|
|
34
25
|
document.addEventListener("clay:save-error", () => toastPersistent("Couldn't save", "error"));
|
|
35
26
|
document.addEventListener("clay:save-offline", () => toastPersistent("Offline, not saved", "warning"));
|
|
36
27
|
|
package/src/ui/toast.js
CHANGED
|
@@ -280,11 +280,14 @@ export function toastCore(message, messageType = "success", config = {}) {
|
|
|
280
280
|
const template = templates.toast[messageType] || templates.toast.success;
|
|
281
281
|
const toastHtml = template
|
|
282
282
|
.replace('{icon}', icon)
|
|
283
|
-
.replace('{message}',
|
|
283
|
+
.replace('{message}', '');
|
|
284
284
|
|
|
285
285
|
const tempDiv = document.createElement('div');
|
|
286
286
|
tempDiv.innerHTML = toastHtml.trim();
|
|
287
287
|
const toastElement = tempDiv.firstElementChild;
|
|
288
|
+
// A message can carry a filename, and a filename is whatever whoever wrote the
|
|
289
|
+
// file chose. Assign it as text so markup in a name can never become markup here.
|
|
290
|
+
toastElement.querySelector('.toast-message, .message').textContent = message;
|
|
288
291
|
|
|
289
292
|
// Add click handler
|
|
290
293
|
toastElement.addEventListener('click', () => {
|
|
@@ -393,11 +396,14 @@ function toastPersistent(message, messageType = "warning") {
|
|
|
393
396
|
const template = templates.toast[messageType] || templates.toast.warning;
|
|
394
397
|
const toastHtml = template
|
|
395
398
|
.replace('{icon}', icon)
|
|
396
|
-
.replace('{message}',
|
|
399
|
+
.replace('{message}', '');
|
|
397
400
|
|
|
398
401
|
const tempDiv = document.createElement('div');
|
|
399
402
|
tempDiv.innerHTML = toastHtml.trim();
|
|
400
403
|
const toastElement = tempDiv.firstElementChild;
|
|
404
|
+
// A message can carry a filename, and a filename is whatever whoever wrote the
|
|
405
|
+
// file chose. Assign it as text so markup in a name can never become markup here.
|
|
406
|
+
toastElement.querySelector('.toast-message, .message').textContent = message;
|
|
401
407
|
toastElement.classList.add('toast-persistent');
|
|
402
408
|
|
|
403
409
|
// Add close button as sibling (not inside message)
|
package/src/utils/debounce.js
CHANGED
|
@@ -1,23 +1,40 @@
|
|
|
1
1
|
// debounce.js
|
|
2
2
|
function debounce(callback, delay) {
|
|
3
3
|
let timeoutId;
|
|
4
|
-
let
|
|
4
|
+
let pending = [];
|
|
5
5
|
|
|
6
6
|
return function (...args) {
|
|
7
7
|
const ctx = this;
|
|
8
8
|
clearTimeout(timeoutId);
|
|
9
9
|
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
pending.push({ resolve, reject });
|
|
12
12
|
|
|
13
13
|
timeoutId = setTimeout(() => {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.then(value => { for (const r of resolvers) r(value); });
|
|
14
|
+
const waiting = pending;
|
|
15
|
+
pending = [];
|
|
16
|
+
settle(callback, ctx, args, waiting);
|
|
18
17
|
}, delay);
|
|
19
18
|
});
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
// Settle every caller piggybacking on this window, from both branches. The
|
|
23
|
+
// callback is invoked synchronously so timing is unchanged; only the failure
|
|
24
|
+
// path is new. Without it a throw escapes into the timer, where no caller is
|
|
25
|
+
// listening, and everyone waiting on that window waits forever.
|
|
26
|
+
function settle(callback, ctx, args, waiting) {
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
result = callback.apply(ctx, args);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
for (const p of waiting) p.reject(error);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
Promise.resolve(result).then(
|
|
35
|
+
value => { for (const p of waiting) p.resolve(value); },
|
|
36
|
+
error => { for (const p of waiting) p.reject(error); },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
export default debounce;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var hyperundo=(()=>{var T=Object.defineProperty;var ee=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var re=Object.prototype.hasOwnProperty;var ne=(e,t)=>{for(var n in t)T(e,n,{get:t[n],enumerable:!0})},oe=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of te(t))!re.call(e,o)&&o!==n&&T(e,o,{get:()=>t[o],enumerable:!(r=ee(t,o))||r.enumerable});return e};var ae=e=>oe(T({},"__esModule",{value:!0}),e);var me={};ne(me,{default:()=>pe,undo:()=>C});function ie(e,t=null){let n=[];if(e.type==="attributes"){let r=e.target,o=e.attributeName,i=e.oldValue,u=r.getAttribute(o);return i==null&&u!=null?n.push({kind:"attr-add",target:r,name:o,newValue:u}):i!=null&&u==null?n.push({kind:"attr-remove",target:r,name:o,oldValue:i}):i!==u&&n.push({kind:"attr-set",target:r,name:o,oldValue:i,newValue:u}),n}if(e.type==="characterData")return n.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),n;if(e.type==="childList"){let r=e.target,o=e.nextSibling,i=t?Array.from(e.addedNodes).filter(d=>!t(d)):Array.from(e.addedNodes),u=t?Array.from(e.removedNodes).filter(d=>!t(d)):Array.from(e.removedNodes);return i.length>0&&n.push({kind:"add",parent:r,nodes:i,before:o}),u.length>0&&n.push({kind:"remove",parent:r,nodes:u,before:o}),n}return n}function P(e,t=null){let n=[],r=new Map;for(let o of e){if(o.type==="attributes"){let i=o.target,u=o.attributeName,d=r.get(i);d||(d=new Map,r.set(i,d)),d.has(u)||(d.set(u,{idx:n.length,oldValue:o.oldValue}),n.push(null));continue}for(let i of ie(o,t))n.push(i)}for(let[o,i]of r)for(let[u,d]of i){let l=d.oldValue,m=o.getAttribute(u),p=null;l===m?p=null:l==null?p={kind:"attr-add",target:o,name:u,newValue:m}:m==null?p={kind:"attr-remove",target:o,name:u,oldValue:l}:p={kind:"attr-set",target:o,name:u,oldValue:l,newValue:m},n[d.idx]=p}return n.filter(o=>o!=null)}function L(e){switch(e.kind){case"attr-set":case"attr-add":e.target.setAttribute(e.name,e.newValue);return;case"attr-remove":e.target.removeAttribute(e.name);return;case"text":e.target.data=e.newValue;return;case"value":e.target[e.prop]=e.newValue;return;case"add":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return;case"remove":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return}}function D(e){switch(e.kind){case"attr-set":e.target.setAttribute(e.name,e.oldValue);return;case"attr-add":e.target.removeAttribute(e.name);return;case"attr-remove":e.target.setAttribute(e.name,e.oldValue);return;case"text":e.target.data=e.oldValue;return;case"value":e.target[e.prop]=e.oldValue;return;case"add":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return;case"remove":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return}}var se=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],ue=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],M=se.join(","),U=new RegExp("^(?:"+ue.join("|")+")(?:-|$)");var de=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function le(){return typeof window<"u"&&window.hyperclay&&window.hyperclay.region&&typeof window.hyperclay.region.resolveRegionPolicy=="function"?window.hyperclay.region.resolveRegionPolicy:null}function ce(e){let t=le();if(t)try{return!t(e).undoable}catch{}let n=e&&e.nodeType!==1?e.parentElement:e;if(n&&n.closest&&n.closest(M))return!0;for(;n&&n.nodeType===1;){for(let r of de)if(n.hasAttribute&&n.hasAttribute(r))return!0;n=n.parentElement}return!1}function R(e,t,n){if(ce(e))return!0;if(n&&n.type==="attributes"){if(n.attributeName&&U.test(n.attributeName.toLowerCase()))return!0;if(t)try{if(t(n.attributeName,n.target))return!0}catch{}}return!1}function X(){let e=new Map;function t(o,i){let u=e.get(o);return u||(u=new Set,e.set(o,u)),u.add(i),()=>n(o,i)}function n(o,i){let u=e.get(o);u&&u.delete(i)}function r(o,i){let u=e.get(o);if(u)for(let d of Array.from(u))try{d(i)}catch(l){console.error("[hyper-undo] listener threw",l)}}return{on:t,off:n,emit:r}}var fe={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function N(e){let t={...fe,...e};if(!t.scope)throw new Error("hyper-undo: scope is required");let n=X(),r=[],o=[],i=[],u=null,d=null,l=0,m=!1;function p(...s){t.debug&&console.log("[hyper-undo]",...s)}function H(){m||(m=!0,d=W(g),d.observe(t.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),p("started"))}function W(s){let c=typeof document<"u"&&t.scope===document.body,f=typeof window<"u"&&window.hyperclay&&window.hyperclay.Mutation||null;return c&&f&&typeof f.createObserver=="function"?(p("sourcing records from window.hyperclay.Mutation (shared observer)"),f.createObserver(s)):new MutationObserver(s)}function E(){m&&(m=!1,d&&(d.disconnect(),d=null),y(),r.length=0,o.length=0,i=[],l=0,p("stopped"))}function V(){let s=t.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function g(s){if(l>0)return;let c=_(s);if(c.length>0){for(let f of c)i.push(f);I()}}function _(s){let c=s.filter(f=>!R(f.target,t.ignoreAttribute,f));return P(c,f=>R(f,t.ignoreAttribute))}function I(){y(),u=setTimeout(K,t.idleWindowMs)}function y(){u!=null&&(clearTimeout(u),u=null)}function K(){if(u=null,i.length===0)return;let s=i;i=[],v(t.idleLabel,s)}function v(s,c){for(r.push({label:s,timestamp:Date.now(),primitives:c}),o.length=0;r.length>t.maxHistory;)r.shift();p("commit",s,"primitives:",c.length),n.emit("commit")}function j(s,c){if(!m)throw new Error("hyper-undo: scope not started");g(d.takeRecords()),w();let f=c();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(g(d.takeRecords()),i.length===0){n.emit("commit");return}y();let b=i;i=[],v(s,b)}function G(s){if(!m)throw new Error("hyper-undo: scope not started");if(!d)return;if(l>1){d.takeRecords();return}w();let c=_(d.takeRecords());c.length!==0&&(y(),v(s,c))}function Z(){d&&d.takeRecords()}function O(s){!m||l>0||!s||(i.push(s),I())}function $(s,{prop:c="value",oldValue:f,newValue:b}={}){!s||f===b||O({kind:"value",target:s,prop:c,oldValue:f,newValue:b})}function w(){l===0&&d&&g(d.takeRecords()),i.length!==0&&(y(),K())}function k(){l===0&&d&&g(d.takeRecords()),l++,p("paused",l)}function x(){l!==0&&(l--,l===0&&d&&d.takeRecords(),p("resumed",l))}function J(){if(V()){E();return}w();let s=r.pop();if(s){k();try{for(let c=s.primitives.length-1;c>=0;c--)try{D(s.primitives[c])}catch(f){p("undo primitive failed (continuing)",f)}}finally{x()}o.push(s),p("undo",s.label),n.emit("undo")}}function Q(){if(V()){E();return}w();let s=o.pop();if(s){k();try{for(let c of s.primitives)try{L(c)}catch(f){p("redo primitive failed (continuing)",f)}}finally{x()}r.push(s),p("redo",s.label),n.emit("redo")}}function Y(){d&&d.takeRecords(),r.length=0,o.length=0,i=[],y(),p("cleared"),n.emit("clear")}return{_config:t,start:H,stop:E,commit:j,commitCaptured:G,discardCaptured:Z,flush:w,record:O,recordValue:$,undo:J,redo:Q,clear:Y,pause:k,resume:x,on:n.on,off:n.off,get canUndo(){return r.length>0},get canRedo(){return o.length>0},get isPaused(){return l>0},get history(){return r.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return t.scope}}}function q(e,t){let n=r=>{let o=e.scope;if(o&&o.nodeType===1&&o.isConnected===!1){t&&t();return}let i=r.target,u=e._config.shadowKeydownIn;if(i&&i.closest&&Array.isArray(u))for(let m of u)try{if(i.closest(m))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let l=(r.key||"").toLowerCase();if(l==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(l==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(l==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",n,!0),()=>window.removeEventListener("keydown",n,!0)}var A=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror"],a=null,S=null,z=null,h=null;function F(e,t){if(h){if(h.inst===e)return h.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let n=q(e,t);return h={inst:e,cleanup:n},n}function B(e){h&&h.inst===e&&(h.cleanup(),h=null)}var C={defaults:{shadowKeydownIn:A},start(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(a){if(S===t)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),a;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A};return a=N(n),a.start(),S=t,e.bindKeys!==!1&&(z=F(a,()=>C.stop())),a},stop(){a&&(B(a),z=null,a.stop(),a=null,S=null)},create(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A},r=N(n),o=null,i={_config:r._config,commit:r.commit,commitCaptured:r.commitCaptured,discardCaptured:r.discardCaptured,flush:r.flush,record:r.record,recordValue:r.recordValue,undo:r.undo,redo:r.redo,clear:r.clear,pause:r.pause,resume:r.resume,on:r.on,off:r.off,get canUndo(){return r.canUndo},get canRedo(){return r.canRedo},get isPaused(){return r.isPaused},get history(){return r.history},get scope(){return r.scope},start(){e.bindKeys&&(o=F(r,()=>i.stop())),r.start()},stop(){B(r),o=null,r.stop()}};return i},commit(e,t){return a?a.commit(e,t):void 0},commitCaptured(e){return a?a.commitCaptured(e):void 0},discardCaptured(){return a?a.discardCaptured():void 0},record(e){return a?a.record(e):void 0},recordValue(e,t){return a?a.recordValue(e,t):void 0},undo(){return a?a.undo():void 0},redo(){return a?a.redo():void 0},clear(){return a?a.clear():void 0},pause(){return a?a.pause():void 0},resume(){return a?a.resume():void 0},flush(){return a?a.flush():void 0},on(e,t){return a?a.on(e,t):void 0},off(e,t){return a?a.off(e,t):void 0},get canUndo(){return a?a.canUndo:!1},get canRedo(){return a?a.canRedo:!1},get history(){return a?a.history:[]},get isPaused(){return a?a.isPaused:!1}};var pe=C;return ae(me);})();
|
|
1
|
+
var hyperundo=(()=>{var T=Object.defineProperty;var ee=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var re=Object.prototype.hasOwnProperty;var ne=(e,t)=>{for(var n in t)T(e,n,{get:t[n],enumerable:!0})},oe=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of te(t))!re.call(e,o)&&o!==n&&T(e,o,{get:()=>t[o],enumerable:!(r=ee(t,o))||r.enumerable});return e};var ae=e=>oe(T({},"__esModule",{value:!0}),e);var me={};ne(me,{default:()=>pe,undo:()=>C});function ie(e,t=null){let n=[];if(e.type==="attributes"){let r=e.target,o=e.attributeName,i=e.oldValue,u=r.getAttribute(o);return i==null&&u!=null?n.push({kind:"attr-add",target:r,name:o,newValue:u}):i!=null&&u==null?n.push({kind:"attr-remove",target:r,name:o,oldValue:i}):i!==u&&n.push({kind:"attr-set",target:r,name:o,oldValue:i,newValue:u}),n}if(e.type==="characterData")return n.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),n;if(e.type==="childList"){let r=e.target,o=e.nextSibling,i=t?Array.from(e.addedNodes).filter(d=>!t(d)):Array.from(e.addedNodes),u=t?Array.from(e.removedNodes).filter(d=>!t(d)):Array.from(e.removedNodes);return i.length>0&&n.push({kind:"add",parent:r,nodes:i,before:o}),u.length>0&&n.push({kind:"remove",parent:r,nodes:u,before:o}),n}return n}function P(e,t=null){let n=[],r=new Map;for(let o of e){if(o.type==="attributes"){let i=o.target,u=o.attributeName,d=r.get(i);d||(d=new Map,r.set(i,d)),d.has(u)||(d.set(u,{idx:n.length,oldValue:o.oldValue}),n.push(null));continue}for(let i of ie(o,t))n.push(i)}for(let[o,i]of r)for(let[u,d]of i){let l=d.oldValue,m=o.getAttribute(u),p=null;l===m?p=null:l==null?p={kind:"attr-add",target:o,name:u,newValue:m}:m==null?p={kind:"attr-remove",target:o,name:u,oldValue:l}:p={kind:"attr-set",target:o,name:u,oldValue:l,newValue:m},n[d.idx]=p}return n.filter(o=>o!=null)}function L(e){switch(e.kind){case"attr-set":case"attr-add":e.target.setAttribute(e.name,e.newValue);return;case"attr-remove":e.target.removeAttribute(e.name);return;case"text":e.target.data=e.newValue;return;case"value":e.target[e.prop]=e.newValue;return;case"add":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return;case"remove":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return}}function D(e){switch(e.kind){case"attr-set":e.target.setAttribute(e.name,e.oldValue);return;case"attr-add":e.target.removeAttribute(e.name);return;case"attr-remove":e.target.setAttribute(e.name,e.oldValue);return;case"text":e.target.data=e.oldValue;return;case"value":e.target[e.prop]=e.oldValue;return;case"add":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return;case"remove":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return}}var se=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],ue=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],M=se.join(","),U=new RegExp("^(?:"+ue.join("|")+")(?:-|$)");var de=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function le(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function ce(e){let t=le();if(t)try{return!t(e).undoable}catch{}let n=e&&e.nodeType!==1?e.parentElement:e;if(n&&n.closest&&n.closest(M))return!0;for(;n&&n.nodeType===1;){for(let r of de)if(n.hasAttribute&&n.hasAttribute(r))return!0;n=n.parentElement}return!1}function R(e,t,n){if(ce(e))return!0;if(n&&n.type==="attributes"){if(n.attributeName&&U.test(n.attributeName.toLowerCase()))return!0;if(t)try{if(t(n.attributeName,n.target))return!0}catch{}}return!1}function X(){let e=new Map;function t(o,i){let u=e.get(o);return u||(u=new Set,e.set(o,u)),u.add(i),()=>n(o,i)}function n(o,i){let u=e.get(o);u&&u.delete(i)}function r(o,i){let u=e.get(o);if(u)for(let d of Array.from(u))try{d(i)}catch(l){console.error("[hyper-undo] listener threw",l)}}return{on:t,off:n,emit:r}}var fe={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function N(e){let t={...fe,...e};if(!t.scope)throw new Error("hyper-undo: scope is required");let n=X(),r=[],o=[],i=[],u=null,d=null,l=0,m=!1;function p(...s){t.debug&&console.log("[hyper-undo]",...s)}function H(){m||(m=!0,d=W(g),d.observe(t.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),p("started"))}function W(s){let c=typeof document<"u"&&t.scope===document.body,f=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return c&&f&&typeof f.createObserver=="function"?(p("sourcing records from the platform Mutation hub (shared observer)"),f.createObserver(s)):new MutationObserver(s)}function E(){m&&(m=!1,d&&(d.disconnect(),d=null),y(),r.length=0,o.length=0,i=[],l=0,p("stopped"))}function V(){let s=t.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function g(s){if(l>0)return;let c=_(s);if(c.length>0){for(let f of c)i.push(f);I()}}function _(s){let c=s.filter(f=>!R(f.target,t.ignoreAttribute,f));return P(c,f=>R(f,t.ignoreAttribute))}function I(){y(),u=setTimeout(K,t.idleWindowMs)}function y(){u!=null&&(clearTimeout(u),u=null)}function K(){if(u=null,i.length===0)return;let s=i;i=[],v(t.idleLabel,s)}function v(s,c){for(r.push({label:s,timestamp:Date.now(),primitives:c}),o.length=0;r.length>t.maxHistory;)r.shift();p("commit",s,"primitives:",c.length),n.emit("commit")}function j(s,c){if(!m)throw new Error("hyper-undo: scope not started");g(d.takeRecords()),w();let f=c();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(g(d.takeRecords()),i.length===0){n.emit("commit");return}y();let b=i;i=[],v(s,b)}function G(s){if(!m)throw new Error("hyper-undo: scope not started");if(!d)return;if(l>1){d.takeRecords();return}w();let c=_(d.takeRecords());c.length!==0&&(y(),v(s,c))}function Z(){d&&d.takeRecords()}function O(s){!m||l>0||!s||(i.push(s),I())}function $(s,{prop:c="value",oldValue:f,newValue:b}={}){!s||f===b||O({kind:"value",target:s,prop:c,oldValue:f,newValue:b})}function w(){l===0&&d&&g(d.takeRecords()),i.length!==0&&(y(),K())}function k(){l===0&&d&&g(d.takeRecords()),l++,p("paused",l)}function x(){l!==0&&(l--,l===0&&d&&d.takeRecords(),p("resumed",l))}function J(){if(V()){E();return}w();let s=r.pop();if(s){k();try{for(let c=s.primitives.length-1;c>=0;c--)try{D(s.primitives[c])}catch(f){p("undo primitive failed (continuing)",f)}}finally{x()}o.push(s),p("undo",s.label),n.emit("undo")}}function Q(){if(V()){E();return}w();let s=o.pop();if(s){k();try{for(let c of s.primitives)try{L(c)}catch(f){p("redo primitive failed (continuing)",f)}}finally{x()}r.push(s),p("redo",s.label),n.emit("redo")}}function Y(){d&&d.takeRecords(),r.length=0,o.length=0,i=[],y(),p("cleared"),n.emit("clear")}return{_config:t,start:H,stop:E,commit:j,commitCaptured:G,discardCaptured:Z,flush:w,record:O,recordValue:$,undo:J,redo:Q,clear:Y,pause:k,resume:x,on:n.on,off:n.off,get canUndo(){return r.length>0},get canRedo(){return o.length>0},get isPaused(){return l>0},get history(){return r.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return t.scope}}}function q(e,t){let n=r=>{let o=e.scope;if(o&&o.nodeType===1&&o.isConnected===!1){t&&t();return}let i=r.target,u=e._config.shadowKeydownIn;if(i&&i.closest&&Array.isArray(u))for(let m of u)try{if(i.closest(m))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let l=(r.key||"").toLowerCase();if(l==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(l==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(l==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",n,!0),()=>window.removeEventListener("keydown",n,!0)}var A=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror"],a=null,S=null,z=null,h=null;function F(e,t){if(h){if(h.inst===e)return h.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let n=q(e,t);return h={inst:e,cleanup:n},n}function B(e){h&&h.inst===e&&(h.cleanup(),h=null)}var C={defaults:{shadowKeydownIn:A},start(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(a){if(S===t)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),a;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A};return a=N(n),a.start(),S=t,e.bindKeys!==!1&&(z=F(a,()=>C.stop())),a},stop(){a&&(B(a),z=null,a.stop(),a=null,S=null)},create(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A},r=N(n),o=null,i={_config:r._config,commit:r.commit,commitCaptured:r.commitCaptured,discardCaptured:r.discardCaptured,flush:r.flush,record:r.record,recordValue:r.recordValue,undo:r.undo,redo:r.redo,clear:r.clear,pause:r.pause,resume:r.resume,on:r.on,off:r.off,get canUndo(){return r.canUndo},get canRedo(){return r.canRedo},get isPaused(){return r.isPaused},get history(){return r.history},get scope(){return r.scope},start(){e.bindKeys&&(o=F(r,()=>i.stop())),r.start()},stop(){B(r),o=null,r.stop()}};return i},commit(e,t){return a?a.commit(e,t):void 0},commitCaptured(e){return a?a.commitCaptured(e):void 0},discardCaptured(){return a?a.discardCaptured():void 0},record(e){return a?a.record(e):void 0},recordValue(e,t){return a?a.recordValue(e,t):void 0},undo(){return a?a.undo():void 0},redo(){return a?a.redo():void 0},clear(){return a?a.clear():void 0},pause(){return a?a.pause():void 0},resume(){return a?a.resume():void 0},flush(){return a?a.flush():void 0},on(e,t){return a?a.on(e,t):void 0},off(e,t){return a?a.off(e,t):void 0},get canUndo(){return a?a.canUndo:!1},get canRedo(){return a?a.canRedo:!1},get history(){return a?a.history:[]},get isPaused(){return a?a.isPaused:!1}};var pe=C;return ae(me);})();
|
|
2
2
|
|
|
3
3
|
// Auto-export to window unless suppressed by loader.
|
|
4
4
|
if (!window.__hyperclayNoAutoExport) {
|