hyperclayjs 1.6.0 → 1.8.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 (61) hide show
  1. package/README.md +14 -14
  2. package/package.json +17 -25
  3. package/src/core/adminContenteditable.js +51 -0
  4. package/{core → src/core}/adminInputs.js +29 -8
  5. package/src/core/adminOnClick.js +54 -0
  6. package/{core → src/core}/adminResources.js +25 -5
  7. package/src/core/optionVisibility.js +216 -0
  8. package/{core → src/core}/savePage.js +1 -1
  9. package/{core → src/core}/savePageCore.js +13 -3
  10. package/{custom-attributes → src/custom-attributes}/domHelpers.js +17 -4
  11. package/{custom-attributes → src/custom-attributes}/events.js +2 -0
  12. package/src/custom-attributes/onmutation.js +90 -0
  13. package/src/custom-attributes/onpagemutation.js +32 -0
  14. package/{custom-attributes → src/custom-attributes}/sortable.js +16 -1
  15. package/{dom-utilities → src/dom-utilities}/All.js +22 -0
  16. package/{hyperclay.js → src/hyperclay.js} +4 -4
  17. package/{module-dependency-graph.json → src/module-dependency-graph.json} +16 -26
  18. package/{ui → src/ui}/prompts.js +13 -18
  19. package/{ui → src/ui}/theModal.js +101 -0
  20. package/{ui → src/ui}/toast.js +4 -3
  21. package/core/adminContenteditable.js +0 -36
  22. package/core/adminOnClick.js +0 -31
  23. package/core/optionVisibilityRuleGenerator.js +0 -171
  24. package/custom-attributes/onpagemutation.js +0 -20
  25. /package/{communication → src/communication}/behaviorCollector.js +0 -0
  26. /package/{communication → src/communication}/sendMessage.js +0 -0
  27. /package/{communication → src/communication}/uploadFile.js +0 -0
  28. /package/{core → src/core}/adminSystem.js +0 -0
  29. /package/{core → src/core}/autosave.js +0 -0
  30. /package/{core → src/core}/editmode.js +0 -0
  31. /package/{core → src/core}/editmodeSystem.js +0 -0
  32. /package/{core → src/core}/enablePersistentFormInputValues.js +0 -0
  33. /package/{core → src/core}/exportToWindow.js +0 -0
  34. /package/{core → src/core}/isAdminOfCurrentResource.js +0 -0
  35. /package/{core → src/core}/saveToast.js +0 -0
  36. /package/{core → src/core}/setPageTypeOnDocumentElement.js +0 -0
  37. /package/{custom-attributes → src/custom-attributes}/ajaxElements.js +0 -0
  38. /package/{custom-attributes → src/custom-attributes}/autosize.js +0 -0
  39. /package/{custom-attributes → src/custom-attributes}/inputHelpers.js +0 -0
  40. /package/{custom-attributes → src/custom-attributes}/onaftersave.js +0 -0
  41. /package/{custom-attributes → src/custom-attributes}/onclickaway.js +0 -0
  42. /package/{custom-attributes → src/custom-attributes}/onclone.js +0 -0
  43. /package/{custom-attributes → src/custom-attributes}/onrender.js +0 -0
  44. /package/{custom-attributes → src/custom-attributes}/preventEnter.js +0 -0
  45. /package/{dom-utilities → src/dom-utilities}/getDataFromForm.js +0 -0
  46. /package/{dom-utilities → src/dom-utilities}/insertStyleTag.js +0 -0
  47. /package/{dom-utilities → src/dom-utilities}/onDomReady.js +0 -0
  48. /package/{dom-utilities → src/dom-utilities}/onLoad.js +0 -0
  49. /package/{string-utilities → src/string-utilities}/copy-to-clipboard.js +0 -0
  50. /package/{string-utilities → src/string-utilities}/query.js +0 -0
  51. /package/{string-utilities → src/string-utilities}/slugify.js +0 -0
  52. /package/{ui → src/ui}/toast-hyperclay.js +0 -0
  53. /package/{utilities → src/utilities}/cookie.js +0 -0
  54. /package/{utilities → src/utilities}/debounce.js +0 -0
  55. /package/{utilities → src/utilities}/loadVendorScript.js +0 -0
  56. /package/{utilities → src/utilities}/mutation.js +0 -0
  57. /package/{utilities → src/utilities}/nearest.js +0 -0
  58. /package/{utilities → src/utilities}/pipe.js +0 -0
  59. /package/{utilities → src/utilities}/throttle.js +0 -0
  60. /package/{vendor → src/vendor}/Sortable.vendor.js +0 -0
  61. /package/{vendor → src/vendor}/idiomorph.min.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
 
