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 |
|
|
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 |
|
|
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 (~
|
|
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 (~
|
|
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,18 +1,102 @@
|
|
|
1
1
|
import { onSnapshot } from './snapshot.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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.
|