hyperclayjs 1.15.0 → 1.16.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/LICENSE +3 -3
- package/README.md +14 -14
- package/package.json +1 -1
- package/src/communication/live-sync.js +7 -15
- package/src/core/savePage.js +76 -19
- package/src/core/savePageCore.js +25 -3
- package/src/core/snapshot.js +91 -2
- package/src/core/unsavedWarning.js +15 -3
- package/src/custom-attributes/autosize.esm.js +1 -0
- package/src/custom-attributes/autosize.js +15 -12
- package/src/custom-attributes/sortable.js +1 -1
- package/src/hyperclay.js +6 -4
- package/src/ui/theModal.js +1 -1
- package/src/ui/toast.js +2 -2
- package/src/utilities/autosaveDebug.js +223 -0
- package/src/utilities/loadVendorScript.js +4 -4
- package/src/utilities/mutation.js +4 -2
- package/src/vendor/hyper-morph.vendor.js +22 -0
- package/src/vendor/idiomorph.min.js +0 -20
package/LICENSE
CHANGED
|
@@ -26,10 +26,10 @@ SOFTWARE.
|
|
|
26
26
|
|
|
27
27
|
This project includes the following third-party libraries:
|
|
28
28
|
|
|
29
|
-
###
|
|
30
|
-
- Copyright (c)
|
|
29
|
+
### HyperMorph
|
|
30
|
+
- Copyright (c) Hyperclay
|
|
31
31
|
- License: 0BSD (Zero-Clause BSD)
|
|
32
|
-
- https://github.com/
|
|
32
|
+
- https://github.com/hyperclay/hyper-morph
|
|
33
33
|
|
|
34
34
|
### Sortable.js
|
|
35
35
|
- Copyright (c) All contributors to Sortable
|
package/README.md
CHANGED
|
@@ -62,12 +62,12 @@ import 'hyperclayjs/presets/standard.js';
|
|
|
62
62
|
| edit-mode-helpers | 7.5KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
|
|
63
63
|
| option-visibility | 7.8KB | Dynamic show/hide based on ancestor state with option:attribute="value" |
|
|
64
64
|
| persist | 2.4KB | Persist input/select/textarea values to the DOM with [persist] attribute |
|
|
65
|
-
| save-core |
|
|
66
|
-
| save-system |
|
|
65
|
+
| save-core | 7.4KB | Basic save function only - hyperclay.savePage() |
|
|
66
|
+
| save-system | 12.1KB | CMD+S, [trigger-save] button, savestatus attribute |
|
|
67
67
|
| save-toast | 0.9KB | Toast notifications for save events |
|
|
68
|
-
| snapshot |
|
|
68
|
+
| snapshot | 10.2KB | Source of truth for page state - captures DOM snapshots for save and sync |
|
|
69
69
|
| tailwind-inject | 0.4KB | Injects tailwind CSS link with cache-bust on save |
|
|
70
|
-
| unsaved-warning |
|
|
70
|
+
| unsaved-warning | 1.3KB | Warn before leaving page with unsaved changes |
|
|
71
71
|
|
|
72
72
|
### Custom Attributes (HTML enhancements)
|
|
73
73
|
|
|
@@ -76,7 +76,7 @@ import 'hyperclayjs/presets/standard.js';
|
|
|
76
76
|
| ajax-elements | 2.6KB | [ajax-form], [ajax-button] for async form submissions |
|
|
77
77
|
| dom-helpers | 6.2KB | el.nearest, el.val, el.text, el.exec, el.cycle |
|
|
78
78
|
| event-attrs | 4.6KB | [onclickaway], [onclickchildren], [onclone], [onpagemutation], [onrender] |
|
|
79
|
-
| input-helpers |
|
|
79
|
+
| input-helpers | 3.9KB | [prevent-enter], [autosize] for textareas |
|
|
80
80
|
| onaftersave | 1KB | [onaftersave] attribute - run JS when save status changes |
|
|
81
81
|
| sortable | 3.4KB | Drag-drop sorting with [sortable], lazy-loads ~118KB Sortable.js in edit mode |
|
|
82
82
|
|
|
@@ -95,7 +95,7 @@ import 'hyperclayjs/presets/standard.js';
|
|
|
95
95
|
| cache-bust | 0.6KB | Cache-bust href/src attributes |
|
|
96
96
|
| cookie | 1.4KB | Cookie management (often auto-included) |
|
|
97
97
|
| debounce | 0.4KB | Function debouncing |
|
|
98
|
-
| mutation |
|
|
98
|
+
| mutation | 13.1KB | DOM mutation observation (often auto-included) |
|
|
99
99
|
| nearest | 3.4KB | Find nearest elements (often auto-included) |
|
|
100
100
|
| throttle | 0.8KB | Function throttling |
|
|
101
101
|
|
|
@@ -121,28 +121,28 @@ import 'hyperclayjs/presets/standard.js';
|
|
|
121
121
|
| Module | Size | Description |
|
|
122
122
|
|--------|------|-------------|
|
|
123
123
|
| file-upload | 10.7KB | File upload with progress |
|
|
124
|
-
| live-sync | 12.
|
|
124
|
+
| live-sync | 12.5KB | Real-time DOM sync across browsers |
|
|
125
125
|
| send-message | 1.3KB | Message sending utility |
|
|
126
126
|
|
|
127
127
|
### Vendor Libraries (Third-party libraries)
|
|
128
128
|
|
|
129
129
|
| Module | Size | Description |
|
|
130
130
|
|--------|------|-------------|
|
|
131
|
-
|
|
|
131
|
+
| hyper-morph | 16.3KB | DOM morphing with content-based element matching |
|
|
132
132
|
|
|
133
133
|
## Presets
|
|
134
134
|
|
|
135
|
-
### Minimal (~
|
|
135
|
+
### Minimal (~46.4KB)
|
|
136
136
|
Essential features for basic editing
|
|
137
137
|
|
|
138
138
|
**Modules:** `save-core`, `snapshot`, `save-system`, `edit-mode-helpers`, `toast`, `save-toast`, `export-to-window`, `view-mode-excludes-edit-modules`
|
|
139
139
|
|
|
140
|
-
### Standard (~
|
|
140
|
+
### Standard (~68.7KB)
|
|
141
141
|
Standard feature set for most use cases
|
|
142
142
|
|
|
143
143
|
**Modules:** `save-core`, `snapshot`, `save-system`, `unsaved-warning`, `edit-mode-helpers`, `persist`, `option-visibility`, `event-attrs`, `dom-helpers`, `toast`, `save-toast`, `export-to-window`, `view-mode-excludes-edit-modules`
|
|
144
144
|
|
|
145
|
-
### Everything (~
|
|
145
|
+
### Everything (~201.5KB)
|
|
146
146
|
All available features
|
|
147
147
|
|
|
148
148
|
Includes all available modules across all categories.
|
|
@@ -157,9 +157,9 @@ Some modules with large vendor dependencies are **lazy-loaded** to optimize page
|
|
|
157
157
|
|
|
158
158
|
**How it works:**
|
|
159
159
|
- The wrapper module checks if the page is in edit mode (`isEditMode`)
|
|
160
|
-
- If true, it injects a `<script save-
|
|
160
|
+
- If true, it injects a `<script save-remove>` tag that loads the vendor script
|
|
161
161
|
- If false, nothing is loaded - viewers don't download the heavy scripts
|
|
162
|
-
- The `save-
|
|
162
|
+
- The `save-remove` attribute strips the script tag when the page is saved
|
|
163
163
|
|
|
164
164
|
This means:
|
|
165
165
|
- **Editors** get full functionality when needed
|
|
@@ -371,7 +371,7 @@ MIT © Hyperclay
|
|
|
371
371
|
|
|
372
372
|
This project includes the following open-source libraries:
|
|
373
373
|
|
|
374
|
-
- **[
|
|
374
|
+
- **[HyperMorph](https://github.com/hyperclay/hyper-morph)** - DOM morphing with content-based element matching (0BSD)
|
|
375
375
|
- **[Sortable.js](https://github.com/SortableJS/Sortable)** - Drag-and-drop library (MIT)
|
|
376
376
|
|
|
377
377
|
## Links
|
package/package.json
CHANGED
|
@@ -22,14 +22,15 @@
|
|
|
22
22
|
* │
|
|
23
23
|
* ▼
|
|
24
24
|
* ┌─────────────────────────────────────────────────────────┐
|
|
25
|
-
* │ 4. MORPH
|
|
25
|
+
* │ 4. MORPH HyperMorph to update DOM │
|
|
26
26
|
* │ (preserves focus, input values) │
|
|
27
27
|
* └─────────────────────────────────────────────────────────┘
|
|
28
28
|
*
|
|
29
|
-
* DEPENDS ON: Idiomorph (for intelligent DOM morphing)
|
|
30
29
|
* INTEGRATES WITH: snapshot.js (receives snapshot-ready events)
|
|
31
30
|
*/
|
|
32
31
|
|
|
32
|
+
import { HyperMorph } from "../vendor/hyper-morph.vendor.js";
|
|
33
|
+
|
|
33
34
|
class LiveSync {
|
|
34
35
|
constructor() {
|
|
35
36
|
this.sse = null;
|
|
@@ -337,19 +338,10 @@ class LiveSync {
|
|
|
337
338
|
const temp = document.createElement('div');
|
|
338
339
|
temp.innerHTML = bodyHtml;
|
|
339
340
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
morphFn(document.body, temp, {
|
|
345
|
-
morphStyle: 'innerHTML',
|
|
346
|
-
ignoreActiveValue: true
|
|
347
|
-
});
|
|
348
|
-
} else {
|
|
349
|
-
// Fallback to innerHTML
|
|
350
|
-
console.warn('[LiveSync] Idiomorph not available, using innerHTML fallback');
|
|
351
|
-
document.body.innerHTML = bodyHtml;
|
|
352
|
-
}
|
|
341
|
+
HyperMorph.morph(document.body, temp, {
|
|
342
|
+
morphStyle: 'innerHTML',
|
|
343
|
+
ignoreActiveValue: true
|
|
344
|
+
});
|
|
353
345
|
|
|
354
346
|
this.rehydrateFormState(document.body);
|
|
355
347
|
} finally {
|
package/src/core/savePage.js
CHANGED
|
@@ -14,11 +14,14 @@ import throttle from "../utilities/throttle.js";
|
|
|
14
14
|
import Mutation from "../utilities/mutation.js";
|
|
15
15
|
import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
saveHtml,
|
|
18
18
|
getPageContents,
|
|
19
19
|
replacePageWith as replacePageWithCore,
|
|
20
|
-
beforeSave
|
|
20
|
+
beforeSave,
|
|
21
|
+
isSaveInProgress
|
|
21
22
|
} from "./savePageCore.js";
|
|
23
|
+
import { captureForComparison, captureForSaveAndComparison } from "./snapshot.js";
|
|
24
|
+
import { logSaveCheck, logBaseline } from "../utilities/autosaveDebug.js";
|
|
22
25
|
|
|
23
26
|
// Reset savestatus to 'saved' in snapshots (each module cleans up its own attrs)
|
|
24
27
|
beforeSave(clone => {
|
|
@@ -88,6 +91,23 @@ window.addEventListener('online', () => {
|
|
|
88
91
|
}
|
|
89
92
|
});
|
|
90
93
|
|
|
94
|
+
// ============================================
|
|
95
|
+
// POST-SAVE BASELINE RECAPTURE
|
|
96
|
+
// ============================================
|
|
97
|
+
// After a successful save, onaftersave handlers may modify the live DOM
|
|
98
|
+
// (e.g., cacheBust updates ?v= query params). We recapture the baseline
|
|
99
|
+
// after these sync handlers complete to prevent false "unsaved changes" warnings.
|
|
100
|
+
|
|
101
|
+
document.addEventListener('hyperclay:save-saved', () => {
|
|
102
|
+
// Use setTimeout(0) to run after all sync onaftersave handlers complete
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
// Store stripped version so comparisons are direct (no parsing needed)
|
|
105
|
+
const contents = captureForComparison();
|
|
106
|
+
lastSavedContents = contents;
|
|
107
|
+
logBaseline('recaptured after onaftersave', `${contents.length} chars`);
|
|
108
|
+
}, 0);
|
|
109
|
+
});
|
|
110
|
+
|
|
91
111
|
// Re-export from core for backward compatibility
|
|
92
112
|
export { beforeSave, getPageContents };
|
|
93
113
|
|
|
@@ -110,6 +130,11 @@ export function savePage(callback = () => {}) {
|
|
|
110
130
|
return;
|
|
111
131
|
}
|
|
112
132
|
|
|
133
|
+
// Don't start a new save if one is already in progress
|
|
134
|
+
if (isSaveInProgress()) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
113
138
|
// Check if offline - set DOM state immediately for UI feedback
|
|
114
139
|
// but still try the fetch (navigator.onLine can be wrong)
|
|
115
140
|
const wasOffline = !navigator.onLine;
|
|
@@ -117,10 +142,24 @@ export function savePage(callback = () => {}) {
|
|
|
117
142
|
setOfflineStateQuiet();
|
|
118
143
|
}
|
|
119
144
|
|
|
120
|
-
|
|
145
|
+
// Single capture: clone once, get both versions
|
|
146
|
+
// forComparison has [save-remove] and [save-ignore] stripped
|
|
147
|
+
// forSave has only [save-remove] stripped
|
|
148
|
+
let forSave, forComparison;
|
|
149
|
+
try {
|
|
150
|
+
({ forSave, forComparison } = captureForSaveAndComparison());
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error('savePage: captureForSaveAndComparison failed', err);
|
|
153
|
+
setSaveState('error', err.message);
|
|
154
|
+
if (typeof callback === 'function') {
|
|
155
|
+
callback({ msg: err.message, msgType: 'error' });
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
121
159
|
|
|
122
|
-
//
|
|
123
|
-
unsavedChanges = (
|
|
160
|
+
// Compare directly - lastSavedContents is already stripped
|
|
161
|
+
unsavedChanges = (forComparison !== lastSavedContents);
|
|
162
|
+
logSaveCheck('savePage dirty check', !unsavedChanges);
|
|
124
163
|
|
|
125
164
|
// Skip if content hasn't changed
|
|
126
165
|
if (!unsavedChanges) {
|
|
@@ -130,24 +169,29 @@ export function savePage(callback = () => {}) {
|
|
|
130
169
|
// Start debounced 'saving' state (only shows if save takes >500ms)
|
|
131
170
|
setSavingState();
|
|
132
171
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
172
|
+
// Use saveHtml directly with our pre-captured content (avoids double capture)
|
|
173
|
+
saveHtml(forSave, (err, data) => {
|
|
174
|
+
if (!err) {
|
|
175
|
+
// SUCCESS - store stripped version for future comparisons
|
|
176
|
+
lastSavedContents = forComparison;
|
|
137
177
|
unsavedChanges = false;
|
|
138
|
-
setSaveState('saved', msg);
|
|
178
|
+
setSaveState('saved', data?.msg || 'Saved');
|
|
179
|
+
logBaseline('updated after save', `${lastSavedContents.length} chars`);
|
|
139
180
|
} else {
|
|
140
181
|
// FAILED - determine if it's offline or server error
|
|
141
182
|
if (!navigator.onLine) {
|
|
142
|
-
setSaveState('offline',
|
|
183
|
+
setSaveState('offline', err.message);
|
|
143
184
|
} else {
|
|
144
|
-
setSaveState('error',
|
|
185
|
+
setSaveState('error', err.message);
|
|
145
186
|
}
|
|
146
187
|
}
|
|
147
188
|
|
|
148
|
-
// Call user callback if provided
|
|
149
|
-
if (typeof callback === 'function'
|
|
150
|
-
callback({
|
|
189
|
+
// Call user callback if provided (preserve server's msgType)
|
|
190
|
+
if (typeof callback === 'function') {
|
|
191
|
+
callback({
|
|
192
|
+
msg: err?.message || data?.msg,
|
|
193
|
+
msgType: err ? 'error' : (data?.msgType || 'success')
|
|
194
|
+
});
|
|
151
195
|
}
|
|
152
196
|
});
|
|
153
197
|
}
|
|
@@ -207,9 +251,11 @@ function initBaselineCapture() {
|
|
|
207
251
|
|
|
208
252
|
// Take immediate snapshot and set as baseline right away
|
|
209
253
|
// This ensures saves during settle window work correctly
|
|
210
|
-
|
|
254
|
+
// Store stripped version so comparisons are direct (no parsing needed)
|
|
255
|
+
const immediateContents = captureForComparison();
|
|
211
256
|
lastSavedContents = immediateContents;
|
|
212
257
|
baselineContents = immediateContents;
|
|
258
|
+
logBaseline('immediate capture', `${immediateContents.length} chars`);
|
|
213
259
|
|
|
214
260
|
// Track user edits to avoid overwriting real changes
|
|
215
261
|
const userEditEvents = ['input', 'change', 'paste'];
|
|
@@ -235,9 +281,13 @@ function initBaselineCapture() {
|
|
|
235
281
|
// Only update if no user edits AND no saves occurred during settle
|
|
236
282
|
// (if a save happened, lastSavedContents would differ from immediateContents)
|
|
237
283
|
if (!userEdited && lastSavedContents === immediateContents) {
|
|
238
|
-
|
|
284
|
+
// Store stripped version so comparisons are direct (no parsing needed)
|
|
285
|
+
const contents = captureForComparison();
|
|
239
286
|
lastSavedContents = contents;
|
|
240
287
|
baselineContents = contents;
|
|
288
|
+
logBaseline('settled capture', `${contents.length} chars`);
|
|
289
|
+
} else {
|
|
290
|
+
logBaseline('settled skipped', userEdited ? 'user edited' : 'save occurred during settle');
|
|
241
291
|
}
|
|
242
292
|
|
|
243
293
|
document.documentElement.setAttribute('savestatus', 'saved');
|
|
@@ -271,10 +321,17 @@ if (document.readyState === 'loading') {
|
|
|
271
321
|
export function savePageThrottled(callback = () => {}) {
|
|
272
322
|
if (!isEditMode) return;
|
|
273
323
|
|
|
274
|
-
const currentContents = getPageContents();
|
|
275
324
|
// For autosave: check both that content changed from baseline AND from last save
|
|
276
325
|
// This prevents saves from initial setup mutations
|
|
277
|
-
|
|
326
|
+
// Compare directly - stored versions are already stripped
|
|
327
|
+
const currentForCompare = captureForComparison();
|
|
328
|
+
const differsFromBaseline = currentForCompare !== baselineContents;
|
|
329
|
+
const differsFromLastSave = currentForCompare !== lastSavedContents;
|
|
330
|
+
|
|
331
|
+
logSaveCheck('throttled vs baseline', !differsFromBaseline);
|
|
332
|
+
logSaveCheck('throttled vs lastSave', !differsFromLastSave);
|
|
333
|
+
|
|
334
|
+
if (differsFromBaseline && differsFromLastSave) {
|
|
278
335
|
unsavedChanges = true;
|
|
279
336
|
throttledSave(callback);
|
|
280
337
|
}
|
package/src/core/savePageCore.js
CHANGED
|
@@ -26,6 +26,14 @@ import {
|
|
|
26
26
|
let saveInProgress = false;
|
|
27
27
|
const saveEndpoint = `/save/${cookie.get("currentResource")}`;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Check if a save is currently in progress.
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isSaveInProgress() {
|
|
34
|
+
return saveInProgress;
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
// =============================================================================
|
|
30
38
|
// RE-EXPORTS FROM SNAPSHOT (for backwards compat)
|
|
31
39
|
// =============================================================================
|
|
@@ -102,7 +110,7 @@ export function savePage(callback = () => {}) {
|
|
|
102
110
|
|
|
103
111
|
// Add timeout - abort if server doesn't respond within 12 seconds
|
|
104
112
|
const controller = new AbortController();
|
|
105
|
-
const timeoutId = setTimeout(() => controller.abort(
|
|
113
|
+
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
106
114
|
|
|
107
115
|
fetch(saveEndpoint, {
|
|
108
116
|
method: 'POST',
|
|
@@ -175,12 +183,18 @@ export function saveHtml(html, callback = () => {}) {
|
|
|
175
183
|
return;
|
|
176
184
|
}
|
|
177
185
|
|
|
186
|
+
// Add timeout - abort if server doesn't respond within 12 seconds
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
189
|
+
|
|
178
190
|
fetch(saveEndpoint, {
|
|
179
191
|
method: 'POST',
|
|
180
192
|
credentials: 'include',
|
|
181
|
-
body: html
|
|
193
|
+
body: html,
|
|
194
|
+
signal: controller.signal
|
|
182
195
|
})
|
|
183
196
|
.then(res => {
|
|
197
|
+
clearTimeout(timeoutId);
|
|
184
198
|
return res.json().then(data => {
|
|
185
199
|
if (!res.ok) {
|
|
186
200
|
throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
|
|
@@ -194,12 +208,20 @@ export function saveHtml(html, callback = () => {}) {
|
|
|
194
208
|
}
|
|
195
209
|
})
|
|
196
210
|
.catch(err => {
|
|
211
|
+
clearTimeout(timeoutId);
|
|
197
212
|
console.error('Failed to save page:', err);
|
|
213
|
+
|
|
214
|
+
// Normalize timeout errors
|
|
215
|
+
const error = err.name === 'AbortError'
|
|
216
|
+
? new Error('Server not responding')
|
|
217
|
+
: err;
|
|
218
|
+
|
|
198
219
|
if (typeof callback === 'function') {
|
|
199
|
-
callback(
|
|
220
|
+
callback(error);
|
|
200
221
|
}
|
|
201
222
|
})
|
|
202
223
|
.finally(() => {
|
|
224
|
+
clearTimeout(timeoutId);
|
|
203
225
|
saveInProgress = false;
|
|
204
226
|
});
|
|
205
227
|
}
|
package/src/core/snapshot.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* │ 3a. PREPARE HOOKS │ │ 3b. DONE │
|
|
22
22
|
* │ onPrepareForSave │ │ (live-sync stops here) │
|
|
23
23
|
* │ [onbeforesave] │ │ │
|
|
24
|
-
* │ [save-
|
|
24
|
+
* │ [save-remove] │ │ → emits snapshot-ready │
|
|
25
25
|
* │ │ └─────────────────────────┘
|
|
26
26
|
* │ ✓ Used by: SAVE only │
|
|
27
27
|
* └─────────────────────────┘
|
|
@@ -102,7 +102,7 @@ function prepareCloneForSave(clone) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Remove elements that shouldn't be saved
|
|
105
|
-
for (const el of clone.querySelectorAll('[save-
|
|
105
|
+
for (const el of clone.querySelectorAll('[save-remove]')) {
|
|
106
106
|
el.remove();
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -114,6 +114,95 @@ function prepareCloneForSave(clone) {
|
|
|
114
114
|
return "<!DOCTYPE html>" + clone.outerHTML;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Capture snapshot prepared for dirty/change comparison.
|
|
119
|
+
*
|
|
120
|
+
* Like captureForSave but also strips [save-ignore] elements.
|
|
121
|
+
* Use this for comparing current state against baselines.
|
|
122
|
+
*
|
|
123
|
+
* @returns {string} HTML string with [save-remove] and [save-ignore] stripped
|
|
124
|
+
*/
|
|
125
|
+
export function captureForComparison() {
|
|
126
|
+
// CodeMirror pages: return editor content directly (same for save and compare)
|
|
127
|
+
if (isCodeMirrorPage()) {
|
|
128
|
+
return getCodeMirrorContents();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const clone = captureSnapshot();
|
|
132
|
+
|
|
133
|
+
// Run inline [onbeforesave] handlers
|
|
134
|
+
for (const el of clone.querySelectorAll('[onbeforesave]')) {
|
|
135
|
+
new Function(el.getAttribute('onbeforesave')).call(el);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Strip before hooks (hooks see the "final" state)
|
|
139
|
+
for (const el of clone.querySelectorAll('[save-remove], [save-ignore]')) {
|
|
140
|
+
el.remove();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Run registered prepare hooks
|
|
144
|
+
for (const hook of prepareForSaveHooks) {
|
|
145
|
+
hook(clone);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return "<!DOCTYPE html>" + clone.outerHTML;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Single-capture function for both saving and comparison.
|
|
153
|
+
*
|
|
154
|
+
* Clones the DOM once, then clones that clone for comparison.
|
|
155
|
+
* More efficient than calling captureForSave() and captureForComparison() separately.
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} options
|
|
158
|
+
* @param {boolean} options.emitForSync - Whether to emit snapshot-ready event (default: true)
|
|
159
|
+
* @returns {{ forSave: string, forComparison: string }}
|
|
160
|
+
*/
|
|
161
|
+
export function captureForSaveAndComparison({ emitForSync = true } = {}) {
|
|
162
|
+
// CodeMirror pages: return editor content directly, skip snapshot-ready
|
|
163
|
+
if (isCodeMirrorPage()) {
|
|
164
|
+
const contents = getCodeMirrorContents();
|
|
165
|
+
return { forSave: contents, forComparison: contents };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const clone = captureSnapshot();
|
|
169
|
+
|
|
170
|
+
// Emit for live-sync before any stripping
|
|
171
|
+
if (emitForSync) {
|
|
172
|
+
document.dispatchEvent(new CustomEvent('hyperclay:snapshot-ready', {
|
|
173
|
+
detail: { documentElement: clone }
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Run inline [onbeforesave] handlers
|
|
178
|
+
for (const el of clone.querySelectorAll('[onbeforesave]')) {
|
|
179
|
+
new Function(el.getAttribute('onbeforesave')).call(el);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Clone for comparison before stripping (cheaper than cloning live DOM)
|
|
183
|
+
const compareClone = clone.cloneNode(true);
|
|
184
|
+
|
|
185
|
+
// Save clone: strip [save-remove], then run hooks
|
|
186
|
+
for (const el of clone.querySelectorAll('[save-remove]')) {
|
|
187
|
+
el.remove();
|
|
188
|
+
}
|
|
189
|
+
for (const hook of prepareForSaveHooks) {
|
|
190
|
+
hook(clone);
|
|
191
|
+
}
|
|
192
|
+
const forSave = "<!DOCTYPE html>" + clone.outerHTML;
|
|
193
|
+
|
|
194
|
+
// Compare clone: strip both, then run hooks
|
|
195
|
+
for (const el of compareClone.querySelectorAll('[save-remove], [save-ignore]')) {
|
|
196
|
+
el.remove();
|
|
197
|
+
}
|
|
198
|
+
for (const hook of prepareForSaveHooks) {
|
|
199
|
+
hook(compareClone);
|
|
200
|
+
}
|
|
201
|
+
const forComparison = "<!DOCTYPE html>" + compareClone.outerHTML;
|
|
202
|
+
|
|
203
|
+
return { forSave, forComparison };
|
|
204
|
+
}
|
|
205
|
+
|
|
117
206
|
/**
|
|
118
207
|
* PHASE 1-4: Full pipeline for saving to server.
|
|
119
208
|
*
|
|
@@ -7,19 +7,31 @@
|
|
|
7
7
|
* Works independently of autosave - no mutation observer needed during editing,
|
|
8
8
|
* just a single comparison when the user tries to leave.
|
|
9
9
|
*
|
|
10
|
+
* Both current and stored content have [save-remove] and [save-ignore] stripped,
|
|
11
|
+
* so comparison is direct with no parsing needed.
|
|
12
|
+
*
|
|
10
13
|
* Requires the 'save-system' module (automatically included as dependency).
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
import { isOwner, isEditMode } from "./isAdminOfCurrentResource.js";
|
|
14
|
-
import {
|
|
17
|
+
import { captureForComparison } from "./snapshot.js";
|
|
18
|
+
import { getLastSavedContents } from "./savePage.js";
|
|
19
|
+
import { logUnloadDiffSync, preloadIfEnabled } from "../utilities/autosaveDebug.js";
|
|
20
|
+
|
|
21
|
+
// Pre-load diff library if debug mode is on (so it's ready for unload)
|
|
22
|
+
preloadIfEnabled();
|
|
15
23
|
|
|
16
24
|
window.addEventListener('beforeunload', (event) => {
|
|
17
25
|
if (!isOwner || !isEditMode) return;
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
// Compare directly - both are already stripped
|
|
28
|
+
const currentForCompare = captureForComparison();
|
|
20
29
|
const lastSaved = getLastSavedContents();
|
|
21
30
|
|
|
22
|
-
if (
|
|
31
|
+
if (currentForCompare !== lastSaved) {
|
|
32
|
+
// Debug: log what's different before showing the warning
|
|
33
|
+
logUnloadDiffSync(currentForCompare, lastSaved);
|
|
34
|
+
|
|
23
35
|
event.preventDefault();
|
|
24
36
|
event.returnValue = '';
|
|
25
37
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=new Map;function t(t){var o=e.get(t);o&&o.destroy()}function o(t){var o=e.get(t);o&&o.update()}var r=null;"undefined"==typeof window?((r=function(e){return e}).destroy=function(e){return e},r.update=function(e){return e}):((r=function(t,o){return t&&Array.prototype.forEach.call(t.length?t:[t],function(t){return function(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!e.has(t)){var o,r=null,n=window.getComputedStyle(t),i=(o=t.value,function(){a({testForHeightReduction:""===o||!t.value.startsWith(o),restoreTextAlign:null}),o=t.value}),l=function(o){t.removeEventListener("autosize:destroy",l),t.removeEventListener("autosize:update",s),t.removeEventListener("input",i),window.removeEventListener("resize",s),Object.keys(o).forEach(function(e){return t.style[e]=o[e]}),e.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,textAlign:t.style.textAlign,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener("autosize:destroy",l),t.addEventListener("autosize:update",s),t.addEventListener("input",i),window.addEventListener("resize",s),t.style.overflowX="hidden",t.style.wordWrap="break-word",e.set(t,{destroy:l,update:s}),s()}function a(e){var o,i,l=e.restoreTextAlign,s=void 0===l?null:l,d=e.testForHeightReduction,u=void 0===d||d,c=n.overflowY;if(0!==t.scrollHeight&&("vertical"===n.resize?t.style.resize="none":"both"===n.resize&&(t.style.resize="horizontal"),u&&(o=function(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push([e.parentNode,e.parentNode.scrollTop]),e=e.parentNode;return function(){return t.forEach(function(e){var t=e[0],o=e[1];t.style.scrollBehavior="auto",t.scrollTop=o,t.style.scrollBehavior=null})}}(t),t.style.height=""),i="content-box"===n.boxSizing?t.scrollHeight-(parseFloat(n.paddingTop)+parseFloat(n.paddingBottom)):t.scrollHeight+parseFloat(n.borderTopWidth)+parseFloat(n.borderBottomWidth),"none"!==n.maxHeight&&i>parseFloat(n.maxHeight)?("hidden"===n.overflowY&&(t.style.overflow="scroll"),i=parseFloat(n.maxHeight)):"hidden"!==n.overflowY&&(t.style.overflow="hidden"),t.style.height=i+"px",s&&(t.style.textAlign=s),o&&o(),r!==i&&(t.dispatchEvent(new Event("autosize:resized",{bubbles:!0})),r=i),c!==n.overflow&&!s)){var v=n.textAlign;"hidden"===n.overflow&&(t.style.textAlign="start"===v?"end":"start"),a({restoreTextAlign:v,testForHeightReduction:!0})}}function s(){a({testForHeightReduction:!0,restoreTextAlign:null})}}(t)}),t}).destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],t),e},r.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],o),e});var n=r;export default n;
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
target.style.overflowY = 'hidden';
|
|
6
|
-
target.style.height = 'auto';
|
|
7
|
-
target.style.height = target.scrollHeight + 'px';
|
|
8
|
-
}
|
|
9
|
-
});
|
|
1
|
+
import autosize from './autosize.esm.js';
|
|
2
|
+
|
|
3
|
+
function init() {
|
|
4
|
+
document.querySelectorAll('textarea[autosize]').forEach(autosize);
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
const observer = new MutationObserver(mutations => {
|
|
7
|
+
mutations.forEach(mutation => {
|
|
8
|
+
mutation.addedNodes.forEach(node => {
|
|
9
|
+
if (node.nodeType === 1) {
|
|
10
|
+
if (node.matches?.('textarea[autosize]')) autosize(node);
|
|
11
|
+
node.querySelectorAll?.('textarea[autosize]').forEach(autosize);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
});
|
|
14
15
|
});
|
|
16
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
15
17
|
}
|
|
18
|
+
|
|
16
19
|
export { init };
|
|
17
20
|
export default init;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- e.g. <ul sortable onsorted="console.log('Items reordered!')"></ul>
|
|
12
12
|
|
|
13
13
|
This wrapper conditionally loads the full Sortable.js vendor script (~118KB)
|
|
14
|
-
only when in edit mode. The script is injected with save-
|
|
14
|
+
only when in edit mode. The script is injected with save-remove so it's
|
|
15
15
|
stripped from the page before saving.
|
|
16
16
|
|
|
17
17
|
*/
|
package/src/hyperclay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HyperclayJS v1.
|
|
2
|
+
* HyperclayJS v1.16.0 - Minimal Browser-Native Loader
|
|
3
3
|
*
|
|
4
4
|
* Modules auto-init when imported (no separate init call needed).
|
|
5
5
|
* Include `export-to-window` feature to export to window.hyperclay.
|
|
@@ -56,7 +56,7 @@ const MODULE_PATHS = {
|
|
|
56
56
|
"all-js": "./dom-utilities/All.js",
|
|
57
57
|
"style-injection": "./dom-utilities/insertStyleTag.js",
|
|
58
58
|
"form-data": "./dom-utilities/getDataFromForm.js",
|
|
59
|
-
"
|
|
59
|
+
"hyper-morph": "./vendor/hyper-morph.vendor.js",
|
|
60
60
|
"slugify": "./string-utilities/slugify.js",
|
|
61
61
|
"copy-to-clipboard": "./string-utilities/copy-to-clipboard.js",
|
|
62
62
|
"query-params": "./string-utilities/query.js",
|
|
@@ -136,7 +136,7 @@ const PRESETS = {
|
|
|
136
136
|
"all-js",
|
|
137
137
|
"style-injection",
|
|
138
138
|
"form-data",
|
|
139
|
-
"
|
|
139
|
+
"hyper-morph",
|
|
140
140
|
"slugify",
|
|
141
141
|
"copy-to-clipboard",
|
|
142
142
|
"query-params",
|
|
@@ -162,6 +162,7 @@ const EDIT_MODE_ONLY = new Set([
|
|
|
162
162
|
"sortable",
|
|
163
163
|
"onaftersave",
|
|
164
164
|
"cache-bust",
|
|
165
|
+
"hyper-morph",
|
|
165
166
|
"file-upload",
|
|
166
167
|
"live-sync",
|
|
167
168
|
"tailwind-inject"
|
|
@@ -276,7 +277,8 @@ export const All = window.hyperclayModules['all-js']?.All ?? window.hyperclayMod
|
|
|
276
277
|
export const insertStyles = window.hyperclayModules['style-injection']?.insertStyles ?? window.hyperclayModules['style-injection']?.default;
|
|
277
278
|
export const insertStyleTag = window.hyperclayModules['style-injection']?.insertStyleTag ?? window.hyperclayModules['style-injection']?.default;
|
|
278
279
|
export const getDataFromForm = window.hyperclayModules['form-data']?.getDataFromForm ?? window.hyperclayModules['form-data']?.default;
|
|
279
|
-
export const
|
|
280
|
+
export const HyperMorph = window.hyperclayModules['hyper-morph']?.HyperMorph ?? window.hyperclayModules['hyper-morph']?.default;
|
|
281
|
+
export const morph = window.hyperclayModules['hyper-morph']?.morph ?? window.hyperclayModules['hyper-morph']?.default;
|
|
280
282
|
export const slugify = window.hyperclayModules['slugify']?.slugify ?? window.hyperclayModules['slugify']?.default;
|
|
281
283
|
export const copyToClipboard = window.hyperclayModules['copy-to-clipboard']?.copyToClipboard ?? window.hyperclayModules['copy-to-clipboard']?.default;
|
|
282
284
|
export const query = window.hyperclayModules['query-params']?.query ?? window.hyperclayModules['query-params']?.default;
|
package/src/ui/theModal.js
CHANGED
|
@@ -595,7 +595,7 @@ const themodal = (() => {
|
|
|
595
595
|
const themodalMain = {
|
|
596
596
|
isShowing: false,
|
|
597
597
|
open() {
|
|
598
|
-
document.body.insertAdjacentHTML("afterbegin", "<div save-
|
|
598
|
+
document.body.insertAdjacentHTML("afterbegin", "<div save-remove class='micromodal-parent'>" + modalCss + modalHtml + "</div>");
|
|
599
599
|
|
|
600
600
|
const modalOverlayElem = document.querySelector(".micromodal__overlay");
|
|
601
601
|
const modalContentElem = document.querySelector(".micromodal__content");
|
package/src/ui/toast.js
CHANGED
|
@@ -191,7 +191,7 @@ export function injectToastStyles(styles, theme) {
|
|
|
191
191
|
|
|
192
192
|
const styleSheet = document.createElement('style');
|
|
193
193
|
styleSheet.className = `toast-styles-${theme}`;
|
|
194
|
-
styleSheet.setAttribute('save-
|
|
194
|
+
styleSheet.setAttribute('save-remove', '');
|
|
195
195
|
styleSheet.textContent = styles;
|
|
196
196
|
document.head.appendChild(styleSheet);
|
|
197
197
|
|
|
@@ -210,7 +210,7 @@ export function toastCore(message, messageType = "success", config = {}) {
|
|
|
210
210
|
toastContainer = document.createElement('div');
|
|
211
211
|
toastContainer.className = 'toast-container';
|
|
212
212
|
toastContainer.setAttribute('data-toast-theme', theme);
|
|
213
|
-
toastContainer.setAttribute('save-
|
|
213
|
+
toastContainer.setAttribute('save-remove', '');
|
|
214
214
|
document.body.append(toastContainer);
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autosave Debug Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides conditional logging for the autosave system.
|
|
5
|
+
* Enable by setting localStorage.setItem('hyperclay:debug:autosave', 'true')
|
|
6
|
+
*
|
|
7
|
+
* When enabled, dynamically imports a diff library to show exactly
|
|
8
|
+
* what changed between DOM states.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEBUG_KEY = 'hyperclay:debug:autosave';
|
|
12
|
+
|
|
13
|
+
let diffModule = null;
|
|
14
|
+
let diffLoadPromise = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if autosave debug mode is enabled
|
|
18
|
+
*/
|
|
19
|
+
export function isDebugEnabled() {
|
|
20
|
+
try {
|
|
21
|
+
return localStorage.getItem(DEBUG_KEY) === 'true';
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazily load the diff library only when needed
|
|
29
|
+
* Uses esm.sh for zero-install dynamic import
|
|
30
|
+
*/
|
|
31
|
+
async function loadDiff() {
|
|
32
|
+
if (diffModule) return diffModule;
|
|
33
|
+
if (diffLoadPromise) return diffLoadPromise;
|
|
34
|
+
|
|
35
|
+
diffLoadPromise = import('https://esm.sh/diff@5.2.0')
|
|
36
|
+
.then(mod => {
|
|
37
|
+
diffModule = mod;
|
|
38
|
+
return mod;
|
|
39
|
+
})
|
|
40
|
+
.catch(err => {
|
|
41
|
+
console.warn('[autosave-debug] Failed to load diff library:', err);
|
|
42
|
+
return null;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return diffLoadPromise;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Log a brief one-liner for save operations
|
|
50
|
+
*/
|
|
51
|
+
export function logSaveCheck(label, matches) {
|
|
52
|
+
if (!isDebugEnabled()) return;
|
|
53
|
+
|
|
54
|
+
const status = matches ? '✓ matches' : '✗ differs';
|
|
55
|
+
console.log(`[autosave] ${label}: ${status}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Log when baseline is captured/updated
|
|
60
|
+
*/
|
|
61
|
+
export function logBaseline(event, details = '') {
|
|
62
|
+
if (!isDebugEnabled()) return;
|
|
63
|
+
|
|
64
|
+
console.log(`[autosave] baseline ${event}${details ? `: ${details}` : ''}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Log and diff two HTML strings (async, for unload warning)
|
|
69
|
+
* Returns a promise that resolves when logging is complete
|
|
70
|
+
*/
|
|
71
|
+
export async function logUnloadDiff(currentContents, lastSaved) {
|
|
72
|
+
if (!isDebugEnabled()) return;
|
|
73
|
+
|
|
74
|
+
console.group('[autosave] Unload warning triggered - content differs from last save');
|
|
75
|
+
console.log('Current length:', currentContents.length);
|
|
76
|
+
console.log('Last saved length:', lastSaved.length);
|
|
77
|
+
console.log('Difference:', currentContents.length - lastSaved.length, 'characters');
|
|
78
|
+
|
|
79
|
+
const diff = await loadDiff();
|
|
80
|
+
|
|
81
|
+
if (diff) {
|
|
82
|
+
const changes = diff.diffLines(lastSaved, currentContents);
|
|
83
|
+
|
|
84
|
+
console.log('\n--- DIFF (last saved → current) ---');
|
|
85
|
+
|
|
86
|
+
let hasChanges = false;
|
|
87
|
+
changes.forEach(part => {
|
|
88
|
+
if (part.added || part.removed) {
|
|
89
|
+
hasChanges = true;
|
|
90
|
+
const prefix = part.added ? '+++ ' : '--- ';
|
|
91
|
+
const lines = part.value.split('\n').filter(l => l.length > 0);
|
|
92
|
+
|
|
93
|
+
// Truncate very long diffs
|
|
94
|
+
const maxLines = 20;
|
|
95
|
+
const displayLines = lines.slice(0, maxLines);
|
|
96
|
+
|
|
97
|
+
displayLines.forEach(line => {
|
|
98
|
+
// Truncate very long lines
|
|
99
|
+
const displayLine = line.length > 200
|
|
100
|
+
? line.substring(0, 200) + '...[truncated]'
|
|
101
|
+
: line;
|
|
102
|
+
console.log(prefix + displayLine);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (lines.length > maxLines) {
|
|
106
|
+
console.log(`... and ${lines.length - maxLines} more lines`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!hasChanges) {
|
|
112
|
+
console.log('(No line-level differences found - may be whitespace or inline changes)');
|
|
113
|
+
// Fall back to character diff for small differences
|
|
114
|
+
if (Math.abs(currentContents.length - lastSaved.length) < 500) {
|
|
115
|
+
const charChanges = diff.diffChars(lastSaved, currentContents);
|
|
116
|
+
console.log('\n--- CHARACTER DIFF ---');
|
|
117
|
+
charChanges.forEach(part => {
|
|
118
|
+
if (part.added || part.removed) {
|
|
119
|
+
const prefix = part.added ? '+++ ' : '--- ';
|
|
120
|
+
const value = part.value.length > 100
|
|
121
|
+
? part.value.substring(0, 100) + '...[truncated]'
|
|
122
|
+
: part.value;
|
|
123
|
+
console.log(prefix + JSON.stringify(value));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
console.log('(diff library not available - showing raw comparison)');
|
|
130
|
+
console.log('First 500 chars of current:', currentContents.substring(0, 500));
|
|
131
|
+
console.log('First 500 chars of last saved:', lastSaved.substring(0, 500));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.groupEnd();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Synchronous version for beforeunload (can't await in event handler)
|
|
139
|
+
* Logs what it can immediately, diff loads async but may not complete before unload
|
|
140
|
+
*/
|
|
141
|
+
export function logUnloadDiffSync(currentContents, lastSaved) {
|
|
142
|
+
if (!isDebugEnabled()) return;
|
|
143
|
+
|
|
144
|
+
console.group('[autosave] Unload warning triggered - content differs from last save');
|
|
145
|
+
console.log('Current length:', currentContents.length);
|
|
146
|
+
console.log('Last saved length:', lastSaved.length);
|
|
147
|
+
console.log('Difference:', currentContents.length - lastSaved.length, 'characters');
|
|
148
|
+
|
|
149
|
+
// If diff is already loaded, use it synchronously
|
|
150
|
+
if (diffModule) {
|
|
151
|
+
const changes = diffModule.diffLines(lastSaved, currentContents);
|
|
152
|
+
|
|
153
|
+
console.log('\n--- DIFF (last saved → current) ---');
|
|
154
|
+
|
|
155
|
+
let hasChanges = false;
|
|
156
|
+
changes.forEach(part => {
|
|
157
|
+
if (part.added || part.removed) {
|
|
158
|
+
hasChanges = true;
|
|
159
|
+
const prefix = part.added ? '+++ ' : '--- ';
|
|
160
|
+
const lines = part.value.split('\n').filter(l => l.length > 0);
|
|
161
|
+
|
|
162
|
+
const maxLines = 20;
|
|
163
|
+
const displayLines = lines.slice(0, maxLines);
|
|
164
|
+
|
|
165
|
+
displayLines.forEach(line => {
|
|
166
|
+
const displayLine = line.length > 200
|
|
167
|
+
? line.substring(0, 200) + '...[truncated]'
|
|
168
|
+
: line;
|
|
169
|
+
console.log(prefix + displayLine);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (lines.length > maxLines) {
|
|
173
|
+
console.log(`... and ${lines.length - maxLines} more lines`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!hasChanges) {
|
|
179
|
+
console.log('(No line-level differences - checking character diff)');
|
|
180
|
+
if (Math.abs(currentContents.length - lastSaved.length) < 500) {
|
|
181
|
+
const charChanges = diffModule.diffChars(lastSaved, currentContents);
|
|
182
|
+
charChanges.forEach(part => {
|
|
183
|
+
if (part.added || part.removed) {
|
|
184
|
+
const prefix = part.added ? '+++ ' : '--- ';
|
|
185
|
+
const value = part.value.length > 100
|
|
186
|
+
? part.value.substring(0, 100) + '...[truncated]'
|
|
187
|
+
: part.value;
|
|
188
|
+
console.log(prefix + JSON.stringify(value));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Kick off async load for next time, show basic info now
|
|
195
|
+
loadDiff();
|
|
196
|
+
console.log('(diff library loading - run localStorage.setItem("hyperclay:debug:autosave", "true") and reload for full diff)');
|
|
197
|
+
|
|
198
|
+
// Find first difference position
|
|
199
|
+
let diffPos = 0;
|
|
200
|
+
const minLen = Math.min(currentContents.length, lastSaved.length);
|
|
201
|
+
while (diffPos < minLen && currentContents[diffPos] === lastSaved[diffPos]) {
|
|
202
|
+
diffPos++;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log('First difference at position:', diffPos);
|
|
206
|
+
console.log('Context around difference:');
|
|
207
|
+
console.log(' Last saved:', JSON.stringify(lastSaved.substring(Math.max(0, diffPos - 50), diffPos + 100)));
|
|
208
|
+
console.log(' Current:', JSON.stringify(currentContents.substring(Math.max(0, diffPos - 50), diffPos + 100)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.groupEnd();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Pre-load the diff library if debug is enabled
|
|
216
|
+
* Call this early to ensure diff is ready for unload events
|
|
217
|
+
*/
|
|
218
|
+
export function preloadIfEnabled() {
|
|
219
|
+
if (isDebugEnabled()) {
|
|
220
|
+
loadDiff();
|
|
221
|
+
console.log('[autosave-debug] Debug mode enabled - diff library loading');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
Lazy-load vendor scripts in edit mode only
|
|
3
3
|
|
|
4
|
-
Injects a <script save-
|
|
5
|
-
The save-
|
|
4
|
+
Injects a <script save-remove> tag that loads the vendor script.
|
|
5
|
+
The save-remove attribute ensures it's stripped when the page is saved.
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
8
|
import { loadVendorScript } from '../utilities/loadVendorScript.js';
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Load a vendor script via script tag with save-
|
|
18
|
+
* Load a vendor script via script tag with save-remove
|
|
19
19
|
* @param {string} url - URL to the vendor script
|
|
20
20
|
* @param {Object} [options] - Options
|
|
21
21
|
* @param {string} [options.globalName] - Window property to return when loaded (for classic scripts)
|
|
@@ -33,7 +33,7 @@ export function loadVendorScript(url, options = {}) {
|
|
|
33
33
|
return new Promise((resolve, reject) => {
|
|
34
34
|
const script = document.createElement('script');
|
|
35
35
|
script.src = url;
|
|
36
|
-
script.setAttribute('save-
|
|
36
|
+
script.setAttribute('save-remove', '');
|
|
37
37
|
if (isModule) {
|
|
38
38
|
script.type = 'module';
|
|
39
39
|
}
|
|
@@ -175,7 +175,9 @@ const Mutation = {
|
|
|
175
175
|
|
|
176
176
|
_shouldIgnore(element) {
|
|
177
177
|
while (element && element.nodeType === 1) {
|
|
178
|
-
if (element.hasAttribute?.('mutations-ignore') ||
|
|
178
|
+
if (element.hasAttribute?.('mutations-ignore') ||
|
|
179
|
+
element.hasAttribute?.('save-remove') ||
|
|
180
|
+
element.hasAttribute?.('save-ignore')) {
|
|
179
181
|
return true;
|
|
180
182
|
}
|
|
181
183
|
element = element.parentElement;
|
|
@@ -244,7 +246,7 @@ const Mutation = {
|
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
for (const node of mutation.removedNodes) {
|
|
247
|
-
if (node.nodeType === 1 && !node.hasAttribute?.('save-ignore') && !node.hasAttribute?.('mutations-ignore')) {
|
|
249
|
+
if (node.nodeType === 1 && !node.hasAttribute?.('save-remove') && !node.hasAttribute?.('save-ignore') && !node.hasAttribute?.('mutations-ignore')) {
|
|
248
250
|
const removedNodes = [node, ...node.querySelectorAll('*')];
|
|
249
251
|
this._log(`Processing ${removedNodes.length} removed nodes`, { removedNodes });
|
|
250
252
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
var HyperMorph=(()=>{var q=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var Y=(s,a)=>{for(var u in a)q(s,u,{get:a[u],enumerable:!0})},J=(s,a,u,d)=>{if(a&&typeof a=="object"||typeof a=="function")for(let f of U(a))!G.call(s,f)&&f!==u&&q(s,f,{get:()=>a[f],enumerable:!(d=z(a,f))||d.enumerable});return s};var K=s=>J(q({},"__esModule",{value:!0}),s);var he={};Y(he,{HyperMorph:()=>V,default:()=>fe,defaults:()=>de,morph:()=>ce});var B={includeClasses:!0,includeAttributes:["href","src","name","type","role","aria-label","alt","title"],excludeAttributePrefixes:["data-morph-","data-hyper-","data-im-"],textHintLength:64,excludeIds:!0,maxPathDepth:4,landmarks:["HEADER","NAV","MAIN","ASIDE","FOOTER","SECTION","ARTICLE"],weights:{signature:100,pathSegment:10,textMatch:20,textMismatch:25,uniqueCandidate:50,positionPenalty:1},minConfidence:101};function Q(s){let a=5381;for(let u=0;u<s.length;u++)a=(a<<5)+a^s.charCodeAt(u);return Math.abs(a).toString(36)}function X(s){if(s.classList&&s.classList.length>0)return Array.from(s.classList).sort().join(" ");let a=s.getAttribute?.("class");return a?a.split(/\s+/).filter(Boolean).sort().join(" "):""}function Z(s,a){let u=[];for(let d of s.attributes||[]){let f=d.name;f==="id"||f==="class"||a.excludeAttributePrefixes.some(y=>f.startsWith(y))||a.includeAttributes.includes(f)&&u.push(`${f}=${d.value}`)}return u.sort().join("|")}function ee(s,a){return(s.textContent||"").replace(/\s+/g," ").trim().slice(0,a.textHintLength)}function te(s,a){let u=[s.tagName];return a.includeClasses&&u.push(X(s)),u.push(Z(s,a)),Q(u.join("|"))}function ne(s){let a=s.tagName,u=1,d=s.previousElementSibling;for(;d;)d.tagName===a&&u++,d=d.previousElementSibling;return u}function re(s,a){return s.id||s.getAttribute?.("role")?!0:a.landmarks.includes(s.tagName)}function se(s){if(s.id)return`#${s.id}`;let a=s.getAttribute?.("role");return a?`@${a}`:s.tagName}function ie(s,a){let u=[],d=s;for(;d&&d.tagName&&u.length<a.maxPathDepth;){let f=`${d.tagName}:${ne(d)}`;if(u.unshift(f),d!==s&&re(d,a)){u.unshift(se(d));break}d=d.parentElement}return u}function ae(s,a){let u=0,d=s.length-1,f=a.length-1;for(;d>=0&&f>=0&&s[d]===a[f];)u++,d--,f--;return u}function R(s,a,u){if(u.has(s))return u.get(s);let d={signature:te(s,a),path:ie(s,a),textHint:ee(s,a)};return u.set(s,d),d}function P(s,a,u,d){if(d.has(s))return d.get(s);let f=new Map,y=s.querySelectorAll("*"),H=0;for(let b of y){let k=R(b,a,u);k.domIndex=H++,f.has(k.signature)||f.set(k.signature,[]),f.get(k.signature).push(b)}return d.set(s,f),f}function oe(s,a,u){u.delete(s),a.delete(s);let d=s.querySelectorAll("*");for(let f of d)a.delete(f)}function F(s,a,u,d,f){let y=R(s,u,d),H=R(a,u,d),b=u.weights,k={},L=0;if(y.signature!==H.signature)return{score:0,breakdown:{rejected:"signature mismatch"}};L+=b.signature,k.signature=b.signature;let I=ae(y.path,H.path)*b.pathSegment;L+=I,k.path=I;let E=!0;if(y.textHint&&H.textHint?y.textHint===H.textHint?(L+=b.textMatch,k.text=b.textMatch):(L-=b.textMismatch,k.text=-b.textMismatch,E=!1):y.textHint!==H.textHint&&(L-=b.textMismatch,k.text=-b.textMismatch,E=!1),f.candidateCount===1&&E&&(L+=b.uniqueCandidate,k.unique=b.uniqueCandidate),typeof y.domIndex=="number"&&typeof H.domIndex=="number"){let x=Math.abs(y.domIndex-H.domIndex),c=Math.min(x*b.positionPenalty,20);L-=c,k.drift=-c}return{score:L,breakdown:k}}function D(s,a,u,d,f){if(u.excludeIds&&s.id)return null;let y=P(a,u,d,f),H=R(s,u,d);if(typeof H.domIndex!="number"){let E=0,x=s.previousElementSibling;for(;x;)E++,x=x.previousElementSibling;H.domIndex=E}let b=y.get(H.signature)||[],k=u.excludeIds?b.filter(E=>!E.id):b;if(k.length===0)return null;let L=null,N=0,I=null;for(let E of k){let{score:x,breakdown:c}=F(s,E,u,d,{candidateCount:k.length});x>N&&(N=x,L=E,I=c)}return N<u.minConfidence?null:{element:L,confidence:N,breakdown:I}}function $(s,a,u,d,f){let y=a.querySelectorAll("*"),H=P(s,u,d,f),b=0;for(let I of y){let E=R(I,u,d);E.domIndex=b++}let k=[];for(let I of y){if(u.excludeIds&&I.id)continue;let E=R(I,u,d),x=H.get(E.signature)||[],c=u.excludeIds?x.filter(S=>!S.id):x;for(let S of c){let{score:p,breakdown:M}=F(I,S,u,d,{candidateCount:c.length});p>=u.minConfidence&&k.push({newEl:I,oldEl:S,score:p,breakdown:M})}}k.sort((I,E)=>E.score-I.score);let L=new Map,N=new Set;for(let{newEl:I,oldEl:E}of k)L.has(I)||N.has(E)||(L.set(I,E),N.add(E));return L}function W(s,a,u,d){let f=R(s,u,d),y=R(a,u,d),{score:H,breakdown:b}=F(s,a,u,d,{candidateCount:1});return{matches:H>=u.minConfidence,score:H,breakdown:b,newMeta:{signature:f.signature,path:f.path,textHint:f.textHint},oldMeta:{signature:y.signature,path:y.path,textHint:y.textHint}}}function C(s={}){let a={...B,...s,weights:{...B.weights,...s.weights}},u=new WeakMap,d=new WeakMap;return{findMatch:(f,y)=>D(f,y,a,u,d),computeMatches:(f,y)=>$(f,y,a,u,d),explain:(f,y)=>W(f,y,a,u),invalidate:f=>oe(f,u,d),session:()=>{let f=new WeakMap,y=new WeakMap;return{findMatch:(H,b)=>D(H,b,a,f,y),computeMatches:(H,b)=>$(H,b,a,f,y),explain:(H,b)=>W(H,b,a,f)}},getConfig:()=>({...a})}}var ue={createMatcher:C,DEFAULT_CONFIG:B};typeof exports<"u"&&(typeof module<"u"&&module.exports&&(module.exports=ue),exports.createMatcher=C,exports.DEFAULT_CONFIG=B);var le=C(),V=(function(){"use strict";let s=()=>{},a={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:s,afterNodeAdded:s,beforeNodeMorphed:s,afterNodeMorphed:s,beforeNodeRemoved:s,afterNodeRemoved:s,beforeAttributeUpdated:s},head:{style:"merge",shouldPreserve:c=>c.getAttribute("im-preserve")==="true",shouldReAppend:c=>c.getAttribute("im-re-append")==="true",shouldRemove:s,afterHeadMorphed:s},scripts:{handle:!1,shouldPreserve:c=>c.getAttribute("im-preserve")==="true",shouldReAppend:c=>c.getAttribute("im-re-append")==="true",shouldRemove:s,afterScriptsHandled:s},restoreFocus:!0},u={computeMatches(c,S){let{computeMatches:p}=le.session();return p(c,S)}};function d(c,S,p={}){c=E(c);let M=x(S),v=I(c,M,p),m=new Set(Array.from(c.querySelectorAll("script")).map(e=>e.outerHTML)),n=y(v,()=>k(v,c,M,e=>e.morphStyle==="innerHTML"?(H(e,c,M),Array.from(c.childNodes)):f(e,c,M)));v.pantry.remove();let o=N(c,m,v);return o.length>0?n instanceof Promise?n.then(e=>Promise.all(o).then(()=>e)):Promise.all(o).then(()=>n):n}function f(c,S,p){let M=x(S);return H(c,M,p,S,S.nextSibling),Array.from(M.childNodes)}function y(c,S){if(!c.config.restoreFocus)return S();let p=document.activeElement;if(!(p instanceof HTMLInputElement||p instanceof HTMLTextAreaElement))return S();let{id:M,selectionStart:v,selectionEnd:m}=p,n=S();return M&&M!==document.activeElement?.getAttribute("id")&&(p=c.target.querySelector(`[id="${M}"]`),p?.focus()),p&&!p.selectionEnd&&m!=null&&p.setSelectionRange(v,m),n}let H=(function(){function c(e,t,i,r=null,l=null){t instanceof HTMLTemplateElement&&i instanceof HTMLTemplateElement&&(t=t.content,i=i.content),r||=t.firstChild;for(let h of i.childNodes){if(r&&r!=l){let A=p(e,h,r,l);if(A){A!==r&&v(e,r,A),b(A,h,e),r=A.nextSibling;continue}}if(h instanceof Element){let A=h.getAttribute("id");if(e.persistentIds.has(A)){let T=m(t,A,r,e);b(T,h,e),r=T.nextSibling;continue}if(!e.idMap.has(h)){let T=e.hyperMatches.get(h);if(T&&!e.idMap.has(T)){o(t,T,r),b(T,h,e),r=T.nextSibling;continue}}}let g=S(t,h,r,e);g&&(r=g.nextSibling)}for(;r&&r!=l;){let h=r;r=r.nextSibling,M(e,h)}}function S(e,t,i,r){if(r.callbacks.beforeNodeAdded(t)===!1)return null;if(r.idMap.has(t)){let l=document.createElement(t.tagName);return e.insertBefore(l,i),b(l,t,r),r.callbacks.afterNodeAdded(l),l}else{let l=document.importNode(t,!0);return e.insertBefore(l,i),r.callbacks.afterNodeAdded(l),l}}let p=(function(){function e(r,l,h,g){let A=l instanceof Element&&!r.idMap.has(l)?r.hyperMatches.get(l):null,T=null,O=l.nextSibling,j=0,w=h;for(;w&&w!=g;){if(i(w,l)){if(t(r,w,l)||w===A&&!r.idMap.has(w))return w;if(T===null){let _=w instanceof Element&&r.hyperMatchedOldElements.has(w);!r.idMap.has(w)&&!_&&(T=w)}}if(T===null&&O&&i(w,O)&&(j++,O=O.nextSibling,j>=2&&(T=void 0)),r.activeElementAndParents.includes(w))break;w=w.nextSibling}return T||null}function t(r,l,h){let g=r.idMap.get(l),A=r.idMap.get(h);if(!A||!g)return!1;for(let T of g)if(A.has(T))return!0;return!1}function i(r,l){let h=r,g=l;return h.nodeType===g.nodeType&&h.tagName===g.tagName&&(!h.getAttribute?.("id")||h.getAttribute?.("id")===g.getAttribute?.("id"))}return e})();function M(e,t){let i=t instanceof Element&&e.hyperMatchedOldElements.has(t)&&!e.idMap.has(t);if(e.idMap.has(t)||i)o(e.pantry,t,null);else{if(e.callbacks.beforeNodeRemoved(t)===!1)return;t.parentNode?.removeChild(t),e.callbacks.afterNodeRemoved(t)}}function v(e,t,i){let r=t;for(;r&&r!==i;){let l=r;r=r.nextSibling,M(e,l)}return r}function m(e,t,i,r){let l=r.target.getAttribute?.("id")===t&&r.target||r.target.querySelector(`[id="${t}"]`)||r.pantry.querySelector(`[id="${t}"]`);return n(l,r),o(e,l,i),l}function n(e,t){let i=e.getAttribute("id");for(;e=e.parentNode;){let r=t.idMap.get(e);r&&(r.delete(i),r.size||t.idMap.delete(e))}}function o(e,t,i){if(e.moveBefore)try{e.moveBefore(t,i)}catch{e.insertBefore(t,i)}else e.insertBefore(t,i)}return c})(),b=(function(){function c(n,o,e){return e.ignoreActive&&n===document.activeElement?null:(e.callbacks.beforeNodeMorphed(n,o)===!1||(n instanceof HTMLHeadElement&&e.head.ignore||(n instanceof HTMLHeadElement&&e.head.style!=="morph"?L(n,o,e):(S(n,o,e),m(n,e)||H(e,n,o))),e.callbacks.afterNodeMorphed(n,o)),n)}function S(n,o,e){let t=o.nodeType;if(t===1){let i=n,r=o,l=i.attributes,h=r.attributes;for(let g of h)v(g.name,i,"update",e)||i.getAttribute(g.name)!==g.value&&i.setAttribute(g.name,g.value);for(let g=l.length-1;0<=g;g--){let A=l[g];if(A&&!r.hasAttribute(A.name)){if(v(A.name,i,"remove",e))continue;i.removeAttribute(A.name)}}m(i,e)||p(i,r,e)}(t===8||t===3)&&n.nodeValue!==o.nodeValue&&(n.nodeValue=o.nodeValue)}function p(n,o,e){if(n instanceof HTMLInputElement&&o instanceof HTMLInputElement&&o.type!=="file"){let t=o.value,i=n.value;M(n,o,"checked",e),M(n,o,"disabled",e),o.hasAttribute("value")?i!==t&&(v("value",n,"update",e)||(n.setAttribute("value",t),n.value=t)):v("value",n,"remove",e)||(n.value="",n.removeAttribute("value"))}else if(n instanceof HTMLOptionElement&&o instanceof HTMLOptionElement)M(n,o,"selected",e);else if(n instanceof HTMLTextAreaElement&&o instanceof HTMLTextAreaElement){let t=o.value,i=n.value;if(v("value",n,"update",e))return;t!==i&&(n.value=t),n.firstChild&&n.firstChild.nodeValue!==t&&(n.firstChild.nodeValue=t)}}function M(n,o,e,t){let i=o[e],r=n[e];if(i!==r){let l=v(e,n,"update",t);l||(n[e]=o[e]),i?l||n.setAttribute(e,""):v(e,n,"remove",t)||n.removeAttribute(e)}}function v(n,o,e,t){return n==="value"&&t.ignoreActiveValue&&o===document.activeElement?!0:t.callbacks.beforeAttributeUpdated(n,o,e)===!1}function m(n,o){return!!o.ignoreActiveValue&&n===document.activeElement&&n!==document.body}return c})();function k(c,S,p,M){if(c.head.block){let v=S.querySelector("head"),m=p.querySelector("head");if(v&&m){let n=L(v,m,c);return Promise.all(n).then(()=>{let o=Object.assign(c,{head:{block:!1,ignore:!0}});return M(o)})}}return M(c)}function L(c,S,p){let M=[],v=[],m=[],n=[],o=new Map;for(let t of S.children)o.set(t.outerHTML,t);for(let t of c.children){let i=o.has(t.outerHTML),r=p.head.shouldReAppend(t),l=p.head.shouldPreserve(t);i||l?r?v.push(t):(o.delete(t.outerHTML),m.push(t)):p.head.style==="append"?r&&(v.push(t),n.push(t)):p.head.shouldRemove(t)!==!1&&v.push(t)}n.push(...o.values());let e=[];for(let t of n){let i=document.createRange().createContextualFragment(t.outerHTML).firstChild;if(p.callbacks.beforeNodeAdded(i)!==!1){if("href"in i&&i.href||"src"in i&&i.src){let r,l=new Promise(function(h){r=h});i.addEventListener("load",function(){r()}),e.push(l)}c.appendChild(i),p.callbacks.afterNodeAdded(i),M.push(i)}}for(let t of v)p.callbacks.beforeNodeRemoved(t)!==!1&&(c.removeChild(t),p.callbacks.afterNodeRemoved(t));return p.head.afterHeadMorphed(c,{added:M,kept:m,removed:v}),e}function N(c,S,p){if(!p.scripts.handle)return[];let M=[],v=[],m=[],n=[],o=Array.from(c.querySelectorAll("script"));for(let t of o){let i=t.outerHTML,r=S.has(i),l=p.scripts.shouldPreserve(t),h=p.scripts.shouldReAppend(t);r||l?h?(v.push(t),n.push(t)):m.push(t):n.push(t)}for(let t of S){let i=o.some(r=>r.outerHTML===t)}let e=[];for(let t of n){if(p.callbacks.beforeNodeAdded(t)===!1)continue;let i=document.createRange().createContextualFragment(t.outerHTML).firstChild;if(i.src){let r,l=new Promise(function(h){r=h});i.addEventListener("load",function(){r()}),i.addEventListener("error",function(){r()}),e.push(l)}t.replaceWith(i),p.callbacks.afterNodeAdded(i),M.push(i)}return p.scripts.afterScriptsHandled(c,{added:M,kept:m,removed:v}),e}let I=(function(){function c(e,t,i){let{persistentIds:r,idMap:l}=n(e,t),h=u.computeMatches(e,t),g=new Set;for(let O of h.values())g.add(O);let A=S(i),T=A.morphStyle||"outerHTML";if(!["innerHTML","outerHTML"].includes(T))throw`Do not understand how to morph style ${T}`;return{target:e,newContent:t,config:A,morphStyle:T,ignoreActive:A.ignoreActive,ignoreActiveValue:A.ignoreActiveValue,restoreFocus:A.restoreFocus,idMap:l,persistentIds:r,hyperMatches:h,hyperMatchedOldElements:g,pantry:p(),activeElementAndParents:M(e),callbacks:A.callbacks,head:A.head,scripts:A.scripts}}function S(e){let t=Object.assign({},a);return Object.assign(t,e),t.callbacks=Object.assign({},a.callbacks,e.callbacks),t.head=Object.assign({},a.head,e.head),t.scripts=Object.assign({},a.scripts,e.scripts),t}function p(){let e=document.createElement("div");return e.hidden=!0,document.body.insertAdjacentElement("afterend",e),e}function M(e){let t=[],i=document.activeElement;if(i?.tagName!=="BODY"&&e.contains(i))for(;i&&(t.push(i),i!==e);)i=i.parentElement;return t}function v(e){let t=Array.from(e.querySelectorAll("[id]"));return e.getAttribute?.("id")&&t.push(e),t}function m(e,t,i,r){for(let l of r){let h=l.getAttribute("id");if(t.has(h)){let g=l;for(;g;){let A=e.get(g);if(A==null&&(A=new Set,e.set(g,A)),A.add(h),g===i)break;g=g.parentElement}}}}function n(e,t){let i=v(e),r=v(t),l=o(i,r),h=new Map;m(h,l,e,i);let g=t.__hyperMorphRoot||t;return m(h,l,g,r),{persistentIds:l,idMap:h}}function o(e,t){let i=new Set,r=new Map;for(let{id:h,tagName:g}of e)r.has(h)?i.add(h):r.set(h,g);let l=new Set;for(let{id:h,tagName:g}of t)l.has(h)?i.add(h):r.get(h)===g&&l.add(h);for(let h of i)l.delete(h);return l}return c})(),{normalizeElement:E,normalizeParent:x}=(function(){let c=new WeakSet;function S(m){return m instanceof Document?m.documentElement:m}function p(m){if(m==null)return document.createElement("div");if(typeof m=="string")return p(v(m));if(c.has(m))return m;if(m instanceof Node){if(m.parentNode)return new M(m);{let n=document.createElement("div");return n.append(m),n}}else{let n=document.createElement("div");for(let o of[...m])n.append(o);return n}}class M{constructor(n){this.originalNode=n,this.realParentNode=n.parentNode,this.previousSibling=n.previousSibling,this.nextSibling=n.nextSibling}get childNodes(){let n=[],o=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;for(;o&&o!=this.nextSibling;)n.push(o),o=o.nextSibling;return n}querySelectorAll(n){return this.childNodes.reduce((o,e)=>{if(e instanceof Element){e.matches(n)&&o.push(e);let t=e.querySelectorAll(n);for(let i=0;i<t.length;i++)o.push(t[i])}return o},[])}insertBefore(n,o){return this.realParentNode.insertBefore(n,o)}moveBefore(n,o){return this.realParentNode.moveBefore(n,o)}get __hyperMorphRoot(){return this.originalNode}}function v(m){let n=new DOMParser,o=m.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(o.match(/<\/html>/)||o.match(/<\/head>/)||o.match(/<\/body>/)){let e=n.parseFromString(m,"text/html");if(o.match(/<\/html>/))return c.add(e),e;{let t=e.firstChild;return t&&c.add(t),t}}else{let t=n.parseFromString("<body><template>"+m+"</template></body>","text/html").body.querySelector("template").content;return c.add(t),t}}return{normalizeElement:S,normalizeParent:p}})();return{morph:d,defaults:a}})();var ce=V.morph,de=V.defaults,fe=V;return K(he);})();
|
|
2
|
+
|
|
3
|
+
// Convenience morph wrapper with data-id support
|
|
4
|
+
var morph = function(oldEl, newEl, options = {}) {
|
|
5
|
+
return HyperMorph.morph(oldEl, newEl, {
|
|
6
|
+
key: (el) => el.getAttribute('data-id') || el.id,
|
|
7
|
+
...options
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Auto-export to window unless suppressed by loader
|
|
12
|
+
if (!window.__hyperclayNoAutoExport) {
|
|
13
|
+
window.hyperclay = window.hyperclay || {};
|
|
14
|
+
window.hyperclay.HyperMorph = HyperMorph;
|
|
15
|
+
window.hyperclay.morph = morph;
|
|
16
|
+
window.HyperMorph = HyperMorph;
|
|
17
|
+
window.morph = morph;
|
|
18
|
+
window.h = window.hyperclay;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { HyperMorph, morph };
|
|
22
|
+
export default HyperMorph;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
var Idiomorph=function(){"use strict";let o=new Set;let n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:t,afterNodeAdded:t,beforeNodeMorphed:t,afterNodeMorphed:t,beforeNodeRemoved:t,afterNodeRemoved:t,beforeAttributeUpdated:t},head:{style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:t,afterHeadMorphed:t}};function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=k(t)}let l=y(t);let r=p(e,l,n);return a(e,l,r)}function a(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=c(n,t,o);Promise.all(e).then(function(){a(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=M(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=d(r,e,o);if(e){return N(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function u(e,t){return t.ignoreActiveValue&&e===document.activeElement}function d(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return e;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!g(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return e;if(n.callbacks.beforeNodeAdded(t)===false)return e;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return e;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){c(t,e,n)}else{r(t,e,n);if(!u(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let a;while(i){a=i;i=a.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(a)===false)return;l.appendChild(a);r.callbacks.afterNodeAdded(a);H(r,a);continue}if(b(a,o,r)){d(o,a,r);o=o.nextSibling;H(r,a);continue}let e=A(n,l,a,o,r);if(e){o=v(o,e,r);d(e,a,r);H(r,a);continue}let t=S(n,l,a,o,r);if(t){o=v(o,t,r);d(t,a,r);H(r,a);continue}if(r.callbacks.beforeNodeAdded(a)===false)return;l.insertBefore(a,o);r.callbacks.afterNodeAdded(a);H(r,a)}while(o!==null){let e=o;o=o.nextSibling;T(e,r)}}function f(e,t,n,l){if(e==="value"&&l.ignoreActiveValue&&t===document.activeElement){return true}return l.callbacks.beforeAttributeUpdated(e,t,n)===false}function r(t,n,l){let e=t.nodeType;if(e===1){const r=t.attributes;const i=n.attributes;for(const o of r){if(f(o.name,n,"update",l)){continue}if(n.getAttribute(o.name)!==o.value){n.setAttribute(o.name,o.value)}}for(let e=i.length-1;0<=e;e--){const a=i[e];if(f(a.name,n,"remove",l)){continue}if(!t.hasAttribute(a.name)){n.removeAttribute(a.name)}}}if(e===8||e===3){if(n.nodeValue!==t.nodeValue){n.nodeValue=t.nodeValue}}if(!u(n,l)){s(t,n,l)}}function i(t,n,l,r){if(t[l]!==n[l]){let e=f(l,n,"update",r);if(!e){n[l]=t[l]}if(t[l]){if(!e){n.setAttribute(l,t[l])}}else{if(!f(l,n,"remove",r)){n.removeAttribute(l)}}}}function s(n,l,r){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){let e=n.value;let t=l.value;i(n,l,"checked",r);i(n,l,"disabled",r);if(!n.hasAttribute("value")){if(!f("value",l,"remove",r)){l.value="";l.removeAttribute("value")}}else if(e!==t){if(!f("value",l,"update",r)){l.setAttribute("value",e);l.value=e}}}else if(n instanceof HTMLOptionElement){i(n,l,"selected",r)}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(f("value",l,"update",r)){return}if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function c(e,t,l){let r=[];let i=[];let o=[];let a=[];let u=l.head.style;let d=new Map;for(const n of e.children){d.set(n.outerHTML,n)}for(const s of t.children){let e=d.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{d.delete(s.outerHTML);o.push(s)}}else{if(u==="append"){if(t){i.push(s);a.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}a.push(...d.values());m("to append: ",a);let f=[];for(const c of a){m("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;m(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});f.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return f}function m(){}function t(){}function h(e){let t={};Object.assign(t,n);Object.assign(t,e);t.callbacks={};Object.assign(t.callbacks,n.callbacks);Object.assign(t.callbacks,e.callbacks);t.head={};Object.assign(t.head,n.head);Object.assign(t.head,e.head);return t}function p(e,t,n){n=h(n);return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:C(e,t),deadIds:new Set,callbacks:n.callbacks,head:n.head}}function b(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return L(n,e,t)>0}}return false}function g(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function v(t,e,n){while(t!==e){let e=t;t=t.nextSibling;T(e,n)}H(n,e);return e.nextSibling}function A(n,e,l,r,i){let o=L(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(b(l,e,i)){return e}t+=L(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function S(e,t,n,l,r){let i=l;let o=n.nextSibling;let a=0;while(i!=null){if(L(r,i,e)>0){return null}if(g(n,i)){return i}if(g(o,i)){a++;o=o.nextSibling;if(a>=2){return null}}i=i.nextSibling}return i}function k(n){let l=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function y(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function N(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function M(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=w(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function w(e,t,n){if(g(e,t)){return.5+L(n,e,t)}return 0}function T(e,t){H(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function E(e,t){return!e.deadIds.has(t)}function x(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function H(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function L(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(E(e,i)&&x(e,i,n)){++r}}return r}function R(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function C(e,t){let n=new Map;R(e,n);R(t,n);return n}return{morph:e,defaults:n}}();
|
|
2
|
-
// MODIFIED, added `morph`
|
|
3
|
-
var morph = function(oldEl, newEl, options = {}) {
|
|
4
|
-
return Idiomorph.morph(oldEl, newEl, {
|
|
5
|
-
key: (el) => el.getAttribute('data-id') || el.id,
|
|
6
|
-
...options
|
|
7
|
-
});
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
// Auto-export to window unless suppressed by loader
|
|
11
|
-
if (!window.__hyperclayNoAutoExport) {
|
|
12
|
-
window.hyperclay = window.hyperclay || {};
|
|
13
|
-
window.hyperclay.Idiomorph = Idiomorph;
|
|
14
|
-
window.hyperclay.morph = morph;
|
|
15
|
-
window.morph = morph;
|
|
16
|
-
window.h = window.hyperclay;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { Idiomorph, morph };
|
|
20
|
-
export default Idiomorph;
|