hyperclayjs 1.7.0 → 1.9.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.
Files changed (67) hide show
  1. package/README.md +25 -21
  2. package/package.json +17 -25
  3. package/src/communication/live-sync.js +396 -0
  4. package/{communication → src/communication}/sendMessage.js +2 -8
  5. package/src/core/adminContenteditable.js +51 -0
  6. package/{core → src/core}/adminInputs.js +29 -8
  7. package/src/core/adminOnClick.js +54 -0
  8. package/{core → src/core}/adminResources.js +25 -5
  9. package/{core → src/core}/autosave.js +6 -17
  10. package/src/core/enablePersistentFormInputValues.js +67 -0
  11. package/{core → src/core}/optionVisibility.js +7 -35
  12. package/{core → src/core}/savePage.js +1 -1
  13. package/src/core/savePageCore.js +256 -0
  14. package/src/core/snapshot.js +203 -0
  15. package/src/core/unsavedWarning.js +26 -0
  16. package/{custom-attributes → src/custom-attributes}/ajaxElements.js +3 -10
  17. package/{custom-attributes → src/custom-attributes}/domHelpers.js +17 -4
  18. package/{custom-attributes → src/custom-attributes}/events.js +2 -0
  19. package/{custom-attributes → src/custom-attributes}/onaftersave.js +5 -8
  20. package/src/custom-attributes/onmutation.js +90 -0
  21. package/src/custom-attributes/onpagemutation.js +32 -0
  22. package/{custom-attributes → src/custom-attributes}/sortable.js +16 -1
  23. package/{dom-utilities → src/dom-utilities}/All.js +22 -0
  24. package/src/dom-utilities/insertStyleTag.js +61 -0
  25. package/{hyperclay.js → src/hyperclay.js} +20 -3
  26. package/{module-dependency-graph.json → src/module-dependency-graph.json} +121 -34
  27. package/{ui → src/ui}/prompts.js +13 -18
  28. package/{ui → src/ui}/theModal.js +103 -0
  29. package/{ui → src/ui}/toast.js +4 -3
  30. package/src/utilities/cacheBust.js +19 -0
  31. package/{vendor → src/vendor}/idiomorph.min.js +1 -0
  32. package/core/adminContenteditable.js +0 -36
  33. package/core/adminOnClick.js +0 -31
  34. package/core/enablePersistentFormInputValues.js +0 -72
  35. package/core/savePageCore.js +0 -245
  36. package/custom-attributes/onpagemutation.js +0 -20
  37. package/dom-utilities/insertStyleTag.js +0 -38
  38. /package/{communication → src/communication}/behaviorCollector.js +0 -0
  39. /package/{communication → src/communication}/uploadFile.js +0 -0
  40. /package/{core → src/core}/adminSystem.js +0 -0
  41. /package/{core → src/core}/editmode.js +0 -0
  42. /package/{core → src/core}/editmodeSystem.js +0 -0
  43. /package/{core → src/core}/exportToWindow.js +0 -0
  44. /package/{core → src/core}/isAdminOfCurrentResource.js +0 -0
  45. /package/{core → src/core}/saveToast.js +0 -0
  46. /package/{core → src/core}/setPageTypeOnDocumentElement.js +0 -0
  47. /package/{custom-attributes → src/custom-attributes}/autosize.js +0 -0
  48. /package/{custom-attributes → src/custom-attributes}/inputHelpers.js +0 -0
  49. /package/{custom-attributes → src/custom-attributes}/onclickaway.js +0 -0
  50. /package/{custom-attributes → src/custom-attributes}/onclone.js +0 -0
  51. /package/{custom-attributes → src/custom-attributes}/onrender.js +0 -0
  52. /package/{custom-attributes → src/custom-attributes}/preventEnter.js +0 -0
  53. /package/{dom-utilities → src/dom-utilities}/getDataFromForm.js +0 -0
  54. /package/{dom-utilities → src/dom-utilities}/onDomReady.js +0 -0
  55. /package/{dom-utilities → src/dom-utilities}/onLoad.js +0 -0
  56. /package/{string-utilities → src/string-utilities}/copy-to-clipboard.js +0 -0
  57. /package/{string-utilities → src/string-utilities}/query.js +0 -0
  58. /package/{string-utilities → src/string-utilities}/slugify.js +0 -0
  59. /package/{ui → src/ui}/toast-hyperclay.js +0 -0
  60. /package/{utilities → src/utilities}/cookie.js +0 -0
  61. /package/{utilities → src/utilities}/debounce.js +0 -0
  62. /package/{utilities → src/utilities}/loadVendorScript.js +0 -0
  63. /package/{utilities → src/utilities}/mutation.js +0 -0
  64. /package/{utilities → src/utilities}/nearest.js +0 -0
  65. /package/{utilities → src/utilities}/pipe.js +0 -0
  66. /package/{utilities → src/utilities}/throttle.js +0 -0
  67. /package/{vendor → src/vendor}/Sortable.vendor.js +0 -0