@@ -59,10 +59,10 @@ import 'hyperclayjs/presets/standard.js';
59
59
  |--------|------|-------------|
60
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
- | edit-mode-helpers | 5.4KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
63
- | option-visibility | 4.7KB | Dynamic show/hide based on ancestor state with option:attribute="value" |
62
+ | edit-mode-helpers | 7.5KB | 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
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() |
65
+ | save-core | 6.5KB | 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
68
 
@@ -71,18 +71,18 @@ import 'hyperclayjs/presets/standard.js';
71
71
  | Module | Size | Description |
72
72
  |--------|------|-------------|
73
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] |
74
+ | dom-helpers | 6.2KB | el.nearest, el.val, el.text, el.exec, el.cycle |
75
+ | event-attrs | 4.1KB | [onclickaway], [onclone], [onpagemutation], [onrender] |
76
76
  | input-helpers | 1.2KB | [prevent-enter], [autosize] for textareas |
77
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 |
78
+ | sortable | 3.4KB | Drag-drop sorting with [sortable], lazy-loads ~118KB Sortable.js in edit mode |
79
79
 
80
80
  ### UI Components (User interface elements)
81
81
 
82
82
  | Module | Size | Description |
83
83
  |--------|------|-------------|
84
- | dialogs | 8.4KB | ask(), consent(), tell(), snippet() dialog functions |
85
- | the-modal | 19.8KB | Full modal window creation system - window.theModal |
84
+ | dialogs | 7.7KB | ask(), consent(), tell(), snippet() dialog functions |
85
+ | the-modal | 21.8KB | Full modal window creation system - window.theModal |
86
86
  | toast | 7.7KB | Success/error message notifications, toast(msg, msgType) |
87
87
 
88
88
  ### Utilities (Core utilities (often auto-included))
@@ -99,7 +99,7 @@ import 'hyperclayjs/presets/standard.js';
99
99
 
100
100
  | Module | Size | Description |
101
101
  |--------|------|-------------|
102
- | all-js | 14KB | Full DOM manipulation library |
102
+ | all-js | 14.4KB | Full DOM manipulation library |
103
103
  | dom-ready | 0.4KB | DOM ready callback |
104
104
  | form-data | 2KB | Extract form data as an object |
105
105
  | style-injection | 1.1KB | Dynamic stylesheet injection |
@@ -127,17 +127,17 @@ import 'hyperclayjs/presets/standard.js';
127
127
 
128
128
  ## Presets
129
129
 
130
- ### Minimal (~27.8KB)
130
+ ### Minimal (~30.1KB)
131
131
  Essential features for basic editing
132
132
 
133
133
  **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `toast`, `save-toast`, `export-to-window`
134
134
 
135
- ### Standard (~44.3KB)
135
+ ### Standard (~48.8KB)
136
136
  Standard feature set for most use cases
137
137
 
138
138
  **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `persist`, `option-visibility`, `event-attrs`, `dom-helpers`, `toast`, `save-toast`, `export-to-window`
139
139
 
140
- ### Everything (~149KB)
140
+ ### Everything (~155.8KB)
141
141
  All available features
142
142
 
143
143
  Includes all available modules across all categories.
