hyperclayjs 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,13 +57,14 @@ import 'hyperclayjs/presets/standard.js';
57
57
 
58
58
  | Module | Size | Description |
59
59
  |--------|------|-------------|
60
- | autosave | 1.2KB | Auto-save on DOM changes, unsaved changes warning |
60
+ | autosave | 1.1KB | Auto-save on DOM changes, unsaved changes warning |
61
61
  | edit-mode | 1.8KB | Toggle edit mode on hyperclay on/off |
62
62
  | edit-mode-helpers | 5.4KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
63
63
  | option-visibility | 4.7KB | Dynamic show/hide based on ancestor state with option:attribute="value" |
64
64
  | persist | 2.5KB | Persist input/select/textarea values to the DOM with [persist] attribute |
65
- | save-core | 5.9KB | Basic save function only - hyperclay.savePage() |
66
- | save-system | 4.9KB | Manual save: keyboard shortcut (CMD+S), save button, change tracking |
65
+ | save-core | 6.3KB | Basic save function only - hyperclay.savePage() |
66
+ | save-system | 7KB | Manual save: keyboard shortcut (CMD+S), save button, change tracking |
67
+ | save-toast | 0.9KB | Toast notifications for save events (opt-in) |
67
68
 
68
69
  ### Custom Attributes (HTML enhancements)
69
70
 
@@ -73,6 +74,7 @@ import 'hyperclayjs/presets/standard.js';
73
74
  | dom-helpers | 5.7KB | el.nearest, el.val, el.text, el.exec, el.cycle |
74
75
  | event-attrs | 3.6KB | [onclickaway], [onclone], [onpagemutation], [onrender] |
75
76
  | input-helpers | 1.2KB | [prevent-enter], [autosize] for textareas |
77
+ | onaftersave | 1.2KB | [onaftersave] attribute - run JS when save status changes |
76
78
  | sortable | 2.8KB | Drag-drop sorting with [sortable], lazy-loads ~118KB Sortable.js in edit mode |
77
79
 
78
80
  ### UI Components (User interface elements)
@@ -126,17 +128,17 @@ import 'hyperclayjs/presets/standard.js';
126
128
 
127
129
  ## Presets
128
130
 
129
- ### Minimal (~24.3KB)
131
+ ### Minimal (~27.7KB)
130
132
  Essential features for basic editing
131
133
 
132
- **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `toast`, `export-to-window`
134
+ **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `toast`, `save-toast`, `export-to-window`
133
135
 
134
- ### Standard (~40.8KB)
136
+ ### Standard (~44.2KB)
135
137
  Standard feature set for most use cases
136
138
 
137
- **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `persist`, `option-visibility`, `event-attrs`, `dom-helpers`, `toast`, `export-to-window`
139
+ **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `persist`, `option-visibility`, `event-attrs`, `dom-helpers`, `toast`, `save-toast`, `export-to-window`
138
140
 
139
- ### Everything (~145.1KB)
141
+ ### Everything (~149.6KB)
140
142
  All available features
141
143
 
142
144
  Includes all available modules across all categories.
package/core/autosave.js CHANGED
@@ -5,9 +5,9 @@
5
5
  * Warns before leaving page with unsaved changes.
6
6
  *
7
7
  * Requires the 'save-system' module to be loaded first.
8
+ * For toast notifications, also load the 'save-toast' module.
8
9
  */
9
10
 
10
- import toast from "../ui/toast.js";
11
11
  import Mutation from "../utilities/mutation.js";
12
12
  import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
13
13
  import {
@@ -24,9 +24,7 @@ function initSavePageOnChange() {
24
24
  debounce: 3333,
25
25
  omitChangeDetails: true
26
26
  }, () => {
27
- savePageThrottled(({msg, msgType} = {}) => {
28
- if (msg) toast(msg, msgType);
29
- });
27
+ savePageThrottled();
30
28
  });
31
29
  }
32
30
 
package/core/savePage.js CHANGED
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Save system for Hyperclay
3
3
  *
4
- * Manual save with change detection, toast notifications,
4
+ * Manual save with change detection, state management,
5
5
  * keyboard shortcuts, and save button support.
6
6
  *
7
7
  * For auto-save on DOM changes, also load the 'autosave' module.
8
+ * For toast notifications, also load the 'save-toast' module.
8
9
  *
9
10
  * Built on top of savePageCore.js
10
11
  */
11
12
 
12
- import toast from "../ui/toast.js";
13
13
  import throttle from "../utilities/throttle.js";
14
14
  import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