package/README.md CHANGED
@@ -21,7 +21,7 @@ Destructure directly from the import:
21
21
 
22
22
  ```html
23
23
  <script type="module">
24
- const { toast, savePage } = await import('https://cdn.jsdelivr.net/npm/hyperclayjs@1/hyperclay.js?preset=standard');
24
+ const { toast, savePage } = await import('https://cdn.jsdelivr.net/npm/hyperclayjs@1/src/hyperclay.js?preset=standard');
25
25
  toast('Hello!');
26
26
  </script>
27
27
  ```
@@ -30,7 +30,7 @@ Or with custom features:
30
30
 
31
31
  ```html
32
32
  <script type="module">
33
- const { toast, ask } = await import('https://cdn.jsdelivr.net/npm/hyperclayjs@1/hyperclay.js?features=toast,dialogs');
33
+ const { toast, ask } = await import('https://cdn.jsdelivr.net/npm/hyperclayjs@1/src/hyperclay.js?features=toast,dialogs');
34
34
  </script>
35
35
  ```
36
36
 
@@ -57,38 +57,41 @@ import 'hyperclayjs/presets/standard.js';
57
57
 
58
58
  | Module | Size | Description |
59
59
  |--------|------|-------------|
60
- | autosave | 1.1KB | Auto-save on DOM changes, unsaved changes warning |
60
+ | autosave | 0.9KB | Auto-save on DOM changes |
61
61
  | edit-mode | 1.8KB | Toggle edit mode on hyperclay on/off |
62
- | edit-mode-helpers | 5.4KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
63
- | option-visibility | 5.9KB | Dynamic show/hide based on ancestor state with option:attribute="value" |
64
- | persist | 2.5KB | Persist input/select/textarea values to the DOM with [persist] attribute |
65
- | save-core | 6.3KB | Basic save function only - hyperclay.savePage() |
62
+ | edit-mode-helpers | 7.5KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
63
+ | option-visibility | 5.3KB | Dynamic show/hide based on ancestor state with option:attribute="value" |
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
66
  | save-system | 7.1KB | Manual save: keyboard shortcut (CMD+S), save button, change tracking |
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 |
69
+ | unsaved-warning | 0.8KB | Warn before leaving page with unsaved changes |
68
70
 
69
71
  ### Custom Attributes (HTML enhancements)
70
72
 
71
73
  | Module | Size | Description |
72
74
  |--------|------|-------------|
73
- | ajax-elements | 2.8KB | [ajax-form], [ajax-button] for async form submissions |
74
- | dom-helpers | 5.7KB | el.nearest, el.val, el.text, el.exec, el.cycle |
75
- | event-attrs | 3.6KB | [onclickaway], [onclone], [onpagemutation], [onrender] |
75
+ | ajax-elements | 2.6KB | [ajax-form], [ajax-button] for async form submissions |
76
+ | dom-helpers | 6.2KB | el.nearest, el.val, el.text, el.exec, el.cycle |
77
+ | event-attrs | 4.1KB | [onclickaway], [onclone], [onpagemutation], [onrender] |
76
78
  | input-helpers | 1.2KB | [prevent-enter], [autosize] for textareas |
