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.
- package/README.md +14 -14
- package/package.json +17 -25
- package/src/core/adminContenteditable.js +51 -0
- package/{core → src/core}/adminInputs.js +29 -8
- package/src/core/adminOnClick.js +54 -0
- package/{core → src/core}/adminResources.js +25 -5
- package/src/core/optionVisibility.js +216 -0
- package/{core → src/core}/savePage.js +1 -1
- package/{core → src/core}/savePageCore.js +13 -3
- package/{custom-attributes → src/custom-attributes}/domHelpers.js +17 -4
- package/{custom-attributes → src/custom-attributes}/events.js +2 -0
- package/src/custom-attributes/onmutation.js +90 -0
- package/src/custom-attributes/onpagemutation.js +32 -0
- package/{custom-attributes → src/custom-attributes}/sortable.js +16 -1
- package/{dom-utilities → src/dom-utilities}/All.js +22 -0
- package/{hyperclay.js → src/hyperclay.js} +4 -4
- package/{module-dependency-graph.json → src/module-dependency-graph.json} +16 -26
- package/{ui → src/ui}/prompts.js +13 -18
- package/{ui → src/ui}/theModal.js +101 -0
- package/{ui → src/ui}/toast.js +4 -3
- package/core/adminContenteditable.js +0 -36
- package/core/adminOnClick.js +0 -31
- package/core/optionVisibilityRuleGenerator.js +0 -171
- package/custom-attributes/onpagemutation.js +0 -20
- /package/{communication → src/communication}/behaviorCollector.js +0 -0
- /package/{communication → src/communication}/sendMessage.js +0 -0
- /package/{communication → src/communication}/uploadFile.js +0 -0
- /package/{core → src/core}/adminSystem.js +0 -0
- /package/{core → src/core}/autosave.js +0 -0
- /package/{core → src/core}/editmode.js +0 -0
- /package/{core → src/core}/editmodeSystem.js +0 -0
- /package/{core → src/core}/enablePersistentFormInputValues.js +0 -0
- /package/{core → src/core}/exportToWindow.js +0 -0
- /package/{core → src/core}/isAdminOfCurrentResource.js +0 -0
- /package/{core → src/core}/saveToast.js +0 -0
- /package/{core → src/core}/setPageTypeOnDocumentElement.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/ajaxElements.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/autosize.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/inputHelpers.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/onaftersave.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/onclickaway.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/onclone.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/onrender.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/preventEnter.js +0 -0
- /package/{dom-utilities → src/dom-utilities}/getDataFromForm.js +0 -0
- /package/{dom-utilities → src/dom-utilities}/insertStyleTag.js +0 -0
- /package/{dom-utilities → src/dom-utilities}/onDomReady.js +0 -0
- /package/{dom-utilities → src/dom-utilities}/onLoad.js +0 -0
- /package/{string-utilities → src/string-utilities}/copy-to-clipboard.js +0 -0
- /package/{string-utilities → src/string-utilities}/query.js +0 -0
- /package/{string-utilities → src/string-utilities}/slugify.js +0 -0
- /package/{ui → src/ui}/toast-hyperclay.js +0 -0
- /package/{utilities → src/utilities}/cookie.js +0 -0
- /package/{utilities → src/utilities}/debounce.js +0 -0
- /package/{utilities → src/utilities}/loadVendorScript.js +0 -0
- /package/{utilities → src/utilities}/mutation.js +0 -0
- /package/{utilities → src/utilities}/nearest.js +0 -0
- /package/{utilities → src/utilities}/pipe.js +0 -0
- /package/{utilities → src/utilities}/throttle.js +0 -0
- /package/{vendor → src/vendor}/Sortable.vendor.js +0 -0
- /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 |
|
|
63
|
-
| option-visibility |
|
|
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.
|
|
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 |
|
|
75
|
-
| event-attrs |
|
|
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 |
|
|
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 |
|
|
85
|
-
| the-modal |
|
|
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 |
|
|
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 (~
|
|
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 (~
|
|
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 (~
|
|
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.
|
|
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
|
-
"
|
|
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/
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 (
|
|
76
|
+
if (saveInProgress) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!isEditMode && !window.hyperclay?.testMode) {
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
|
|
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
|
-
:
|
|
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 =>
|
|
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
|
|
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
|
}
|