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 CHANGED
@@ -26,10 +26,10 @@ SOFTWARE.
26
26
 
27
27
  This project includes the following third-party libraries:
28
28
 
29
- ### Idiomorph
30
- - Copyright (c) Big Sky Software
29
+ ### HyperMorph
30
+ - Copyright (c) Hyperclay
31
31
  - License: 0BSD (Zero-Clause BSD)
32
- - https://github.com/bigskysoftware/idiomorph
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 | 6.8KB | Basic save function only - hyperclay.savePage() |
66
- | save-system | 9.6KB | CMD+S, [trigger-save] button, savestatus attribute |
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 | 7.5KB | Source of truth for page state - captures DOM snapshots for save and sync |
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 | 0.8KB | Warn before leaving page with unsaved changes |
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 | 1.2KB | [prevent-enter], [autosize] for textareas |
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 | 13KB | DOM mutation observation (often auto-included) |
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.7KB | Real-time DOM sync across browsers |
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
- | idiomorph | 8.3KB | Efficient DOM morphing library |
131
+ | hyper-morph | 16.3KB | DOM morphing with content-based element matching |
132
132
 
133
133
  ## Presets
134
134
 
135
- ### Minimal (~40.6KB)
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 (~62.4KB)
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 (~184.6KB)
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-ignore>` tag that loads the vendor script
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-ignore` attribute strips the script tag when the page is saved
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
- - **[Idiomorph](https://github.com/bigskysoftware/idiomorph)** - DOM morphing library by Big Sky Software (0BSD)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Modular JavaScript library for building interactive HTML applications with Hyperclay",
5
5
  "type": "module",
6
6
  "main": "src/hyperclay.js",
@@ -22,14 +22,15 @@
22
22
  * │
23
23
  * ▼
24
24
  * ┌─────────────────────────────────────────────────────────┐
25
- * │ 4. MORPH Idiomorph to update DOM │
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
- // Use Idiomorph if available
341
- const morphFn = window.Idiomorph?.morph;
342
-
343
- if (morphFn) {
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 {
@@ -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
- savePage as savePageCore,
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
- const currentContents = getPageContents();
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
- // Track whether there are unsaved changes
123
- unsavedChanges = (currentContents !== lastSavedContents);
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
- savePageCore(({msg, msgType}) => {
134
- if (msgType !== 'error') {
135
- // SUCCESS
136
- lastSavedContents = currentContents;
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', msg);
183
+ setSaveState('offline', err.message);
143
184
  } else {
144
- setSaveState('error', msg);
185
+ setSaveState('error', err.message);
145
186
  }
146
187
  }
147
188
 
148
- // Call user callback if provided
149
- if (typeof callback === 'function' && msg) {
150
- callback({msg, msgType});
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
- const immediateContents = getPageContents();
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
- const contents = getPageContents();
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
- if (currentContents !== baselineContents && currentContents !== lastSavedContents) {
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
  }
@@ -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('Save timeout'), 12000);
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(err);
220
+ callback(error);
200
221
  }
201
222
  })
202
223
  .finally(() => {
224
+ clearTimeout(timeoutId);
203
225
  saveInProgress = false;
204
226
  });
205
227
  }
@@ -21,7 +21,7 @@
21
21
  * │ 3a. PREPARE HOOKS │ │ 3b. DONE │
22
22
  * │ onPrepareForSave │ │ (live-sync stops here) │
23
23
  * │ [onbeforesave] │ │ │
24
- * │ [save-ignore] │ │ → emits snapshot-ready │
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-ignore]')) {
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 { getPageContents, getLastSavedContents } from "./savePage.js";
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
- const currentContents = getPageContents();
27
+ // Compare directly - both are already stripped
28
+ const currentForCompare = captureForComparison();
20
29
  const lastSaved = getLastSavedContents();
21
30
 
22
- if (currentContents !== lastSaved) {
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
- function init () {
2
- document.addEventListener('input', event => {
3
- const target = event.target;
4
- if (target.matches('textarea[autosize]')) {
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
- document.querySelectorAll('textarea[autosize]').forEach(textarea => {
12
- textarea.style.overflowY = 'hidden';
13
- textarea.style.height = textarea.scrollHeight + 'px';
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-ignore so it's
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.15.0 - Minimal Browser-Native Loader
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
- "idiomorph": "./vendor/idiomorph.min.js",
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
- "idiomorph",
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 Idiomorph = window.hyperclayModules['idiomorph']?.Idiomorph ?? window.hyperclayModules['idiomorph']?.default;
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;
@@ -595,7 +595,7 @@ const themodal = (() => {
595
595
  const themodalMain = {
596
596
  isShowing: false,
597
597
  open() {
598
- document.body.insertAdjacentHTML("afterbegin", "<div save-ignore class='micromodal-parent'>" + modalCss + modalHtml + "</div>");
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-ignore', '');
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-ignore', '');
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-ignore> tag that loads the vendor script.
5
- The save-ignore attribute ensures it's stripped when the page is saved.
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-ignore
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-ignore', '');
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') || element.hasAttribute?.('save-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;