77
- | onaftersave | 1.2KB | [onaftersave] attribute - run JS when save status changes |
78
- | sortable | 2.8KB | Drag-drop sorting with [sortable], lazy-loads ~118KB Sortable.js in edit mode |
79
+ | onaftersave | 1KB | [onaftersave] attribute - run JS when save status changes |
80
+ | sortable | 3.4KB | Drag-drop sorting with [sortable], lazy-loads ~118KB Sortable.js in edit mode |
79
81
 
80
82
  ### UI Components (User interface elements)
81
83
 
82
84
  | Module | Size | Description |
83
85
  |--------|------|-------------|
84
- | dialogs | 8.4KB | ask(), consent(), tell(), snippet() dialog functions |
85
- | the-modal | 19.8KB | Full modal window creation system - window.theModal |
86
+ | dialogs | 7.7KB | ask(), consent(), tell(), snippet() dialog functions |
87
+ | the-modal | 21.8KB | Full modal window creation system - window.theModal |
86
88
  | toast | 7.7KB | Success/error message notifications, toast(msg, msgType) |
87
89
 
88
90
  ### Utilities (Core utilities (often auto-included))
89
91
 
90
92
  | Module | Size | Description |
91
93
  |--------|------|-------------|
94
+ | cache-bust | 0.6KB | Cache-bust href/src attributes |
92
95
  | cookie | 1.4KB | Cookie management (often auto-included) |
93
96
  | debounce | 0.4KB | Function debouncing |
94
97
  | mutation | 13KB | DOM mutation observation (often auto-included) |
@@ -99,10 +102,10 @@ import 'hyperclayjs/presets/standard.js';
99
102
 
100
103
  | Module | Size | Description |
101
104
  |--------|------|-------------|
102
- | all-js | 14KB | Full DOM manipulation library |
105
+ | all-js | 14.4KB | Full DOM manipulation library |
103
106
  | dom-ready | 0.4KB | DOM ready callback |
104
107
  | form-data | 2KB | Extract form data as an object |
105
- | style-injection | 1.1KB | Dynamic stylesheet injection |
108
+ | style-injection | 1.9KB | Dynamic stylesheet injection |
106
109
 
107
110
  ### String Utilities (String manipulation helpers)
108
111
 
@@ -117,27 +120,28 @@ import 'hyperclayjs/presets/standard.js';
117
120
  | Module | Size | Description |
118
121
  |--------|------|-------------|
119
122
  | file-upload | 10.7KB | File upload with progress |
120
- | send-message | 1.4KB | Message sending utility |
123
+ | live-sync | 12KB | Real-time DOM sync across browsers and with file system |
124
+ | send-message | 1.3KB | Message sending utility |
121
125
 
122
126
  ### Vendor Libraries (Third-party libraries)
123
127
 
124
128
  | Module | Size | Description |
125
129
  |--------|------|-------------|
126
- | idiomorph | 8.2KB | Efficient DOM morphing library |
130
+ | idiomorph | 8.3KB | Efficient DOM morphing library |
127
131
 
128
132
  ## Presets
129
133
 
130
- ### Minimal (~27.8KB)
134
+ ### Minimal (~30.4KB)
131
135
  Essential features for basic editing
132
136
 
133
137
  **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `toast`, `save-toast`, `export-to-window`
134
138
 
135
- ### Standard (~45.5KB)
139
+ ### Standard (~48.4KB)
136
140
  Standard feature set for most use cases
137
141
 
138
142
  **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `persist`, `option-visibility`, `event-attrs`, `dom-helpers`, `toast`, `save-toast`, `export-to-window`
139
143
 