15
15
  import {
@@ -19,6 +19,69 @@ import {
19
19
  beforeSave
20
20
  } from "./savePageCore.js";
21
21
 
22
+ // ============================================
23
+ // SAVE STATE MANAGEMENT
24
+ // ============================================
25
+
26
+ let savingTimeout = null;
27
+
28
+ /**
29
+ * Sets the save status on <html> and dispatches an event.
30
+ *
31
+ * @param {string} state - One of: 'saving', 'saved', 'offline', 'error'
32
+ * @param {string} msg - Optional message (e.g., error details)
33
+ */
34
+ function setSaveState(state, msg = '') {
35
+ if (savingTimeout) {
36
+ clearTimeout(savingTimeout);
37
+ savingTimeout = null;
38
+ }
39
+
40
+ document.documentElement.setAttribute('savestatus', state);
41
+
42
+ const event = new CustomEvent(`hyperclay:save-${state}`, {
43
+ detail: { msg, timestamp: Date.now() }
44
+ });
45
+ document.dispatchEvent(event);
46
+ }
47
+
48
+ /**
49
+ * Sets DOM state to 'offline' immediately, but does NOT fire an event.
50
+ * Used for instant UI feedback before we know the final state.
51
+ */
52
+ function setOfflineStateQuiet() {
53
+ if (savingTimeout) {
54
+ clearTimeout(savingTimeout);
55
+ savingTimeout = null;
56
+ }
57
+ document.documentElement.setAttribute('savestatus', 'offline');
58
+ }
59
+
60
+ /**
61
+ * Starts a debounced 'saving' state.
62
+ * Only shows 'saving' if the save takes longer than 500ms.
63
+ * This prevents UI flicker on fast saves.
64
+ */
65
+ function setSavingState() {
66
+ savingTimeout = setTimeout(() => {
67
+ setSaveState('saving');
68
+ }, 500);
69
+ }
70
+
71
+ // ============================================
72
+ // OFFLINE DETECTION
73
+ // ============================================
74
+
75
+ window.addEventListener('offline', () => {
76
+ setOfflineStateQuiet();
77
+ });
78
+
79
+ window.addEventListener('online', () => {
80
+ if (document.documentElement.getAttribute('savestatus') === 'offline') {
81
+ savePage();
82
+ }
83
+ });
84
+
22
85
  // Re-export from core for backward compatibility
23
86
  export { beforeSave, getPageContents };
24
87
 
@@ -41,7 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
41
104
  });
42
105
 
43
106
  /**
44
- * Save the current page with change detection and toast notifications
107
+ * Save the current page with change detection and state management
45
108
  *
46
109
  * @param {Function} callback - Optional callback for custom handling
47
110
  */
@@ -50,6 +113,13 @@ export function savePage(callback = () => {}) {
50
113
  return;
51
114
  }
52
115
 
116
+ // Check if offline - set DOM state immediately for UI feedback
117
+ // but still try the fetch (navigator.onLine can be wrong)
118
+ const wasOffline = !navigator.onLine;
119
+ if (wasOffline) {
120
+ setOfflineStateQuiet();
121
+ }
122
+
53
123
  const currentContents = getPageContents();
54
124
 
55
125
  // Track whether there are unsaved changes
@@ -60,11 +130,22 @@ export function savePage(callback = () => {}) {
60
130
  return;
61
131
  }
62
132
 
