hyperclayjs 1.24.1 → 1.24.2

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
@@ -61,7 +61,7 @@ import 'hyperclayjs/presets/standard.js';
61
61
  | edit-mode | 1.8KB | Toggle edit mode on hyperclay on/off |
62
62
  | edit-mode-helpers | 6.8KB | Admin-only functionality: [viewmode:disabled], [editmode:resource], [editmode:onclick] |
63
63
  | option-visibility | 7.1KB | 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 |
64
+ | persist | 6.4KB | Persist input/select/textarea values to the DOM with [persist] attribute |
65
65
  | save-core | 8.9KB | Basic save function only - hyperclay.savePage() |
66
66
  | save-system | 13.4KB | CMD+S, [trigger-save] button, savestatus attribute |
67
67
  | save-toast | 0.9KB | Toast notifications for save events |
@@ -75,7 +75,7 @@ import 'hyperclayjs/presets/standard.js';
75
75
  |--------|------|-------------|
76
76
  | ajax-elements | 2.6KB | [ajax-form], [ajax-button] for async form submissions |
77
77
  | dom-helpers | 6.8KB | el.nearest, el.val, el.text, el.exec, el.cycle |
78
- | event-attrs | 4.6KB | [onclickaway], [onclickchildren], [onclone], [onpagemutation], [onrender] |
78
+ | event-attrs | 5.3KB | [onclickaway], [onclickchildren], [onclone], [onpagemutation], [onrender] |
79
79
  | input-helpers | 3.9KB | [prevent-enter], [autosize] for textareas |
80
80
  | movable | 2.5KB | Free-positioning drag with [movable] and [movable-handle], edit mode only |
81
81
  | onaftersave | 1KB | [onaftersave] attribute - run JS when save status changes |
@@ -139,12 +139,12 @@ Essential features for basic editing
139
139
 
140
140
  **Modules:** `save-core`, `snapshot`, `save-system`, `edit-mode-helpers`, `toast`, `save-toast`, `export-to-window`, `view-mode-excludes-edit-modules`
141
141
 
142
- ### Standard (~79.2KB)
142
+ ### Standard (~83.9KB)
143
143
  Standard feature set for most use cases
144
144
 
145
145
  **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`
146
146
 
147
- ### Everything (~220.5KB)
147
+ ### Everything (~225.2KB)
148
148
  All available features
149
149
 
150
150
  Includes all available modules across all categories.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.24.1",
3
+ "version": "1.24.2",
4
4
  "description": "Modular JavaScript library for building interactive malleable HTML files with Hyperclay",
5
5
  "type": "module",
6
6
  "main": "src/hyperclay.js",
@@ -1,18 +1,102 @@
1
1
  import { onSnapshot } from './snapshot.js';
2
2
 
3
- // <input type="checkbox" persist>
3
+ // Persistent Form Input Values
4
+ //
5
+ // Problem: Browser form values (.value, .checked, .selectedIndex) live in JS
6
+ // memory, not in DOM attributes. When Hyperclay serializes the page via
7
+ // cloneNode() or outerHTML, those JS-only values are lost. This module syncs
8
+ // them back to the DOM so they survive saves, live-sync, and cloning.
9
+ //
10
+ // Strategy: sync to the DOM immediately on every user interaction, not just at
11
+ // snapshot time. This means cloneNode() always gets current values.
12
+ //
13
+ // Why this is safe for each element type:
14
+ //
15
+ // <input type="text"> — setAttribute("value", ...) updates the DOM attribute
16
+ // but does NOT change the displayed text or cursor position. The browser's
17
+ // "dirty value flag" (WHATWG spec) means that once a user has typed into an
18
+ // input, the browser ignores attribute changes for display purposes. The
19
+ // attribute and the live .value property become independent surfaces.
20
+ //
21
+ // <select> — Setting/removing the "selected" attribute on <option> elements
22
+ // has no side effects on display or interaction.
23
+ //
24
+ // <input type="checkbox/radio"> — Setting/removing the "checked" attribute
25
+ // has no side effects.
26
+ //
27
+ // <textarea> — The hard case. Unlike <input>, a textarea stores its default
28
+ // value as child text nodes, not an attribute. Writing textContent while
29
+ // focused destroys cursor position, scroll, and selection. So instead, we
30
+ // write to a harmless "data-value" attribute on every keystroke (completely
31
+ // inert — no cursor, scroll, or reflow). At snapshot time, the onSnapshot
32
+ // hook reads data-value from the cloned textarea, writes it into the
33
+ // clone's textContent, and strips the attribute. The saved HTML is clean.
34
+ //
35
+ // The onSnapshot hook also serves as a safety net for all types, catching any
36
+ // values that weren't synced by the live listeners (e.g., a textarea that was
37
+ // never typed into but had its value set programmatically).
38
+ //
39
+ // Synergy with onclone (custom-attributes/onclone.js):
40
+ // When event-attrs is loaded, its cloneNode() intercept (onclone.js) patches
41
+ // data-value into textContent on cloned textareas automatically.
42
+
4
43
  export default function enablePersistentFormInputValues(filterBySelector = "[persist]") {
5
44
  const inputSelector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"])`;
6
45
  const textareaSelector = `textarea${filterBySelector}`;
7
46
  const selectSelector = `select${filterBySelector}`;
8
47
 