140
- ### Everything (~150.2KB)
144
+ ### Everything (~176.5KB)
141
145
  All available features
142
146
 
143
147
  Includes all available modules across all categories.
package/package.json CHANGED
@@ -1,39 +1,31 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Modular JavaScript library for building interactive HTML applications with Hyperclay",
5
5
  "type": "module",
6
- "main": "hyperclay.js",
7
- "module": "hyperclay.js",
6
+ "main": "src/hyperclay.js",
7
+ "module": "src/hyperclay.js",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./hyperclay.js",
11
- "default": "./hyperclay.js"
10
+ "import": "./src/hyperclay.js",
11
+ "default": "./src/hyperclay.js"
12
12
  },
13
- "./core/*": "./core/*.js",
14
- "./custom-attributes/*": "./custom-attributes/*.js",
15
- "./ui/*": "./ui/*.js",
16
- "./utilities/*": "./utilities/*.js",
17
- "./dom-utilities/*": "./dom-utilities/*.js",
18
- "./string-utilities/*": "./string-utilities/*.js",
19
- "./communication/*": "./communication/*.js",
20
- "./vendor/*": "./vendor/*.js"
13
+ "./core/*": "./src/core/*.js",
14
+ "./custom-attributes/*": "./src/custom-attributes/*.js",
15
+ "./ui/*": "./src/ui/*.js",
16
+ "./utilities/*": "./src/utilities/*.js",
17
+ "./dom-utilities/*": "./src/dom-utilities/*.js",
18
+ "./string-utilities/*": "./src/string-utilities/*.js",
19
+ "./communication/*": "./src/communication/*.js",
20
+ "./vendor/*": "./src/vendor/*.js"
21
21
  },
22
22
  "files": [
23
- "core",
24
- "custom-attributes",
25
- "ui",
26
- "utilities",
27
- "dom-utilities",
28
- "string-utilities",
29
- "communication",
30
- "vendor",
31
- "hyperclay.js",
32
- "module-dependency-graph.json"
23
+ "src"
33
24
  ],
34
25
  "scripts": {
35
- "dev": "npm run build && http-server -p 3535 -c-1 -o /index.html",
26
+ "dev": "npm run build && npm run build:website && http-server website -p 3535 -c-1 -o /index.html",
36
27
  "build": "npm run generate:deps && npm run build:loader && npm run build:readme && npm run build:load-jsdelivr && npm run build:index-url",
28
+ "build:website": "node scripts/build-website.js",
37
29
  "generate:deps": "node build/generate-dependency-graph.js",
38
30
  "build:loader": "node build/build-loader.js",
39
31
  "build:readme": "node build/generate-readme.js",
@@ -44,7 +36,7 @@
44
36
  "format": "prettier --write .",
45
37
  "release": "./scripts/release.sh",
46
38
  "prepublishOnly": "npm run build && npm test",
47
- "postpublish": "test -n \"$SKIP_POSTPUBLISH\" || open http://127.0.0.1:3535/build/load-jsdelivr.html"
39
+ "postpublish": "test -n \"$SKIP_POSTPUBLISH\" || open http://127.0.0.1:3535/load-jsdelivr.html"
48
40
  },
