hyperclayjs 1.6.0 → 1.7.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/core/optionVisibility.js +216 -0
- package/core/savePageCore.js +1 -1
- package/hyperclay.js +2 -2
- package/module-dependency-graph.json +4 -4
- package/package.json +1 -1
- package/core/optionVisibilityRuleGenerator.js +0 -171
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ import 'hyperclayjs/presets/standard.js';
|
|
|
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
62
|
| edit-mode-helpers | 5.4KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
|
|
63
|
-
| option-visibility |
|
|
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
65
|
| save-core | 6.3KB | Basic save function only - hyperclay.savePage() |
|
|
66
66
|
| save-system | 7.1KB | Manual save: keyboard shortcut (CMD+S), save button, change tracking |
|
|
@@ -132,12 +132,12 @@ 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 (~45.5KB)
|
|
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 (~150.2KB)
|
|
141
141
|
All available features
|
|
142
142
|
|
|
143
143
|
Includes all available modules across all categories.
|
|
@@ -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();
|
package/core/savePageCore.js
CHANGED
|
@@ -121,7 +121,7 @@ export function savePage(callback = () => {}) {
|
|
|
121
121
|
|
|
122
122
|
const msg = err.name === 'AbortError'
|
|
123
123
|
? 'Server not responding'
|
|
124
|
-
:
|
|
124
|
+
: 'Save failed';
|
|
125
125
|
|
|
126
126
|
if (typeof callback === 'function') {
|
|
127
127
|
callback({msg, msgType: "error"});
|
package/hyperclay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HyperclayJS v1.
|
|
2
|
+
* HyperclayJS v1.7.0 - Minimal Browser-Native Loader
|
|
3
3
|
*
|
|
4
4
|
* Modules auto-init when imported (no separate init call needed).
|
|
5
5
|
* Include `export-to-window` feature to export to window.hyperclay.
|
|
@@ -31,7 +31,7 @@ const MODULE_PATHS = {
|
|
|
31
31
|
"save-toast": "./core/saveToast.js",
|
|
32
32
|
"edit-mode-helpers": "./core/adminSystem.js",
|
|
33
33
|
"persist": "./core/enablePersistentFormInputValues.js",
|
|
34
|
-
"option-visibility": "./core/
|
|
34
|
+
"option-visibility": "./core/optionVisibility.js",
|
|
35
35
|
"edit-mode": "./core/editmodeSystem.js",
|
|
36
36
|
"event-attrs": "./custom-attributes/events.js",
|
|
37
37
|
"ajax-elements": "./custom-attributes/ajaxElements.js",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"string-utilities/query.js",
|
|
69
69
|
"utilities/cookie.js"
|
|
70
70
|
],
|
|
71
|
-
"core/
|
|
71
|
+
"core/optionVisibility.js": [
|
|
72
72
|
"utilities/mutation.js"
|
|
73
73
|
],
|
|
74
74
|
"core/savePage.js": [
|
|
@@ -242,9 +242,9 @@
|
|
|
242
242
|
"option-visibility": {
|
|
243
243
|
"name": "option-visibility",
|
|
244
244
|
"category": "core",
|
|
245
|
-
"size":
|
|
245
|
+
"size": 5.9,
|
|
246
246
|
"files": [
|
|
247
|
-
"core/
|
|
247
|
+
"core/optionVisibility.js"
|
|
248
248
|
],
|
|
249
249
|
"description": "Dynamic show/hide based on ancestor state with option:attribute=\"value\"",
|
|
250
250
|
"exports": {}
|
|
@@ -678,7 +678,7 @@
|
|
|
678
678
|
"save-toast": "./core/saveToast.js",
|
|
679
679
|
"edit-mode-helpers": "./core/adminSystem.js",
|
|
680
680
|
"persist": "./core/enablePersistentFormInputValues.js",
|
|
681
|
-
"option-visibility": "./core/
|
|
681
|
+
"option-visibility": "./core/optionVisibility.js",
|
|
682
682
|
"edit-mode": "./core/editmodeSystem.js",
|
|
683
683
|
"event-attrs": "./custom-attributes/events.js",
|
|
684
684
|
"ajax-elements": "./custom-attributes/ajaxElements.js",
|
package/package.json
CHANGED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Automatically show/hide elements with "option" attributes based on ancestors' attributes.
|
|
4
|
-
*
|
|
5
|
-
* # Usage:
|
|
6
|
-
* optionVisibilityRuleGenerator.debug = true;
|
|
7
|
-
* optionVisibilityRuleGenerator.start();
|
|
8
|
-
*
|
|
9
|
-
* # HTML Example:
|
|
10
|
-
* <div editmode="true"> <!-- Parent element with matching attribute -->
|
|
11
|
-
* <div option:editmode="true"></div> <!-- This will be visible -->
|
|
12
|
-
* <div option:editmode="false"></div> <!-- This will be hidden -->
|
|
13
|
-
* </div>
|
|
14
|
-
*
|
|
15
|
-
* Elements with `option:` attributes will be:
|
|
16
|
-
* - Visible if any ancestor has matching attribute
|
|
17
|
-
* - Hidden if no ancestor has matching attribute
|
|
18
|
-
*
|
|
19
|
-
*/
|
|
20
|
-
import Mutation from "../utilities/mutation.js";
|
|
21
|
-
|
|
22
|
-
const optionVisibilityRuleGenerator = {
|
|
23
|
-
debug: false,
|
|
24
|
-
styleElement: null,
|
|
25
|
-
|
|
26
|
-
HIDDEN_STYLES: `
|
|
27
|
-
visibility: hidden;
|
|
28
|
-
pointer-events: none;
|
|
29
|
-
width: 0;
|
|
30
|
-
height: 0;
|
|
31
|
-
overflow: hidden;
|
|
32
|
-
`,
|
|
33
|
-
VISIBLE_STYLES: `
|
|
34
|
-
visibility: visible;
|
|
35
|
-
pointer-events: auto;
|
|
36
|
-
width: auto;
|
|
37
|
-
height: auto;
|
|
38
|
-
overflow: visible;
|
|
39
|
-
`,
|
|
40
|
-
|
|
41
|
-
STYLE_CLASS: 'option-visibility-styles',
|
|
42
|
-
|
|
43
|
-
log(message, ...args) {
|
|
44
|
-
if (this.debug) {
|
|
45
|
-
console.log(`[OptionVisibilityRuleGenerator] ${message}`, ...args);
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
findOptionAttributes() {
|
|
50
|
-
const html = document.documentElement.outerHTML;
|
|
51
|
-
const optionAttributes = new Set(); // Using Set for unique combinations
|
|
52
|
-
const optionRegex = /option:([^\s"']+)=["']([^"']+)["']/g; // regex: "option:" + (anything but space and quote) + equal + quote + (anything but quote) + quote
|
|
53
|
-
|
|
54
|
-
let match;
|
|
55
|
-
while ((match = optionRegex.exec(html)) !== null) {
|
|
56
|
-
// Create a unique key for each name-value pair
|
|
57
|
-
const key = JSON.stringify({name: match[1], value: match[2]});
|
|
58
|
-
optionAttributes.add(key);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Convert back to objects
|
|
62
|
-
return Array.from(optionAttributes).map(key => JSON.parse(key));
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
minifyCSS(css) {
|
|
66
|
-
return css
|
|
67
|
-
.replace(/\s+/g, ' ')
|
|
68
|
-
.replace(/{\s+/g, '{')
|
|
69
|
-
.replace(/\s+}/g, '}')
|
|
70
|
-
.replace(/;\s+/g, ';')
|
|
71
|
-
.replace(/:\s+/g, ':')
|
|
72
|
-
.trim();
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
generateCSSRules(optionAttributes) {
|
|
76
|
-
const rules = [];
|
|
77
|
-
|
|
78
|
-
optionAttributes.forEach(({name, value}) => {
|
|
79
|
-
const escapedValue = value.replace(/["\\]/g, '\\$&');
|
|
80
|
-
|
|
81
|
-
rules.push(`
|
|
82
|
-
[option\\:${name}="${escapedValue}"] {
|
|
83
|
-
${this.HIDDEN_STYLES}
|
|
84
|
-
}
|
|
85
|
-
`);
|
|
86
|
-
|
|
87
|
-
rules.push(`
|
|
88
|
-
[${name}="${escapedValue}"] [option\\:${name}="${escapedValue}"] {
|
|
89
|
-
${this.VISIBLE_STYLES}
|
|
90
|
-
}
|
|
91
|
-
`);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return this.minifyCSS(rules.join('\n'));
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
generateRules() {
|
|
98
|
-
try {
|
|
99
|
-
this.log('Starting rule generation');
|
|
100
|
-
|
|
101
|
-
const optionAttributes = this.findOptionAttributes();
|
|
102
|
-
this.log('Found option attributes:', optionAttributes);
|
|
103
|
-
|
|
104
|
-
// Early return if no option attributes found
|
|
105
|
-
if (optionAttributes.length === 0) {
|
|
106
|
-
this.log('No option attributes found, skipping style creation');
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const cssRules = this.generateCSSRules(optionAttributes);
|
|
111
|
-
this.log('Generated CSS rules:', cssRules);
|
|
112
|
-
|
|
113
|
-
// Check if we already have these exact rules
|
|
114
|
-
const existingStyleElement = document.head.querySelector(`.${this.STYLE_CLASS}`);
|
|
115
|
-
if (existingStyleElement && existingStyleElement.textContent.trim() === cssRules) {
|
|
116
|
-
this.log('Rules unchanged, skipping update');
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Create new style element
|
|
121
|
-
const newStyleElement = document.createElement('style');
|
|
122
|
-
newStyleElement.className = this.STYLE_CLASS;
|
|
123
|
-
newStyleElement.textContent = cssRules;
|
|
124
|
-
document.head.appendChild(newStyleElement);
|
|
125
|
-
|
|
126
|
-
// Remove all previous style elements
|
|
127
|
-
document.head
|
|
128
|
-
.querySelectorAll(`.${this.STYLE_CLASS}`)
|
|
129
|
-
.forEach(el => {
|
|
130
|
-
if (el !== newStyleElement) {
|
|
131
|
-
el.remove();
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
this.styleElement = newStyleElement;
|
|
136
|
-
|
|
137
|
-
this.log('Rule generation complete');
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.error('Error generating visibility rules:', error);
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
start() {
|
|
144
|
-
Mutation.onAnyChange({
|
|
145
|
-
selectorFilter: el => [...el.attributes].some(attr => attr.name.startsWith('option:')),
|
|
146
|
-
debounce: 200
|
|
147
|
-
}, () => {
|
|
148
|
-
this.generateRules();
|
|
149
|
-
});
|
|
150
|
-
this.generateRules();
|
|
151
|
-
this.log('Started observing DOM mutations');
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// Auto-export to window unless suppressed by loader
|
|
156
|
-
if (!window.__hyperclayNoAutoExport) {
|
|
157
|
-
window.optionVisibilityRuleGenerator = optionVisibilityRuleGenerator;
|
|
158
|
-
window.hyperclay = window.hyperclay || {};
|
|
159
|
-
window.hyperclay.optionVisibilityRuleGenerator = optionVisibilityRuleGenerator;
|
|
160
|
-
window.h = window.hyperclay;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export default optionVisibilityRuleGenerator;
|
|
164
|
-
|
|
165
|
-
// Auto-initialize
|
|
166
|
-
export function init() {
|
|
167
|
-
optionVisibilityRuleGenerator.start();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Auto-init when module is imported
|
|
171
|
-
init();
|