9
- // Use onSnapshot so form values are synced for both save AND live-sync
48
+ // --- Live DOM sync: keep attributes in sync on every user interaction ---
49
+
50
+ document.addEventListener('input', (e) => {
51
+ const el = e.target;
52
+
53
+ // Text-like inputs: setAttribute("value") is safe — dirty flag prevents display change
54
+ if (el.matches(inputSelector) && el.type !== 'checkbox' && el.type !== 'radio') {
55
+ el.setAttribute('value', el.value);
56
+ return;
57
+ }
58
+
59
+ // Textareas: write to data-value (inert, no cursor/scroll disruption)
60
+ if (el.matches(textareaSelector)) {
61
+ el.setAttribute('data-value', el.value);
62
+ return;
63
+ }
64
+ }, true);
65
+
66
+ document.addEventListener('change', (e) => {
67
+ const el = e.target;
68
+
69
+ // Checkboxes and radios
70
+ if (el.matches(inputSelector) && (el.type === 'checkbox' || el.type === 'radio')) {
71
+ el.checked ? el.setAttribute('checked', '') : el.removeAttribute('checked');
72
+ return;
73
+ }
74
+
75
+ // Selects: sync selected attribute on options
76
+ if (el.matches(selectSelector)) {
77
+ const options = el.querySelectorAll('option');
78
+ options.forEach(opt => opt.removeAttribute('selected'));
79
+ if (el.multiple) {
80
+ Array.from(el.selectedOptions).forEach(opt => {
81
+ const idx = Array.from(el.options).indexOf(opt);
82
+ if (options[idx]) options[idx].setAttribute('selected', '');
83
+ });
84
+ } else if (el.selectedIndex >= 0 && options[el.selectedIndex]) {
85
+ options[el.selectedIndex].setAttribute('selected', '');
86
+ }
87
+ return;
88
+ }
89
+ }, true);
90
+
91
+ // --- Snapshot hook: final safety net at serialize time ---
92
+
10
93
  onSnapshot((doc) => {
11
- // Sync text inputs
94
+ // Guard: if another onSnapshot hook mutated the clone, lists may diverge
12
95
  const liveInputs = document.querySelectorAll(inputSelector);
13
96
  const clonedInputs = doc.querySelectorAll(inputSelector);
14
97
  clonedInputs.forEach((cloned, i) => {
15
98
  const live = liveInputs[i];
99
+ if (!live) return;
16
100
  if (live.type === 'checkbox' || live.type === 'radio') {
17
101
  if (live.checked) {
18
102
  cloned.setAttribute('checked', '');
@@ -24,18 +108,24 @@ export default function enablePersistentFormInputValues(filterBySelector = "[per
24
108
  }
25
109
  });
26
110
 
27
- // Sync textareas
111
+ // Always read the live .value — it's the true source of truth.
112
+ // data-value is useful for cloneNode() outside of snapshots, but here
113
+ // we must use .value because code may have set textarea.value directly
114
+ // without firing an input event (which would leave data-value stale).
28
115
  const liveTextareas = document.querySelectorAll(textareaSelector);
29
116
  const clonedTextareas = doc.querySelectorAll(textareaSelector);
30
117
  clonedTextareas.forEach((cloned, i) => {
31
- cloned.textContent = liveTextareas[i].value;
118
+ const live = liveTextareas[i];
119
+ if (!live) return;
120
+ cloned.textContent = live.value;
121
+ cloned.removeAttribute('data-value');
32
122
  });
33
123
 
34
- // Sync selects
35
124
  const liveSelects = document.querySelectorAll(selectSelector);
36
125
  const clonedSelects = doc.querySelectorAll(selectSelector);
37
126
  clonedSelects.forEach((cloned, i) => {
38
127
  const live = liveSelects[i];
128
+ if (!live) return;
39
129
  const clonedOptions = cloned.querySelectorAll('option');
40
130
  clonedOptions.forEach(opt => opt.removeAttribute('selected'));
41
131
 
@@ -15,6 +15,23 @@ function init() {
15
15
  if (clonedNode.nodeType === Node.ELEMENT_NODE) {
16
16
  processOnclone(clonedNode);
17
17
  clonedNode.querySelectorAll('[onclone]').forEach(processOnclone);
18
+
19
+ // Patch textareas: the persist module writes live values to data-value
20
+ // on every keystroke (because writing textContent on a focused textarea
21
+ // destroys cursor/scroll). Shift data-value into textContent on the
22
+ // clone so consumers get the current value without special handling.
23
+ if (deep) {
24
+ const textareas = clonedNode.tagName === 'TEXTAREA'
25
+ ? [clonedNode]
26
+ : clonedNode.querySelectorAll('textarea[data-value]');
27
+ textareas.forEach(ta => {
28
+ const val = ta.getAttribute('data-value');
29
+ if (val !== null) {
30
+ ta.textContent = val;
31
+ ta.removeAttribute('data-value');
32
+ }
33
+ });
34
+ }
18
35
  }
19
36
 
20
37
  return clonedNode;
package/src/hyperclay.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * DO NOT EDIT THIS FILE DIRECTLY — it is generated from build/hyperclay.template.js
3
3
  *
4
- * HyperclayJS v1.24.1 - Minimal Browser-Native Loader
4
+ * HyperclayJS v1.24.2 - Minimal Browser-Native Loader
5
5
  *
6
6
  * Modules auto-init when imported (no separate init call needed).
7
7
  * Include `export-to-window` feature to export to window.hyperclay.