package/package.json CHANGED
@@ -1,39 +1,31 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.6.0",
3
+ "version": "1.8.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,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;
@@ -18,13 +18,28 @@ export function enableAdminInputsOnPageLoad() {
18
18
  if (!isEditMode) return;
19
19
 
20
20
  onDomReady(() => {
21
- document.querySelectorAll('[edit-mode-input]').forEach(input => {
22
- if (supportsReadonly(input)) {
23
- input.removeAttribute('readonly');
24
- } else {
25
- input.removeAttribute('disabled');
26
- }
27
- });
21
+ enableAdminInputs();
22
+ });
23
+ }
24
+
25
+ // Runtime toggle functions
26
+ export function enableAdminInputs() {
27
+ document.querySelectorAll('[edit-mode-input]').forEach(input => {
28
+ if (supportsReadonly(input)) {
29
+ input.removeAttribute('readonly');
30
+ } else {
31
+ input.removeAttribute('disabled');
32
+ }
33
+ });
34
+ }
35
+
36
+ export function disableAdminInputs() {
37
+ document.querySelectorAll('[edit-mode-input]').forEach(input => {
38
+ if (supportsReadonly(input)) {
39
+ input.setAttribute('readonly', '');
40
+ } else {
41
+ input.setAttribute('disabled', '');
42
+ }
28
43
  });
29
44
  }
30
45
 