49
41
  "repository": {
50
42
  "type": "git",
@@ -0,0 +1,396 @@
1
+ /**
2
+ * live-sync.js — Real-time sync between admin users
3
+ *
4
+ * HOW IT WORKS:
5
+ *
6
+ * ┌─────────────────────────────────────────────────────────┐
7
+ * │ 1. LISTEN snapshot-ready event from save │
8
+ * │ (body with form values, no strip) │
9
+ * └─────────────────────────────────────────────────────────┘
10
+ * │
11
+ * ▼
12
+ * ┌─────────────────────────────────────────────────────────┐
13
+ * │ 2. SEND POST body to /live-sync/save │
14
+ * │ (debounced, skip if unchanged) │
15
+ * └─────────────────────────────────────────────────────────┘
16
+ * │
17
+ * ▼
18
+ * ┌─────────────────────────────────────────────────────────┐
19
+ * │ 3. RECEIVE SSE stream from server │
20
+ * │ (other clients' changes) │
21
+ * └─────────────────────────────────────────────────────────┘
22
+ * │
23
+ * ▼
24
+ * ┌─────────────────────────────────────────────────────────┐
25
+ * │ 4. MORPH Idiomorph to update DOM │
26
+ * │ (preserves focus, input values) │
27
+ * └─────────────────────────────────────────────────────────┘
28
+ *
29
+ * DEPENDS ON: Idiomorph (for intelligent DOM morphing)
30
+ * INTEGRATES WITH: snapshot.js (receives snapshot-ready events)
31
+ */
32
+
33
+ class LiveSync {
34
+ constructor() {
35
+ this.sse = null;
36
+ this.currentFile = null;
37
+ this.lastHeadHash = null;
38
+ this.lastBodyHtml = null;
39
+ this.clientId = this.generateClientId();
40
+ this.debounceMs = 150;
41
+ this.debounceTimer = null;
42
+ this.isPaused = false;
43
+ this.isDestroyed = false;
44
+
45
+ // Store handler reference for cleanup
46
+ this._snapshotHandler = null;
47
+
48
+ // Callbacks
49
+ this.onConnect = null;
50
+ this.onDisconnect = null;
51
+ this.onUpdate = null;
52
+ this.onError = null;
53
+ }
54
+
55
+ /**
56
+ * Generate or retrieve a persistent client ID
57
+ */
58
+ generateClientId() {
59
+ let id = null;
60
+
61
+ try {
62
+ id = localStorage.getItem('livesync-client-id');
63
+ } catch (e) {
64
+ // localStorage might not be available
65
+ }
66
+
67
+ if (!id) {
68
+ id = Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
69
+ try {
70
+ localStorage.setItem('livesync-client-id', id);
71
+ } catch (e) {
72
+ // That's okay
73
+ }
74
+ }
75
+
76
+ return id;
77
+ }
78
+
79
+ /**
80
+ * Start the LiveSync system
81
+ * Can be called after stop() to restart with a new file
82
+ */
83
+ start(file = null) {
84
+ // Reset destroyed flag to allow restart after stop()
85
+ this.isDestroyed = false;
86
+
87
+ // Prevent double-connect: clean up existing connection first
88
+ if (this.sse || this._snapshotHandler) {
89
+ this.cleanup();
90
+ }
91
+
92
+ this.currentFile = file || this.detectCurrentFile();
93
+
94
+ if (!this.currentFile) {
95
+ console.warn('[LiveSync] No file detected');
96
+ return;
97
+ }
98
+
99
+ // Reset state for new connection
100
+ this.lastHeadHash = null;
101
+ this.lastBodyHtml = null;
102
+
103
+ console.log('[LiveSync] Starting for:', this.currentFile);
104
+ this.connect();
105
+ this.listenForSnapshots();
106
+ }
107
+
108
+ /**
109
+ * Clean up resources without marking as destroyed
110
+ * Used internally by start() to prevent double-connect
111
+ */
112
+ cleanup() {
113
+ if (this.sse) {
114
+ this.sse.close();
115
+ this.sse = null;
116
+ }
117
+
118
+ if (this._snapshotHandler) {
119
+ document.removeEventListener('hyperclay:snapshot-ready', this._snapshotHandler);
120
+ this._snapshotHandler = null;
121
+ }
122
+
123
+ clearTimeout(this.debounceTimer);
124
+ }
125
+
126
+ /**
127
+ * Auto-detect the current site identifier from the URL
128
+ * Returns site ID without .html extension
129
+ *
130
+ * Handles:
131
+ * - / -> index
132
+ * - /about -> about
133
+ * - /about.html -> about
134
+ * - /about/ -> about/index
135
+ * - /pages/contact -> pages/contact
136
+ * - /pages/contact/ -> pages/contact/index
137
+ */
138
+ detectCurrentFile() {
139
+ let pathname = window.location.pathname;
140
+
141
+ // Root path
142
+ if (pathname === '/') {
143
+ return 'index';
144
+ }
145
+
146
+ // Remove leading slash
147
+ pathname = pathname.replace(/^\//, '');
148
+
149
+ // Handle trailing slash -> directory index
150
+ if (pathname.endsWith('/')) {
151
+ return pathname + 'index';
152
+ }
153
+
154
+ // Remove .html extension if present
155
+ if (pathname.endsWith('.html')) {
156
+ return pathname.slice(0, -5);
157
+ }
158
+
159
+ // Already a site identifier
160
+ return pathname;
161
+ }
162
+
163
+ /**
164
+ * Connect to the SSE endpoint
165
+ * Uses native EventSource reconnection behavior
166
+ */
167
+ connect() {
168
+ if (this.isDestroyed) return;
169
+
170
+ const url = `/live-sync/stream?file=${encodeURIComponent(this.currentFile)}`;
171
+ this.sse = new EventSource(url);
172
+
173
+ this.sse.onopen = () => {
174
+ console.log('[LiveSync] Connected');
175
+ if (this.onConnect) this.onConnect();
176
+ };
177
+
178
+ this.sse.onmessage = (event) => {
179
+ const data = JSON.parse(event.data);
180
+
181
+ // Handle error events from server
182
+ if (data.error) {
183
+ console.error('[LiveSync] Server error:', data.error);
184
+ if (this.onError) this.onError(new Error(data.error));
185
+ return;
186
+ }
187
+
188
+ const { body, headHash, sender } = data;
189
+
190
+ // Ignore own changes
191
+ if (sender === this.clientId) return;
192
+
193
+ // Guard against invalid body - never apply non-string
194
+ if (typeof body !== 'string') {
195
+ console.error('[LiveSync] Received invalid body (not a string), ignoring');
196
+ return;
197
+ }
198
+
199
+ // Check for head changes -> full reload
200
+ // Only compare when BOTH hashes exist (server must send headHash)
201
+ // Only set lastHeadHash when incoming hash is valid
202
+ if (headHash) {
203
+ if (this.lastHeadHash && headHash !== this.lastHeadHash) {
204
+ console.log('[LiveSync] Head changed, reloading');
205
+ location.reload();
206
+ return;
207
+ }
208
+ this.lastHeadHash = headHash;
209
+ }
210
+
211
+ console.log('[LiveSync] Received update from:', sender);
212
+ this.applyUpdate(body);
213
+ if (this.onUpdate) this.onUpdate({ body, sender });
214
+ };
215
+
216
+ // Native EventSource auto-reconnects on transient errors
217
+ // We just surface the status via callbacks
218
+ this.sse.onerror = () => {
219
+ if (this.sse.readyState === EventSource.CONNECTING) {
220
+ console.log('[LiveSync] Reconnecting...');
221
+ } else if (this.sse.readyState === EventSource.CLOSED) {
222
+ console.log('[LiveSync] Connection closed');
223
+ if (this.onError) this.onError(new Error('Connection closed'));
224
+ }
225
+ if (this.onDisconnect) this.onDisconnect();
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Listen for snapshot-ready events from the save system.
231
+ * This replaces DOM observation — we sync when save happens.
232
+ */
233
+ listenForSnapshots() {
234
+ this._snapshotHandler = (event) => {
235
+ if (this.isPaused) return;
236
+
237
+ const { body } = event.detail;
238
+ if (!body) return;
239
+
240
+ this.sendBody(body);
241
+ };
242
+
243
+ document.addEventListener('hyperclay:snapshot-ready', this._snapshotHandler);
244
+ }
245
+
246
+ /**
247
+ * Send body HTML to the server (debounced)
248
+ * Only updates lastBodyHtml after successful save
249
+ */
250
+ sendBody(body) {
251
+ clearTimeout(this.debounceTimer);
252
+
253
+ this.debounceTimer = setTimeout(() => {
254
+ // Skip if unchanged
255
+ if (body === this.lastBodyHtml) return;
256
+
257
+ console.log('[LiveSync] Sending update');
258
+
259
+ // Compute head hash to send to server (for hosted mode)
260
+ const headHash = this.computeHeadHash();
261
+ this.lastHeadHash = headHash; // Track local head changes
262
+
263
+ fetch('/live-sync/save', {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/json' },
266
+ body: JSON.stringify({
267
+ file: this.currentFile,
268
+ body: body,
269
+ sender: this.clientId,
270
+ headHash: headHash
271
+ })
272
+ }).then(response => {
273
+ if (response.ok) {
274
+ // Only update lastBodyHtml after successful save
275
+ this.lastBodyHtml = body;
276
+ } else {
277
+ // Log non-OK responses but don't suppress future sends
278
+ console.warn('[LiveSync] Save returned status:', response.status);
279
+ }
280
+ }).catch(err => {
281
+ // Network error - don't update lastBodyHtml so next mutation will retry
282
+ console.error('[LiveSync] Save failed:', err);
283
+ if (this.onError) this.onError(err);
284
+ });
285
+ }, this.debounceMs);
286
+ }
287
+
288
+ /**
289
+ * Compute MD5-like hash of head content (first 8 hex chars)
290
+ * Uses a simple string hash since we don't have crypto in browser
291
+ */
292
+ computeHeadHash() {
293
+ const head = document.head?.innerHTML;
294
+ if (!head) return null;
295
+
296
+ // Simple hash function (djb2)
297
+ let hash = 5381;
298
+ for (let i = 0; i < head.length; i++) {
299
+ hash = ((hash << 5) + hash) + head.charCodeAt(i);
300
+ hash = hash & hash; // Convert to 32bit integer
301
+ }
302
+ // Convert to hex and take first 8 chars
303
+ return Math.abs(hash).toString(16).padStart(8, '0').slice(0, 8);
304
+ }
305
+
306
+ /**
307
+ * Apply an update received from the server
308
+ * Guards against non-string values
309
+ */
310
+ applyUpdate(bodyHtml) {
311
+ // Guard against non-string values
312
+ if (typeof bodyHtml !== 'string') {
313
+ console.error('[LiveSync] applyUpdate called with non-string value, ignoring');
314
+ return;
315
+ }
316
+
317
+ this.isPaused = true;
318
+ this.lastBodyHtml = bodyHtml;
319
+
320
+ try {
321
+ const temp = document.createElement('div');
322
+ temp.innerHTML = bodyHtml;
323
+
324
+ // Use Idiomorph if available
325
+ const morphFn = window.Idiomorph?.morph;
326
+
327
+ if (morphFn) {
328
+ morphFn(document.body, temp, {
329
+ morphStyle: 'innerHTML',
330
+ ignoreActiveValue: true
331
+ });
332
+ } else {
333
+ // Fallback to innerHTML
334
+ console.warn('[LiveSync] Idiomorph not available, using innerHTML fallback');
335
+ document.body.innerHTML = bodyHtml;
336
+ }
337
+
338
+ this.rehydrateFormState(document.body);
339
+ } finally {
340
+ this.isPaused = false;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Sync form control attributes to properties after DOM morph
346
+ */
347
+ rehydrateFormState(container) {
348
+ const focused = document.activeElement;
349
+
350
+ // Text inputs and textareas
351
+ container.querySelectorAll('input[value], textarea[value]').forEach(el => {
352
+ if (el === focused) return;
353
+ el.value = el.getAttribute('value') || '';
354
+ });
355
+
356
+ // Checkboxes and radios
357
+ container.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(el => {
358
+ if (el === focused) return;
359
+ el.checked = el.hasAttribute('checked');
360
+ });
361
+
362
+ // Select dropdowns
363
+ container.querySelectorAll('select').forEach(select => {
364
+ if (select === focused) return;
365
+ select.querySelectorAll('option').forEach(opt => {
366
+ opt.selected = opt.hasAttribute('selected');
367
+ });
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Stop LiveSync and clean up resources
373
+ * Can call start() again to restart
374
+ */
375
+ stop() {
376
+ this.cleanup();
377
+ this.isDestroyed = true;
378
+ console.log('[LiveSync] Stopped');
379
+ }
380
+ }
381
+
382
+ // Singleton instance
383
+ const liveSync = new LiveSync();
384
+
385
+ // Auto-initialize when DOM is ready
386
+ if (typeof window !== 'undefined') {
387
+ if (document.readyState === 'loading') {
388
+ document.addEventListener('DOMContentLoaded', () => liveSync.start());
389
+ } else {
390
+ liveSync.start();
391
+ }
392
+ }
393
+
394
+ // Export for hyperclayjs module system
395
+ export { liveSync };
396
+ export default liveSync;
@@ -5,17 +5,11 @@ import toast from "../ui/toast.js";
5
5
  function sendMessage(eventOrObj, successMessage = "Successfully sent", callback) {
6
6
  let form;
7
7
  let data;
8
-
8
+
9
9
  if (eventOrObj instanceof Event) {
10
10
  eventOrObj.preventDefault();
11
11
  form = eventOrObj.target.closest('form');
12
-
13
- if (!form) {
14
- toast('No form found for this element', 'error');
15
- return Promise.reject('No form found');
16
- }
17
-
18
- data = getDataFromForm(form);
12
+ data = form ? getDataFromForm(form) : {};
19
13
  } else {
20
14
  data = eventOrObj;
21
15
  if (this?.closest) {
@@ -0,0 +1,51 @@
1
+ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
2
+ import onDomReady from "../dom-utilities/onDomReady.js";
3
+ import {beforeSave} from "./savePage.js";
4
+
5
+ export function disableContentEditableBeforeSave () {
6
+ beforeSave(docElem => {
7
+ docElem.querySelectorAll('[edit-mode-contenteditable]').forEach(resource => {
8
+ const originalValue = resource.getAttribute("contenteditable");
9
+ resource.setAttribute("inert-contenteditable", originalValue);
10
+ resource.removeAttribute("contenteditable");
11
+ });
12
+ });
13
+ }
14
+
15
+ export function enableContentEditableForAdminOnPageLoad () {
16
+ if (!isEditMode) return;
17
+
18
+ onDomReady(() => {
19
+ enableContentEditable();
20
+ });
21
+ }
22
+
23
+ // Runtime toggle functions
24
+ export function enableContentEditable() {
25
+ document.querySelectorAll('[edit-mode-contenteditable]').forEach(el => {
26
+ let val = el.getAttribute("inert-contenteditable");
27
+ if (!["false", "plaintext-only"].includes(val)) val = "true";
28
+ el.setAttribute("contenteditable", val);
29
+ el.removeAttribute("inert-contenteditable");
30
+ });
31
+ }
32
+
33
+ export function disableContentEditable() {
34
+ document.querySelectorAll('[edit-mode-contenteditable]').forEach(el => {
35
+ const val = el.getAttribute("contenteditable") || "true";
36
+ el.setAttribute("inert-contenteditable", val);
37
+ el.removeAttribute("contenteditable");
38
+ });
39
+ }
40
+
41
+ // Auto-initialize
42
+ export function init() {
43
+ disableContentEditableBeforeSave();
44
+ enableContentEditableForAdminOnPageLoad();
45
+ }
46
+
47
+ // Export to window
48
+ window.hyperclay = window.hyperclay || {};
49
+ window.hyperclay.enableContentEditable = enableContentEditable;
50
+ window.hyperclay.disableContentEditable = disableContentEditable;
51
+ window.h = window.hyperclay;