133
+ // Start debounced 'saving' state (only shows if save takes >500ms)
134
+ setSavingState();
135
+
63
136
  savePageCore(({msg, msgType}) => {
64
- // Update tracking on success
65
137
  if (msgType !== 'error') {
138
+ // SUCCESS
66
139
  lastSavedContents = currentContents;
67
140
  unsavedChanges = false;
141
+ setSaveState('saved', msg);
142
+ } else {
143
+ // FAILED - determine if it's offline or server error
144
+ if (!navigator.onLine) {
145
+ setSaveState('offline', msg);
146
+ } else {
147
+ setSaveState('error', msg);
148
+ }
68
149
  }
69
150
 
70
151
  // Call user callback if provided
@@ -76,7 +157,7 @@ export function savePage(callback = () => {}) {
76
157
 
77
158
  /**
78
159
  * Fetch HTML from a URL and save it, then reload
79
- * Shows toast notifications
160
+ * Emits error event if save fails
80
161
  *
81
162
  * @param {string} url - URL to fetch from
82
163
  */
@@ -87,8 +168,8 @@ export function replacePageWith(url) {
87
168
 
88
169
  replacePageWithCore(url, (err, data) => {
89
170
  if (err) {
90
- // Show error toast if save failed
91
- toast(err.message || "Failed to save template", "error");
171
+ // Emit error event (save-toast will show toast if loaded)
172
+ setSaveState('error', err.message || "Failed to save template");
92
173
  } else {
93
174
  // Only reload if save was successful
94
175
  window.location.reload();
@@ -138,9 +219,7 @@ export function initSaveKeyboardShortcut() {
138
219
  let metaKeyPressed = isMac ? event.metaKey : event.ctrlKey;
139
220
  if (metaKeyPressed && event.keyCode == 83) {
140
221
  event.preventDefault();
141
- savePage(({msg, msgType} = {}) => {
142
- if (msg) toast(msg, msgType);
143
- });
222
+ savePage();
144
223
  }
145
224
  });
146
225
  }
@@ -152,9 +231,7 @@ export function initSaveKeyboardShortcut() {
152
231
  export function initHyperclaySaveButton() {
153
232
  document.addEventListener("click", event => {
154
233
  if (event.target.closest("[trigger-save]")) {
155
- savePage(({msg, msgType} = {}) => {
156
- if (msg) toast(msg, msgType);
157
- });
234
+ savePage();
158
235
  }
159
236
  });
160
237
  }
@@ -91,12 +91,18 @@ export function savePage(callback = () => {}) {
91
91
  return;
92
92
  }
93
93
 
94
+ // Add timeout - abort if server doesn't respond within 5 seconds
95
+ const controller = new AbortController();
96
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
97
+
94
98
  fetch(saveEndpoint, {
95
99
  method: 'POST',
96
100
  credentials: 'include',
97
- body: currentContents
101
+ body: currentContents,
102
+ signal: controller.signal
98
103
  })
99
104
  .then(res => {
105
+ clearTimeout(timeoutId);
100
106
  return res.json().then(data => {
101
107
  if (!res.ok) {
102
108
  throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
@@ -110,12 +116,19 @@ export function savePage(callback = () => {}) {
110
116
  }
111
117
  })
112
118
  .catch(err => {
119
+ clearTimeout(timeoutId);
113
120
  console.error('Failed to save page:', err);
121
+
122
+ const msg = err.name === 'AbortError'
123
+ ? 'Server not responding'
124
+ : (err.message || 'Failed to save');
125
+
114
126
  if (typeof callback === 'function') {
115
- callback({msg: err.message || "Failed to save", msgType: "error"});
127
+ callback({msg, msgType: "error"});
116
128
  }
117
129
  })
118
130
  .finally(() => {
131
+ clearTimeout(timeoutId);
119
132
  saveInProgress = false;
120
133
  });
121
134
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Save Toast Module
3
+ *
4
+ * Listens for save lifecycle events and shows toast notifications.
5
+ * This is opt-in - only included if you want toast notifications.
6
+ *
7
+ * Events handled:
8
+ * - hyperclay:save-saved → success toast
9
+ * - hyperclay:save-error → error toast
10
+ * - hyperclay:save-offline → error toast (treated as error for notifications)
11
+ */
12
+
13
+ import toast from "../ui/toast.js";
14
+ import { isEditMode } from "./isAdminOfCurrentResource.js";
15
+
16
+ function init() {
17
+ if (!isEditMode) return;
18
+
19
+ document.addEventListener('hyperclay:save-saved', (e) => {
20
+ const msg = e.detail?.msg || 'Saved';
21
+ toast(msg, 'success');
22
+ });
23
+
24
+ document.addEventListener('hyperclay:save-error', (e) => {
25
+ const msg = e.detail?.msg || 'Failed to save';
26
+ toast(msg, 'error');
27
+ });
28
+
29
+ document.addEventListener('hyperclay:save-offline', (e) => {
30
+ const msg = e.detail?.msg || 'No internet connection';
31
+ toast(msg, 'error');
32
+ });
33
+ }
34
+
35
+ init();
36
+
37
+ export default init;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * [onaftersave] Custom Attribute
3
+ *
4
+ * Runs inline JavaScript when save status changes.
5
+ * Pairs with the existing [onbeforesave] attribute.
6
+ *
7
+ * Usage:
8
+ * <span onaftersave="this.innerText = event.detail.msg"></span>
9
+ * <div onaftersave="console.log('Status:', event.detail.status)"></div>
10
+ *
11
+ * The event.detail object contains:
12
+ * - status: 'saving' | 'saved' | 'offline' | 'error'
13
+ * - msg: string (e.g., 'Saved' or error message)
14
+ * - timestamp: number (Date.now())
15
+ */
16
+
17
+ function broadcast(e) {
18
+ const status = e.type.replace('hyperclay:save-', '');
19
+ const detail = { ...e.detail, status };
20
+
21
+ document.querySelectorAll('[onaftersave]').forEach(el => {
22
+ try {
23
+ const event = new CustomEvent('aftersave', { detail });
24
+ const handler = new Function('event', el.getAttribute('onaftersave'));
25
+ handler.call(el, event);
26
+ } catch (err) {
27
+ console.error('[onaftersave] Error in handler:', err);
28
+ }
29
+ });
30
+ }
31
+
32
+ function init() {
33
+ document.addEventListener('hyperclay:save-saving', broadcast);
34
+ document.addEventListener('hyperclay:save-saved', broadcast);
35
+ document.addEventListener('hyperclay:save-offline', broadcast);
36
+ document.addEventListener('hyperclay:save-error', broadcast);
37
+ }
38
+
39
+ init();
40
+
41
+ export default init;
package/hyperclay.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperclayJS v1.4.0 - Minimal Browser-Native Loader
2
+ * HyperclayJS v1.5.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.
@@ -28,6 +28,7 @@ const MODULE_PATHS = {
28
28
  "save-core": "./core/savePageCore.js",
29
29
  "save-system": "./core/savePage.js",
30
30
  "autosave": "./core/autosave.js",
31
+ "save-toast": "./core/saveToast.js",
31
32
  "edit-mode-helpers": "./core/adminSystem.js",
32
33
  "persist": "./core/enablePersistentFormInputValues.js",
33
34
  "option-visibility": "./core/optionVisibilityRuleGenerator.js",
@@ -37,6 +38,7 @@ const MODULE_PATHS = {
37
38
  "sortable": "./custom-attributes/sortable.js",
38
39
  "dom-helpers": "./custom-attributes/domHelpers.js",
39
40
  "input-helpers": "./custom-attributes/inputHelpers.js",
41
+ "onaftersave": "./custom-attributes/onaftersave.js",
40
42
  "dialogs": "./ui/prompts.js",
41
43
  "toast": "./ui/toast.js",
42
44
  "toast-hyperclay": "./ui/toast-hyperclay.js",
@@ -70,6 +72,7 @@ const PRESETS = {
70
72
  "save-system",
71
73
  "edit-mode-helpers",
72
74
  "toast",
75
+ "save-toast",
73
76
  "export-to-window"
74
77
  ]
75
78
  },
@@ -85,6 +88,7 @@ const PRESETS = {
85
88
  "event-attrs",
86
89
  "dom-helpers",
87
90
  "toast",
91
+ "save-toast",
88
92
  "export-to-window"
89
93
  ]
90
94
  },
@@ -95,6 +99,7 @@ const PRESETS = {
95
99
  "save-core",
96
100
  "save-system",
97
101
  "autosave",
102
+ "save-toast",
98
103
  "edit-mode-helpers",
99
104
  "persist",
100
105
  "option-visibility",
@@ -104,6 +109,7 @@ const PRESETS = {
104
109
  "sortable",
105
110
  "dom-helpers",
106
111
  "input-helpers",
112
+ "onaftersave",
107
113
  "dialogs",
108
114
  "toast",
109
115
  "toast-hyperclay",
@@ -50,7 +50,6 @@
50
50
  "core/autosave.js": [
51
51
  "core/isAdminOfCurrentResource.js",
52
52
  "core/savePage.js",
53
- "ui/toast.js",
54
53
  "utilities/mutation.js"
55
54
  ],
56
55
  "core/editmode.js": [
@@ -75,13 +74,16 @@
75
74
  "core/savePage.js": [
76
75
  "core/isAdminOfCurrentResource.js",
77
76
  "core/savePageCore.js",
78
- "ui/toast.js",
79
77
  "utilities/throttle.js"
80
78
  ],
81
79
  "core/savePageCore.js": [
82
80
  "core/isAdminOfCurrentResource.js",
83
81
  "utilities/cookie.js"
84
82
  ],
83
+ "core/saveToast.js": [
84
+ "core/isAdminOfCurrentResource.js",
85
+ "ui/toast.js"
86
+ ],
85
87
  "core/setPageTypeOnDocumentElement.js": [
86
88
  "core/isAdminOfCurrentResource.js",
87
89
  "core/savePage.js",
@@ -108,6 +110,7 @@
108
110
  "custom-attributes/autosize.js",
109
111
  "custom-attributes/preventEnter.js"
110
112
  ],
113
+ "custom-attributes/onaftersave.js": [],
111
114
  "custom-attributes/onclickaway.js": [],
112
115
  "custom-attributes/onclone.js": [],
113
116
  "custom-attributes/onpagemutation.js": [
@@ -165,7 +168,7 @@
165
168
  "save-core": {
166
169
  "name": "save-core",
167
170
  "category": "core",
168
- "size": 5.9,
171
+ "size": 6.3,
169
172
  "files": [
170
173
  "core/savePageCore.js"
171
174
  ],
@@ -179,7 +182,7 @@
179
182
  "save-system": {
180
183
  "name": "save-system",
181
184
  "category": "core",
182
- "size": 4.9,
185
+ "size": 7,
183
186
  "files": [
184
187
  "core/savePage.js"
185
188
  ],
@@ -202,13 +205,23 @@
202
205
  "autosave": {
203
206
  "name": "autosave",
204
207
  "category": "core",
205
- "size": 1.2,
208
+ "size": 1.1,
206
209
  "files": [
207
210
  "core/autosave.js"
208
211
  ],
209
212
  "description": "Auto-save on DOM changes, unsaved changes warning",
210
213
  "exports": {}
211
214
  },
215
+ "save-toast": {
216
+ "name": "save-toast",
217
+ "category": "core",
218
+ "size": 0.9,
219
+ "files": [
220
+ "core/saveToast.js"
221
+ ],
222
+ "description": "Toast notifications for save events (opt-in)",
223
+ "exports": {}
224
+ },
212
225
  "edit-mode-helpers": {
213
226
  "name": "edit-mode-helpers",
214
227
  "category": "core",
@@ -321,6 +334,16 @@
321
334
  "description": "[prevent-enter], [autosize] for textareas",
322
335
  "exports": {}
323
336
  },
337
+ "onaftersave": {
338
+ "name": "onaftersave",
339
+ "category": "custom-attributes",
340
+ "size": 1.2,
341
+ "files": [
342
+ "custom-attributes/onaftersave.js"
343
+ ],
344
+ "description": "[onaftersave] attribute - run JS when save status changes",
345
+ "exports": {}
346
+ },
324
347
  "dialogs": {
325
348
  "name": "dialogs",
326
349
  "category": "ui",
@@ -669,6 +692,7 @@
669
692
  "save-core": "./core/savePageCore.js",
670
693
  "save-system": "./core/savePage.js",
671
694
  "autosave": "./core/autosave.js",
695
+ "save-toast": "./core/saveToast.js",
672
696
  "edit-mode-helpers": "./core/adminSystem.js",
673
697
  "persist": "./core/enablePersistentFormInputValues.js",
674
698
  "option-visibility": "./core/optionVisibilityRuleGenerator.js",
@@ -678,6 +702,7 @@
678
702
  "sortable": "./custom-attributes/sortable.js",
679
703
  "dom-helpers": "./custom-attributes/domHelpers.js",
680
704
  "input-helpers": "./custom-attributes/inputHelpers.js",
705
+ "onaftersave": "./custom-attributes/onaftersave.js",
681
706
  "dialogs": "./ui/prompts.js",
682
707
  "toast": "./ui/toast.js",
683
708
  "toast-hyperclay": "./ui/toast-hyperclay.js",
@@ -710,6 +735,7 @@
710
735
  "save-core",
711
736
  "save-system",
712
737
  "autosave",
738
+ "save-toast",
713
739
  "edit-mode-helpers",
714
740
  "persist",
715
741
  "option-visibility",
@@ -724,7 +750,8 @@
724
750
  "ajax-elements",
725
751
  "sortable",
726
752
  "dom-helpers",
727
- "input-helpers"
753
+ "input-helpers",
754
+ "onaftersave"
728
755
  ]
729
756
  },
730
757
  "ui": {
@@ -792,6 +819,7 @@
792
819
  "save-system",
793
820
  "edit-mode-helpers",
794
821
  "toast",
822
+ "save-toast",
795
823
  "export-to-window"
796
824
  ]
797
825
  },
@@ -807,6 +835,7 @@
807
835
  "event-attrs",
808
836
  "dom-helpers",
809
837
  "toast",
838
+ "save-toast",
810
839
  "export-to-window"
811
840
  ]
812
841
  },
@@ -817,6 +846,7 @@
817
846
  "save-core",
818
847
  "save-system",
819
848
  "autosave",
849
+ "save-toast",
820
850
  "edit-mode-helpers",
821
851
  "persist",
822
852
  "option-visibility",
@@ -826,6 +856,7 @@
826
856
  "sortable",
827
857
  "dom-helpers",
828
858
  "input-helpers",
859
+ "onaftersave",
829
860
  "dialogs",
830
861
  "toast",
831
862
  "toast-hyperclay",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Modular JavaScript library for building interactive HTML applications with Hyperclay",
5
5
  "type": "module",
6
6
  "main": "hyperclay.js",