@@ -55,4 +70,10 @@ function supportsReadonly(element) {
55
70
  export function init() {
56
71
  disableAdminInputsBeforeSave();
57
72
  enableAdminInputsOnPageLoad();
58
- }
73
+ }
74
+
75
+ // Export to window
76
+ window.hyperclay = window.hyperclay || {};
77
+ window.hyperclay.enableAdminInputs = enableAdminInputs;
78
+ window.hyperclay.disableAdminInputs = disableAdminInputs;
79
+ window.h = window.hyperclay;
@@ -0,0 +1,54 @@
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 disableOnClickBeforeSave () {
6
+ beforeSave(docElem => {
7
+ docElem.querySelectorAll('[edit-mode-onclick]').forEach(resource => {
8
+ const originalValue = resource.getAttribute("onclick");
9
+ resource.setAttribute("inert-onclick", originalValue);
10
+ resource.removeAttribute("onclick");
11
+ });
12
+ });
13
+ }
14
+
15
+ export function enableOnClickForAdminOnPageLoad () {
16
+ if (!isEditMode) return;
17
+
18
+ onDomReady(() => {
19
+ enableOnClick();
20
+ });
21
+ }
22
+
23
+ // Runtime toggle functions
24
+ export function enableOnClick() {
25
+ document.querySelectorAll('[edit-mode-onclick]').forEach(el => {
26
+ const val = el.getAttribute("inert-onclick");
27
+ if (val) {
28
+ el.setAttribute("onclick", val);
29
+ el.removeAttribute("inert-onclick");
30
+ }
31
+ });
32
+ }
33
+
34
+ export function disableOnClick() {
35
+ document.querySelectorAll('[edit-mode-onclick]').forEach(el => {
36
+ const val = el.getAttribute("onclick");
37
+ if (val) {
38
+ el.setAttribute("inert-onclick", val);
39
+ el.removeAttribute("onclick");
40
+ }
41
+ });
42
+ }
43
+
44
+ // Auto-initialize
45
+ export function init() {
46
+ disableOnClickBeforeSave();
47
+ enableOnClickForAdminOnPageLoad();
48
+ }
49
+
50
+ // Export to window
51
+ window.hyperclay = window.hyperclay || {};
52
+ window.hyperclay.enableOnClick = enableOnClick;
53
+ window.hyperclay.disableOnClick = disableOnClick;
54
+ window.h = window.hyperclay;
@@ -18,11 +18,25 @@ export function enableAdminResourcesOnPageLoad () {
18
18
  if (!isEditMode) return;
19
19
 
20
20
  onDomReady(() => {
21
- document.querySelectorAll('[edit-mode-resource]:is(style, link, script)[type^="inert/"]').forEach(resource => {
22
- // works for js and css
23
- resource.type = resource.type.replace(/inert\//g, '');
21
+ enableAdminResources();
22
+ });
23
+ }
24
+
25
+ // Runtime toggle functions
26
+ export function enableAdminResources() {
27
+ document.querySelectorAll('[edit-mode-resource]:is(style, link, script)[type^="inert/"]').forEach(resource => {
28
+ resource.type = resource.type.replace(/inert\//g, '');
29
+ resource.replaceWith(resource.cloneNode(true));
30
+ });
31
+ }
32
+
33
+ export function disableAdminResources() {
34
+ document.querySelectorAll('[edit-mode-resource]:is(style, link, script)').forEach(resource => {
35
+ const currentType = resource.getAttribute('type') || 'text/javascript';
36
+ if (!currentType.startsWith('inert/')) {
37
+ resource.setAttribute('type', `inert/${currentType}`);
24
38
  resource.replaceWith(resource.cloneNode(true));
25
- });
39
+ }
26
40
  });
27
41
  }
28
42
 
@@ -30,4 +44,10 @@ export function enableAdminResourcesOnPageLoad () {
30
44
  export function init() {
31
45
  disableAdminResourcesBeforeSave();
32
46
  enableAdminResourcesOnPageLoad();
33
- }
47
+ }
48
+
49
+ // Export to window
50
+ window.hyperclay = window.hyperclay || {};
51
+ window.hyperclay.enableAdminResources = enableAdminResources;
52
+ window.hyperclay.disableAdminResources = disableAdminResources;
53
+ window.h = window.hyperclay;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Option Visibility (CSS Layers Implementation)
3
+ *
4
+ * Shows/hides elements based on `option:` attributes and ancestor matches.
5
+ *
6
+ * Usage:
7
+ * <div editmode="true">
8
+ * <button option:editmode="true">Visible</button>
9
+ * <button option:editmode="false">Hidden</button>
10
+ * </div>
11
+ *
12
+ * An element with `option:name="value"` is hidden by default.
13
+ * It becomes visible when ANY ancestor has `name="value"`.
14
+ *
15
+ * ---
16
+ *
17
+ * HOW IT WORKS:
18
+ * 1. Uses `display: none !important` to forcefully hide elements
19
+ * 2. Uses `display: revert-layer !important` to un-hide when ancestor matches
20
+ * `revert-layer` tells the browser: "Ignore rules in this layer, fall back to author styles"
21
+ * 3. This preserves the user's original `display` (flex, grid, block) without us knowing what it is
22
+ *
23
+ * BROWSER SUPPORT:
24
+ * Requires `@layer` and `revert-layer` support (~92% of browsers, 2022+).
25
+ * Falls back gracefully - elements remain visible if unsupported.
26
+ *
27
+ * TRADEOFFS:
28
+ * - Pro: Pure CSS after generation, zero JS overhead for toggling
29
+ * - Pro: Simple code, similar to original approach
30
+ * - Con: Loses to user `!important` rules (layered !important < unlayered !important)
31
+ */
32
+
33
+ import Mutation from "../utilities/mutation.js";
34
+
35
+ const optionVisibility = {
36
+ debug: false,
37
+ _started: false,
38
+ _styleElement: null,
39
+ _unsubscribe: null,
40
+
41
+ LAYER_NAME: 'option-visibility',
42
+ STYLE_CLASS: 'option-visibility-layer-styles',
43
+
44
+ log(...args) {
45
+ if (this.debug) console.log('[OptionVisibility:Layer]', ...args);
46
+ },
47
+
48
+ /**
49
+ * Check if browser supports the layer approach
50
+ */
51
+ isSupported() {
52
+ return typeof CSS !== 'undefined'
53
+ && typeof CSS.supports === 'function'
54
+ && CSS.supports('display', 'revert-layer');
55
+ },
56
+
57
+ /**
58
+ * Find all unique option:name="value" patterns using XPath (faster than regex on HTML)
59
+ */
60
+ findOptionAttributes() {
61
+ const patterns = new Map();
62
+
63
+ try {
64
+ const snapshot = document.evaluate(
65
+ '//*[@*[starts-with(name(), "option:")]]',
66
+ document.documentElement,
67
+ null,
68
+ XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
69
+ null
70
+ );
71
+
72
+ for (let i = 0; i < snapshot.snapshotLength; i++) {
73
+ const el = snapshot.snapshotItem(i);
74
+ for (const attr of el.attributes) {
75
+ if (attr.name.startsWith('option:')) {
76
+ const name = attr.name.slice(7);
77
+ const value = attr.value;
78
+ const key = `${name}=${value}`;
79
+ if (!patterns.has(key)) {
80
+ patterns.set(key, { name, value });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ } catch (error) {
86
+ this.log('XPath error, falling back to empty', error);
87
+ }
88
+
89
+ return [...patterns.values()];
90
+ },
91
+
92
+ /**
93
+ * Generate CSS rules wrapped in @layer
94
+ */
95
+ generateCSS(attributes) {
96
+ if (!attributes.length) return '';
97
+
98
+ const rules = attributes.map(({ name, value }) => {
99
+ const safeName = CSS.escape(name);
100
+ const safeValue = CSS.escape(value);
101
+
102
+ // Hidden by default, visible when ancestor matches
103
+ // Both rules need !important for consistency within the layer
104
+ return `[option\\:${safeName}="${safeValue}"]{display:none!important}[${safeName}="${safeValue}"] [option\\:${safeName}="${safeValue}"]{display:revert-layer!important}`;
105
+ }).join('');
106
+
107
+ return `@layer ${this.LAYER_NAME}{${rules}}`;
108
+ },
109
+
110
+ /**
111
+ * Update the style element with current rules
112
+ */
113
+ update() {
114
+ if (!this.isSupported()) {
115
+ this.log('Browser lacks revert-layer support, skipping');
116
+ return;
117
+ }
118
+
119
+ try {
120
+ const attributes = this.findOptionAttributes();
121
+ const css = this.generateCSS(attributes);
122
+
123
+ // Remove style element if no attributes
124
+ if (!css) {
125
+ if (this._styleElement) {
126
+ this._styleElement.remove();
127
+ this._styleElement = null;
128
+ this.log('Removed empty style element');
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Skip if unchanged
134
+ if (this._styleElement?.textContent === css) {
135
+ this.log('Styles unchanged');
136
+ return;
137
+ }
138
+
139
+ // Create or update
140
+ if (!this._styleElement) {
141
+ this._styleElement = document.createElement('style');
142
+ this._styleElement.className = this.STYLE_CLASS;
143
+ document.head.appendChild(this._styleElement);
144
+ }
145
+
146
+ this._styleElement.textContent = css;
147
+ this.log(`Generated ${attributes.length} rules`);
148
+
149
+ } catch (error) {
150
+ console.error('[OptionVisibility:Layer] Error generating rules:', error);
151
+ }
152
+ },
153
+
154
+ start() {
155
+ if (this._started) return;
156
+
157
+ if (document.readyState === 'loading') {
158
+ document.addEventListener('DOMContentLoaded', () => this.start(), { once: true });
159
+ return;
160
+ }
161
+
162
+ this._started = true;
163
+
164
+ if (!this.isSupported()) {
165
+ console.warn('[OptionVisibility:Layer] Browser lacks revert-layer support. Elements will remain visible.');
166
+ return;
167
+ }
168
+
169
+ this.update();
170
+
171
+ // selectorFilter only triggers on option:* attribute changes (new patterns).
172
+ // Ancestor attribute changes (e.g., editmode="true" -> "false") are handled
173
+ // automatically by the browser - CSS rules re-evaluate when attributes change.
174
+ this._unsubscribe = Mutation.onAnyChange({
175
+ debounce: 200,
176
+ selectorFilter: el => [...el.attributes].some(attr => attr.name.startsWith('option:')),
177
+ omitChangeDetails: true
178
+ }, () => this.update());
179
+
180
+ this.log('Started');
181
+ },
182
+
183
+ stop() {
184
+ if (!this._started) return;
185
+
186
+ this._started = false;
187
+
188
+ if (this._unsubscribe) {
189
+ this._unsubscribe();
190
+ this._unsubscribe = null;
191
+ }
192
+
193
+ if (this._styleElement) {
194
+ this._styleElement.remove();
195
+ this._styleElement = null;
196
+ }
197
+
198
+ this.log('Stopped');
199
+ }
200
+ };
201
+
202
+ // Auto-export
203
+ if (!window.__hyperclayNoAutoExport) {
204
+ window.optionVisibility = optionVisibility;
205
+ window.hyperclay = window.hyperclay || {};
206
+ window.hyperclay.optionVisibility = optionVisibility;
207
+ window.h = window.hyperclay;
208
+ }
209
+
210
+ export default optionVisibility;
211
+
212
+ export function init() {
213
+ optionVisibility.start();
214
+ }
215
+
216
+ init();
@@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => {
111
111
  * @param {Function} callback - Optional callback for custom handling
112
112
  */
113
113
  export function savePage(callback = () => {}) {
114
- if (!isEditMode) {
114
+ if (!isEditMode && !window.hyperclay?.testMode) {
115
115
  return;
116
116
  }
117
117
 
@@ -73,11 +73,21 @@ export function getPageContents() {
73
73
  * });
74
74
  */
75
75
  export function savePage(callback = () => {}) {
76
- if (!isEditMode || saveInProgress) {
76
+ if (saveInProgress) {
77
+ return;
78
+ }
79
+ if (!isEditMode && !window.hyperclay?.testMode) {
77
80
  return;
78
81
  }
79
82
 
80
- const currentContents = getPageContents();
83
+ let currentContents;
84
+ try {
85
+ currentContents = getPageContents();
86
+ } catch (err) {
87
+ console.error('savePage: getPageContents failed', err);
88
+ callback({msg: err.message, msgType: "error"});
89
+ return;
90
+ }
81
91
  saveInProgress = true;
82
92
 
83
93
  // Test mode: skip network request, return mock success
@@ -121,7 +131,7 @@ export function savePage(callback = () => {}) {
121
131
 
122
132
  const msg = err.name === 'AbortError'
123
133
  ? 'Server not responding'
124
- : (err.message || 'Failed to save');
134
+ : 'Save failed';
125
135
 
126
136
  if (typeof callback === 'function') {
127
137
  callback({msg, msgType: "error"});
@@ -24,22 +24,35 @@ function init () {
24
24
  }
25
25
  });
26
26
 
27
- // elem.val.project returns the value of the nearest "project" attribute
28
- // elem.val.project = "hello world" sets the value of the nearest "project" attribute
27
+ // elem.val.project returns the value of the nearest element with "project" attribute
28
+ // elem.val.project = "hello world" sets the value of the nearest element with "project" attribute
29
+ // For form elements (input/select/textarea), uses the value property; otherwise uses the attribute
29
30
  Object.defineProperty(HTMLElement.prototype, 'val', {
30
31
  configurable: true,
31
32
  get: function() {
32
33
  let element = this;
33
34
 
35
+ const isFormElement = (elem) =>
36
+ elem.tagName === 'INPUT' || elem.tagName === 'SELECT' || elem.tagName === 'TEXTAREA';
37
+
34
38
  const handler = {
35
39
  get(target, prop) {
36
- return nearest(element, `[${prop}], .${prop}`, elem => elem.getAttribute(prop));
40
+ return nearest(element, `[${prop}], .${prop}`, elem => {
41
+ if (isFormElement(elem)) {
42
+ return elem.value;
43
+ }
44
+ return elem.getAttribute(prop);
45
+ });
37
46
  },
38
47
  set(target, prop, value) {
39
48
  const foundElem = nearest(element, `[${prop}], .${prop}`);
40
49
 
41
50
  if (foundElem) {
42
- foundElem.setAttribute(prop, value);
51
+ if (isFormElement(foundElem)) {
52
+ foundElem.value = value;
53
+ } else {
54
+ foundElem.setAttribute(prop, value);
55
+ }
43
56
  }
44
57
 
45
58
  return true;
@@ -1,12 +1,14 @@
1
1
  // Events module - combines all event attribute handlers
2
2
  import { init as initOnclickaway } from './onclickaway.js';
3
3
  import { init as initOnclone } from './onclone.js';
4
+ import { init as initOnmutation } from './onmutation.js';
4
5
  import { init as initOnpagemutation } from './onpagemutation.js';
5
6
  import { init as initOnrender } from './onrender.js';
6
7
 
7
8
  function init() {
8
9
  initOnclickaway();
9
10
  initOnclone();
11
+ initOnmutation();
10
12
  initOnpagemutation();
11
13
  initOnrender();
12
14
  }