hyperclayjs 1.14.1 → 1.15.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 +3 -3
- package/package.json +1 -1
- package/src/core/optionVisibility.js +83 -26
- package/src/hyperclay.js +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ import 'hyperclayjs/presets/standard.js';
|
|
|
60
60
|
| autosave | 0.9KB | Auto-save on DOM changes |
|
|
61
61
|
| edit-mode | 1.8KB | Toggle edit mode on hyperclay on/off |
|
|
62
62
|
| edit-mode-helpers | 7.5KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
|
|
63
|
-
| option-visibility |
|
|
63
|
+
| option-visibility | 7.8KB | Dynamic show/hide based on ancestor state with option:attribute="value" |
|
|
64
64
|
| persist | 2.4KB | Persist input/select/textarea values to the DOM with [persist] attribute |
|
|
65
65
|
| save-core | 6.8KB | Basic save function only - hyperclay.savePage() |
|
|
66
66
|
| save-system | 9.6KB | CMD+S, [trigger-save] button, savestatus attribute |
|
|
@@ -137,12 +137,12 @@ Essential features for basic editing
|
|
|
137
137
|
|
|
138
138
|
**Modules:** `save-core`, `snapshot`, `save-system`, `edit-mode-helpers`, `toast`, `save-toast`, `export-to-window`, `view-mode-excludes-edit-modules`
|
|
139
139
|
|
|
140
|
-
### Standard (~
|
|
140
|
+
### Standard (~62.4KB)
|
|
141
141
|
Standard feature set for most use cases
|
|
142
142
|
|
|
143
143
|
**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`
|
|
144
144
|
|
|
145
|
-
### Everything (~
|
|
145
|
+
### Everything (~184.6KB)
|
|
146
146
|
All available features
|
|
147
147
|
|
|
148
148
|
Includes all available modules across all categories.
|
package/package.json
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Option Visibility (CSS Layers Implementation)
|
|
3
3
|
*
|
|
4
|
-
* Shows/hides elements based on `option:`
|
|
4
|
+
* Shows/hides elements based on `option:` and `option-not:` attributes.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* SYNTAX:
|
|
7
|
+
* option:name="value" - Show when ancestor has name="value"
|
|
8
|
+
* option:name="a|b|c" - Show when ancestor has name="a" OR "b" OR "c"
|
|
9
|
+
* option:name="" - Show when ancestor has name="" (empty value)
|
|
10
|
+
* option:name="|saved" - Show when ancestor has name="" OR name="saved"
|
|
11
|
+
* option-not:name="value" - Show when ancestor has name attr but ≠ "value"
|
|
12
|
+
* option-not:name="a|b" - Show when ancestor has name attr but ≠ "a" AND ≠ "b"
|
|
13
|
+
*
|
|
14
|
+
* EXAMPLES:
|
|
7
15
|
* <div editmode="true">
|
|
8
|
-
* <button option:editmode="true">Visible</button>
|
|
16
|
+
* <button option:editmode="true">Visible in edit mode</button>
|
|
9
17
|
* <button option:editmode="false">Hidden</button>
|
|
10
18
|
* </div>
|
|
11
19
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
20
|
+
* <div savestatus="error">
|
|
21
|
+
* <span option:savestatus="saved|error">Visible (matches error)</span>
|
|
22
|
+
* <span option-not:savestatus="saving">Visible (error ≠ saving)</span>
|
|
23
|
+
* </div>
|
|
16
24
|
*
|
|
17
25
|
* HOW IT WORKS:
|
|
18
26
|
* 1. Uses `display: none !important` to forcefully hide elements
|
|
@@ -21,13 +29,14 @@
|
|
|
21
29
|
* 3. This preserves the user's original `display` (flex, grid, block) without us knowing what it is
|
|
22
30
|
*
|
|
23
31
|
* BROWSER SUPPORT:
|
|
24
|
-
* Requires `@layer
|
|
32
|
+
* Requires `@layer`, `revert-layer`, and `:not()` selector lists (~92% of browsers, 2022+).
|
|
25
33
|
* Falls back gracefully - elements remain visible if unsupported.
|
|
26
34
|
*
|
|
27
35
|
* TRADEOFFS:
|
|
28
36
|
* - Pro: Pure CSS after generation, zero JS overhead for toggling
|
|
29
37
|
* - Pro: Simple code, similar to original approach
|
|
30
38
|
* - Con: Loses to user `!important` rules (layered !important < unlayered !important)
|
|
39
|
+
* - Con: Pipe character `|` cannot be used as a literal value (reserved as OR delimiter)
|
|
31
40
|
*/
|
|
32
41
|
|
|
33
42
|
import Mutation from "../utilities/mutation.js";
|
|
@@ -35,6 +44,34 @@ import insertStyles from "../dom-utilities/insertStyleTag.js";
|
|
|
35
44
|
|
|
36
45
|
const STYLE_NAME = 'option-visibility';
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Parse an option:/option-not: attribute into a pattern object.
|
|
49
|
+
* Pure function for easy testing.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} attrName - Attribute name (e.g., 'option:editmode', 'option-not:status')
|
|
52
|
+
* @param {string} attrValue - Attribute value (e.g., 'true', 'a|b|c')
|
|
53
|
+
* @returns {Object|null} Pattern object or null if not a valid option attribute
|
|
54
|
+
*/
|
|
55
|
+
export function parseOptionAttribute(attrName, attrValue) {
|
|
56
|
+
let negated = false;
|
|
57
|
+
let name;
|
|
58
|
+
|
|
59
|
+
if (attrName.startsWith('option-not:')) {
|
|
60
|
+
negated = true;
|
|
61
|
+
name = attrName.slice(11);
|
|
62
|
+
} else if (attrName.startsWith('option:')) {
|
|
63
|
+
name = attrName.slice(7);
|
|
64
|
+
} else {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rawValue = attrValue;
|
|
69
|
+
// Split by pipe, keep empty strings (they match empty attribute values)
|
|
70
|
+
const values = rawValue.split('|');
|
|
71
|
+
|
|
72
|
+
return { name, rawValue, values, negated };
|
|
73
|
+
}
|
|
74
|
+
|
|
38
75
|
const optionVisibility = {
|
|
39
76
|
debug: false,
|
|
40
77
|
_started: false,
|
|
@@ -54,14 +91,15 @@ const optionVisibility = {
|
|
|
54
91
|
},
|
|
55
92
|
|
|
56
93
|
/**
|
|
57
|
-
* Find all unique option:
|
|
94
|
+
* Find all unique option:/option-not: patterns using XPath
|
|
95
|
+
* Returns array of { name, rawValue, values, negated }
|
|
58
96
|
*/
|
|
59
97
|
findOptionAttributes() {
|
|
60
98
|
const patterns = new Map();
|
|
61
99
|
|
|
62
100
|
try {
|
|
63
101
|
const snapshot = document.evaluate(
|
|
64
|
-
'//*[@*[starts-with(name(), "option:")]]',
|
|
102
|
+
'//*[@*[starts-with(name(), "option:") or starts-with(name(), "option-not:")]]',
|
|
65
103
|
document.documentElement,
|
|
66
104
|
null,
|
|
67
105
|
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
|
|
@@ -71,13 +109,12 @@ const optionVisibility = {
|
|
|
71
109
|
for (let i = 0; i < snapshot.snapshotLength; i++) {
|
|
72
110
|
const el = snapshot.snapshotItem(i);
|
|
73
111
|
for (const attr of el.attributes) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
112
|
+
const pattern = parseOptionAttribute(attr.name, attr.value);
|
|
113
|
+
if (!pattern) continue;
|
|
114
|
+
|
|
115
|
+
const key = `${pattern.negated ? '!' : ''}${pattern.name}=${pattern.rawValue}`;
|
|
116
|
+
if (!patterns.has(key)) {
|
|
117
|
+
patterns.set(key, pattern);
|
|
81
118
|
}
|
|
82
119
|
}
|
|
83
120
|
}
|
|
@@ -91,16 +128,34 @@ const optionVisibility = {
|
|
|
91
128
|
/**
|
|
92
129
|
* Generate CSS rules wrapped in @layer
|
|
93
130
|
*/
|
|
94
|
-
generateCSS(
|
|
95
|
-
if (!
|
|
131
|
+
generateCSS(patterns) {
|
|
132
|
+
if (!patterns.length) return '';
|
|
96
133
|
|
|
97
|
-
const rules =
|
|
134
|
+
const rules = patterns.map(({ name, rawValue, values, negated }) => {
|
|
98
135
|
const safeName = CSS.escape(name);
|
|
99
|
-
const
|
|
136
|
+
const safeRawValue = CSS.escape(rawValue);
|
|
137
|
+
const prefix = negated ? 'option-not' : 'option';
|
|
138
|
+
const attrSelector = `[${prefix}\\:${safeName}="${safeRawValue}"]`;
|
|
139
|
+
|
|
140
|
+
// Hide rule (same for both types)
|
|
141
|
+
const hideRule = `${attrSelector}{display:none!important}`;
|
|
142
|
+
|
|
143
|
+
// Show rule depends on type
|
|
144
|
+
let showRule;
|
|
145
|
+
if (negated) {
|
|
146
|
+
// option-not: show when ancestor has attr but NOT any of the values
|
|
147
|
+
// Uses :not(sel1, sel2) selector list syntax
|
|
148
|
+
const notList = values.map(v => `[${safeName}="${CSS.escape(v)}"]`).join(',');
|
|
149
|
+
showRule = `[${safeName}]:not(${notList}) ${attrSelector}{display:revert-layer!important}`;
|
|
150
|
+
} else {
|
|
151
|
+
// option: show when ancestor has ANY of the values
|
|
152
|
+
const showSelectors = values.map(v =>
|
|
153
|
+
`[${safeName}="${CSS.escape(v)}"] ${attrSelector}`
|
|
154
|
+
).join(',');
|
|
155
|
+
showRule = `${showSelectors}{display:revert-layer!important}`;
|
|
156
|
+
}
|
|
100
157
|
|
|
101
|
-
|
|
102
|
-
// Both rules need !important for consistency within the layer
|
|
103
|
-
return `[option\\:${safeName}="${safeValue}"]{display:none!important}[${safeName}="${safeValue}"] [option\\:${safeName}="${safeValue}"]{display:revert-layer!important}`;
|
|
158
|
+
return hideRule + showRule;
|
|
104
159
|
}).join('');
|
|
105
160
|
|
|
106
161
|
return `@layer ${STYLE_NAME}{${rules}}`;
|
|
@@ -146,12 +201,14 @@ const optionVisibility = {
|
|
|
146
201
|
|
|
147
202
|
this.update();
|
|
148
203
|
|
|
149
|
-
// selectorFilter only triggers on option
|
|
204
|
+
// selectorFilter only triggers on option:/option-not: attribute changes (new patterns).
|
|
150
205
|
// Ancestor attribute changes (e.g., editmode="true" -> "false") are handled
|
|
151
206
|
// automatically by the browser - CSS rules re-evaluate when attributes change.
|
|
152
207
|
this._unsubscribe = Mutation.onAnyChange({
|
|
153
208
|
debounce: 200,
|
|
154
|
-
selectorFilter: el => [...el.attributes].some(attr =>
|
|
209
|
+
selectorFilter: el => [...el.attributes].some(attr =>
|
|
210
|
+
attr.name.startsWith('option:') || attr.name.startsWith('option-not:')
|
|
211
|
+
),
|
|
155
212
|
omitChangeDetails: true
|
|
156
213
|
}, () => this.update());
|
|
157
214
|
|
package/src/hyperclay.js
CHANGED