hyperclayjs 1.0.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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/README.template.md +276 -0
  4. package/communication/behaviorCollector.js +230 -0
  5. package/communication/sendMessage.js +48 -0
  6. package/communication/uploadFile.js +348 -0
  7. package/core/adminContenteditable.js +36 -0
  8. package/core/adminInputs.js +58 -0
  9. package/core/adminOnClick.js +31 -0
  10. package/core/adminResources.js +33 -0
  11. package/core/adminSystem.js +15 -0
  12. package/core/editmode.js +8 -0
  13. package/core/editmodeSystem.js +18 -0
  14. package/core/enablePersistentFormInputValues.js +62 -0
  15. package/core/isAdminOfCurrentResource.js +13 -0
  16. package/core/optionVisibilityRuleGenerator.js +160 -0
  17. package/core/savePage.js +196 -0
  18. package/core/savePageCore.js +236 -0
  19. package/core/setPageTypeOnDocumentElement.js +23 -0
  20. package/custom-attributes/ajaxElements.js +94 -0
  21. package/custom-attributes/autosize.js +17 -0
  22. package/custom-attributes/domHelpers.js +175 -0
  23. package/custom-attributes/events.js +15 -0
  24. package/custom-attributes/inputHelpers.js +11 -0
  25. package/custom-attributes/onclickaway.js +27 -0
  26. package/custom-attributes/onclone.js +35 -0
  27. package/custom-attributes/onpagemutation.js +20 -0
  28. package/custom-attributes/onrender.js +30 -0
  29. package/custom-attributes/preventEnter.js +13 -0
  30. package/custom-attributes/sortable.js +76 -0
  31. package/dom-utilities/All.js +412 -0
  32. package/dom-utilities/getDataFromForm.js +60 -0
  33. package/dom-utilities/insertStyleTag.js +28 -0
  34. package/dom-utilities/onDomReady.js +7 -0
  35. package/dom-utilities/onLoad.js +7 -0
  36. package/hyperclay.js +465 -0
  37. package/module-dependency-graph.json +612 -0
  38. package/package.json +95 -0
  39. package/string-utilities/copy-to-clipboard.js +35 -0
  40. package/string-utilities/emmet-html.js +54 -0
  41. package/string-utilities/query.js +1 -0
  42. package/string-utilities/slugify.js +21 -0
  43. package/ui/info.js +39 -0
  44. package/ui/prompts.js +179 -0
  45. package/ui/theModal.js +677 -0
  46. package/ui/toast.js +273 -0
  47. package/utilities/cookie.js +45 -0
  48. package/utilities/debounce.js +12 -0
  49. package/utilities/mutation.js +403 -0
  50. package/utilities/nearest.js +97 -0
  51. package/utilities/pipe.js +1 -0
  52. package/utilities/throttle.js +21 -0
  53. package/vendor/Sortable.js +3351 -0
  54. package/vendor/idiomorph.min.js +8 -0
  55. package/vendor/tailwind-base.css +1471 -0
  56. package/vendor/tailwind-play.js +169 -0
@@ -0,0 +1,13 @@
1
+ // [prevent-enter]: Prevents Enter key from creating newlines
2
+ function init () {
3
+ document.addEventListener('keydown', function(event) {
4
+ if (event.key === 'Enter' || event.keyCode === 13) {
5
+ const preventEnterElement = event.target.closest('[prevent-enter]');
6
+ if (preventEnterElement) {
7
+ event.preventDefault();
8
+ }
9
+ }
10
+ });
11
+ }
12
+ export { init };
13
+ export default init;
@@ -0,0 +1,76 @@
1
+ /*
2
+
3
+ Make elements drag-and-drop sortable
4
+
5
+ How to use:
6
+ - add `sortable` attribute to an element to make children sortable
7
+ - e.g. <div sortable></div>
8
+ - add `onsorted` attribute to execute code when items are sorted
9
+ - e.g. <ul sortable onsorted="console.log('Items reordered!')"></ul>
10
+
11
+ */
12
+ import { isEditMode, isOwner } from "../core/isAdminOfCurrentResource.js";
13
+ import Mutation from "../utilities/mutation.js";
14
+ import Sortable from '../vendor/Sortable.js';
15
+
16
+ function makeSortable (sortableElem) {
17
+ let options = {};
18
+
19
+ // Check if Sortable instance already exists
20
+ if (Sortable.get(sortableElem)) return;
21
+
22
+ const groupName = sortableElem.getAttribute('sortable');
23
+ if (groupName) options.group = groupName;
24
+
25
+ // Check for handles, but exclude those inside nested sortable elements
26
+ const handles = sortableElem.querySelectorAll('[sortable-handle]');
27
+ const nestedSortables = sortableElem.querySelectorAll('[sortable]');
28
+
29
+ // Check if any handle is NOT inside a nested sortable
30
+ const hasDirectHandle = Array.from(handles).some(handle => {
31
+ return !Array.from(nestedSortables).some(nested => nested.contains(handle));
32
+ });
33
+
34
+ if (hasDirectHandle) {
35
+ options.handle = '[sortable-handle]';
36
+ }
37
+
38
+ // Add onsorted callback if attribute exists
39
+ const onsortedCode = sortableElem.getAttribute('onsorted');
40
+ if (onsortedCode) {
41
+ options.onEnd = function(evt) {
42
+ try {
43
+ const asyncFn = new Function(`return (async function(evt) { ${onsortedCode} })`)();
44
+ asyncFn.call(sortableElem, evt);
45
+ } catch (error) {
46
+ console.error('Error in onsorted execution:', error);
47
+ }
48
+ };
49
+ }
50
+
51
+ Sortable.create(sortableElem, options);
52
+ }
53
+
54
+ function init () {
55
+ if (!isEditMode) return;
56
+
57
+ // Set up sortable on page load
58
+ document.querySelectorAll('[sortable]').forEach(makeSortable);
59
+
60
+ // Set up listener for dynamically added elements
61
+ Mutation.onAddElement({
62
+ selectorFilter: "[sortable]",
63
+ debounce: 200
64
+ }, (changes) => {
65
+ changes.forEach(({ element }) => {
66
+ makeSortable(element);
67
+ });
68
+ });
69
+
70
+ // ❗️re-initializing sortable on parent elements isn't necessary
71
+ // sortable.js handles this automatically
72
+ // ❌ onElementAdded(newElem => makeSortable(newElem.closest('[sortable]')))
73
+ }
74
+
75
+ export { init };
76
+ export default init;
@@ -0,0 +1,412 @@
1
+ // Helper to check if an array contains DOM nodes
2
+ const isElementArray = (arr) => {
3
+ if (!Array.isArray(arr)) return false;
4
+ // Empty arrays from element operations should stay wrapped for chainability
5
+ if (arr.length === 0) return true;
6
+ return arr.every(item => item instanceof Element || item instanceof Node || item instanceof Document);
7
+ };
8
+
9
+ const createMethodHandler = (elements, plugins, methods) => ({
10
+ get(target, prop) {
11
+ // Handle array-like numeric access and length
12
+ if (prop === 'length') return elements.length;
13
+
14
+ // Handle Symbol.iterator BEFORE number check
15
+ if (prop === Symbol.iterator) {
16
+ return elements[Symbol.iterator].bind(elements);
17
+ }
18
+
19
+ // Handle other Symbols (toStringTag, etc.) - return undefined to avoid Symbol-to-number conversion
20
+ if (typeof prop === 'symbol') {
21
+ return undefined;
22
+ }
23
+
24
+ // Handle numeric indices
25
+ if (!isNaN(prop)) return elements[prop];
26
+
27
+ // Handle array methods
28
+ if (prop in Array.prototype) {
29
+ const arrayMethod = Array.prototype[prop];
30
+ return (...args) => {
31
+ const result = arrayMethod.apply(elements, args);
32
+ // For methods that return arrays, only wrap if they contain DOM nodes
33
+ // Otherwise return plain array (jQuery-like behavior for primitives)
34
+ if (Array.isArray(result)) {
35
+ return isElementArray(result)
36
+ ? createElementProxy(result, plugins, methods)
37
+ : result; // Return plain array for primitives
38
+ }
39
+ // For single element returns (like find), return raw value
40
+ if (result instanceof Element) {
41
+ return result;
42
+ }
43
+ // For boolean/primitive returns (some, every, etc), return raw value
44
+ return result;
45
+ };
46
+ }
47
+
48
+ // Handle event listeners and support event delegation
49
+ if (prop.startsWith('on')) {
50
+ return (selectorOrHandlerOrObject, optionalHandler) => {
51
+ const eventName = prop.slice(2);
52
+
53
+ // Handle object of bindings
54
+ if (selectorOrHandlerOrObject && typeof selectorOrHandlerOrObject === 'object') {
55
+ Object.entries(selectorOrHandlerOrObject).forEach(([selector, handler]) => {
56
+ elements.forEach(el => {
57
+ el.addEventListener(eventName, (e) => {
58
+ const closest = e.target.closest(selector);
59
+ if (closest && el.contains(closest)) {
60
+ handler.call(closest, e);
61
+ }
62
+ });
63
+ });
64
+ });
65
+ return createElementProxy(elements, plugins, methods);
66
+ }
67
+
68
+ // Handle single binding (existing delegation code)
69
+ if (optionalHandler) {
70
+ const selector = selectorOrHandlerOrObject;
71
+ const handler = optionalHandler;
72
+
73
+ elements.forEach(el => {
74
+ el.addEventListener(eventName, (e) => {
75
+ const closest = e.target.closest(selector);
76
+ if (closest && el.contains(closest)) {
77
+ handler.call(closest, e);
78
+ }
79
+ });
80
+ });
81
+ } else {
82
+ const handler = selectorOrHandlerOrObject;
83
+ elements.forEach(el => el.addEventListener(eventName, handler));
84
+ }
85
+
86
+ return createElementProxy(elements, plugins, methods);
87
+ };
88
+ }
89
+
90
+ // Check plugins first
91
+ if (plugins[prop]) {
92
+ const plugin = plugins[prop];
93
+ if (typeof plugin === 'function') {
94
+ return createElementProxy(plugin(elements), plugins, methods);
95
+ }
96
+ if (typeof plugin === 'object') {
97
+ if (plugin.value) {
98
+ return createElementProxy(plugin.value(elements), plugins, methods);
99
+ }
100
+ if (plugin.properties) {
101
+ return createNestedPluginProxy(elements, plugin.properties, plugins, methods);
102
+ }
103
+ }
104
+ return plugin;
105
+ }
106
+
107
+ if (methods[prop]) {
108
+ const method = methods[prop];
109
+ return (...args) => {
110
+ const result = method.apply(createElementProxy(elements, plugins, methods), args);
111
+ // Only wrap arrays that contain DOM nodes, return plain arrays for primitives
112
+ return (result instanceof Array && isElementArray(result))
113
+ ? createElementProxy(result, plugins, methods)
114
+ : result;
115
+ };
116
+ }
117
+
118
+ // Check if we have any elements to work with
119
+ // Example: All('.missing') returns [] so firstEl is null
120
+ const firstEl = elements[0];
121
+ if (!firstEl) {
122
+ // No elements found - need to handle property access gracefully
123
+ // so chains like All.missing.classList.add('hidden') don't break
124
+
125
+ // These properties return objects with their own methods (classList.add, style.color, etc)
126
+ // We know these are safe to proxy even when empty
127
+ if (['style', 'classList', 'dataset'].includes(prop)) {
128
+ return createIntermediateProxy([], prop, plugins, methods);
129
+ }
130
+
131
+ // For unknown properties, we need to check what they are on the DOM
132
+ // BUT we can't do Element.prototype.classList - that throws "Illegal invocation"
133
+ // Instead we use getOwnPropertyDescriptor which safely tells us ABOUT the property
134
+ const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, prop) ||
135
+ Object.getOwnPropertyDescriptor(Node.prototype, prop);
136
+
137
+ if (descriptor?.get) {
138
+ // Properties like 'children', 'parentElement' have getters
139
+ // Treat them like classList - return a proxy that won't break chains
140
+ return createIntermediateProxy([], prop, plugins, methods);
141
+ }
142
+
143
+ if (typeof descriptor?.value === 'function') {
144
+ // Regular methods like 'appendChild', 'remove'
145
+ // Return a no-op function: All.missing.remove() does nothing, returns proxy
146
+ return (...args) => createElementProxy([], plugins, methods);
147
+ }
148
+
149
+ // Property doesn't exist on DOM elements (like All.missing.foobar)
150
+ return undefined;
151
+ }
152
+
153
+ const value = firstEl[prop];
154
+
155
+ // The library is designed to handle chaining correctly
156
+ // - When a method returns an Element (like cloneNode), it wraps it in a proxy
157
+ // - When a method returns undefined (like removeAttribute), it chains on the original elements
158
+ // - For other return values, like strings, it returns the results in an array
159
+ // We also handle passing in an all-wrapped proxy object as an argument and loop over all elements in it
160
+ // In the method handler's get function, replace the function call handling with:
161
+ if (typeof value === 'function') {
162
+ return (...args) => {
163
+ // Unwrap any proxy arguments
164
+ const unwrappedArgs = args.map(arg => {
165
+ if (arg && arg.constructor === Proxy) {
166
+ return Array.from(arg);
167
+ }
168
+ return arg;
169
+ });
170
+
171
+ const results = elements.map(el => {
172
+ // Check if any of the unwrapped arguments are arrays (from proxies)
173
+ const hasProxyArgs = unwrappedArgs.some(Array.isArray);
174
+
175
+ if (hasProxyArgs) {
176
+ // Handle proxy arguments case
177
+ const elementResults = unwrappedArgs.map(arg => {
178
+ if (Array.isArray(arg)) {
179
+ return arg.map(proxyEl => el[prop](proxyEl));
180
+ }
181
+ return [el[prop](...args)];
182
+ }).flat();
183
+ return elementResults[elementResults.length - 1];
184
+ } else {
185
+ // Simple case - just call the method once with original arguments
186
+ return el[prop](...args);
187
+ }
188
+ });
189
+
190
+ if (results[0] instanceof Element) {
191
+ return createElementProxy(results.filter(Boolean), plugins, methods);
192
+ }
193
+ if (results[0] === undefined) {
194
+ return createElementProxy(elements, plugins, methods);
195
+ }
196
+ return results;
197
+ };
198
+ }
199
+
200
+ // Handle intermediate objects (style, classList, dataset)
201
+ if (['style', 'classList', 'dataset'].includes(prop)) {
202
+ return createIntermediateProxy(elements, prop, plugins, methods);
203
+ }
204
+
205
+ // Return array of values for leaf properties
206
+ return elements.map(el => el[prop]);
207
+ },
208
+
209
+ set(target, prop, value) {
210
+ elements.forEach(el => {
211
+ el[prop] = value;
212
+ });
213
+ return true;
214
+ }
215
+ });
216
+
217
+ const createNestedPluginProxy = (elements, properties, plugins, methods) => {
218
+ return new Proxy({}, {
219
+ get(target, prop) {
220
+ if (properties[prop]) {
221
+ const plugin = properties[prop];
222
+ if (typeof plugin === 'function') {
223
+ return createElementProxy(plugin(elements), plugins, methods);
224
+ }
225
+ return plugin;
226
+ }
227
+ return undefined;
228
+ }
229
+ });
230
+ };
231
+
232
+ const createIntermediateProxy = (elements, propName, plugins, methods) => {
233
+ return new Proxy({}, {
234
+ get(target, prop) {
235
+ // Handle function properties (like classList.add)
236
+ const firstEl = elements[0];
237
+ if (!firstEl) {
238
+ // Return no-op function for any property access on empty collection
239
+ // This ensures chaining continues to work even with no elements
240
+ return (...args) => createElementProxy(elements, plugins, methods);
241
+ }
242
+
243
+ const value = firstEl[propName][prop];
244
+ if (typeof value === 'function') {
245
+ return (...args) => {
246
+ elements.forEach(el => el[propName][prop](...args));
247
+ return createElementProxy(elements, plugins, methods);
248
+ };
249
+ }
250
+
251
+ // Return array of values for leaf properties
252
+ return elements.map(el => el[propName][prop]);
253
+ },
254
+
255
+ set(target, prop, value) {
256
+ elements.forEach(el => {
257
+ el[propName][prop] = value;
258
+ });
259
+ return true;
260
+ }
261
+ });
262
+ };
263
+
264
+ const sharedState = {
265
+ plugins: {},
266
+ methods: {}
267
+ };
268
+
269
+ const createElementProxy = (elements, plugins = sharedState.plugins, methods = sharedState.methods) => {
270
+ return new Proxy(elements, createMethodHandler(elements, plugins, methods));
271
+ };
272
+
273
+ const toElementArray = (input) => {
274
+ if (input instanceof Element || input instanceof Document) {
275
+ return [input];
276
+ }
277
+ if (Array.isArray(input)) {
278
+ if (input.every(el => el instanceof Element || el instanceof Document)) {
279
+ return input;
280
+ }
281
+ throw new TypeError('All array elements must be DOM Elements or Document');
282
+ }
283
+ if (typeof input === 'string') {
284
+ return Array.from(document.querySelectorAll(input));
285
+ }
286
+ throw new TypeError('Input must be a selector string, Element, Document, or Array of Elements');
287
+ };
288
+
289
+ // Default plugins
290
+ const defaultPlugins = {
291
+ methods: {
292
+ eq(index) {
293
+ if (typeof index !== 'number') {
294
+ throw new TypeError('eq() requires a number as an argument');
295
+ }
296
+
297
+ // Handle negative indices (counting from the end)
298
+ const normalizedIndex = index < 0 ? this.length + index : index;
299
+
300
+ // Return array with single element at index, or empty array if index is invalid
301
+ // ❗️ Allows chaining to continue
302
+ return this[normalizedIndex] ? [this[normalizedIndex]] : [];
303
+ },
304
+
305
+ at(index) {
306
+ if (typeof index !== 'number') {
307
+ throw new TypeError('at() requires a number as an argument');
308
+ }
309
+ const normalizedIndex = index < 0 ? this.length + index : index;
310
+ return this[normalizedIndex];
311
+ },
312
+
313
+ prop(properties) {
314
+ if (typeof properties !== 'object' || properties === null) {
315
+ throw new TypeError('prop() requires an object of properties');
316
+ }
317
+
318
+ Object.entries(properties).forEach(([key, value]) => {
319
+ this.forEach(el => {
320
+ el[key] = value;
321
+ });
322
+ });
323
+
324
+ return this;
325
+ },
326
+
327
+ css(styles) {
328
+ if (typeof styles !== 'object' || styles === null) {
329
+ throw new TypeError('css() requires an object of styles');
330
+ }
331
+
332
+ Object.entries(styles).forEach(([property, value]) => {
333
+ this.forEach(el => {
334
+ el.style[property] = value;
335
+ });
336
+ });
337
+
338
+ return this;
339
+ }
340
+ }
341
+ };
342
+
343
+ const All = new Proxy(function (selectorOrElements, contextSelector) {
344
+ // If there's a context selector, search within that context
345
+ if (arguments.length === 2) {
346
+ if (typeof contextSelector !== 'string') {
347
+ throw new TypeError('Context selector must be a string');
348
+ }
349
+
350
+ // Get the context element(s)
351
+ const contextElements = Array.from(document.querySelectorAll(contextSelector));
352
+
353
+ // If first arg is a string selector, search within each context
354
+ if (typeof selectorOrElements === 'string') {
355
+ const elements = contextElements.flatMap(context => {
356
+ // Include context itself if it matches the selector
357
+ const matches = context.matches(selectorOrElements) ? [context] : [];
358
+ // Plus all descendants that match
359
+ const descendants = Array.from(context.querySelectorAll(selectorOrElements));
360
+ return [...matches, ...descendants];
361
+ });
362
+ return createElementProxy(elements);
363
+ }
364
+
365
+ // If first arg is elements, filter to those within context
366
+ const searchElements = toElementArray(selectorOrElements);
367
+ const elements = searchElements.filter(el =>
368
+ contextElements.some(context => context.contains(el))
369
+ );
370
+ return createElementProxy(elements);
371
+ }
372
+
373
+ // Single argument - normalize and return
374
+ const elements = toElementArray(selectorOrElements);
375
+ return createElementProxy(elements);
376
+ }, {
377
+ get(target, prop) {
378
+ if (prop === 'use') {
379
+ return function (plugin) {
380
+ if (!plugin || typeof plugin !== 'object') {
381
+ throw new TypeError('Plugin must be an object with "properties" or "methods"');
382
+ }
383
+ if (plugin.properties) {
384
+ Object.assign(sharedState.plugins, plugin.properties);
385
+ }
386
+ if (plugin.methods) {
387
+ Object.assign(sharedState.methods, plugin.methods);
388
+ }
389
+ return this;
390
+ };
391
+ }
392
+
393
+ if (prop === Symbol.iterator) return undefined;
394
+ if (prop in target) return target[prop];
395
+
396
+ const elements = Array.from(document.querySelectorAll(`[${prop}], .${prop}`));
397
+ return createElementProxy(elements);
398
+ }
399
+ });
400
+
401
+ // Install default plugins
402
+ All.use(defaultPlugins);
403
+
404
+ // Export to window for global access
405
+ export function exportToWindow() {
406
+ if (!window.hyperclay) {
407
+ window.hyperclay = {};
408
+ }
409
+ window.hyperclay.All = All;
410
+ }
411
+
412
+ export default All;
@@ -0,0 +1,60 @@
1
+ // easily convert a form into a JS object
2
+ export default function getDataFromForm(container) {
3
+ const formData = {};
4
+
5
+ // Helper function to process a single element
6
+ const processElement = (elem) => {
7
+ const name = elem.getAttribute('name');
8
+ const value = elem.getAttribute('value') || elem.value;
9
+
10
+ // Skip elements without a name or with a disabled attribute
11
+ if (!name || elem.disabled) return;
12
+
13
+ // Handle different element types
14
+ switch (elem.type) {
15
+ case 'checkbox':
16
+ if (!formData[name]) {
17
+ formData[name] = [];
18
+ }
19
+ if (elem.checked) {
20
+ formData[name].push(value);
21
+ }
22
+ break;
23
+ case 'radio':
24
+ if (elem.checked) {
25
+ formData[name] = value;
26
+ }
27
+ break;
28
+ case 'select-multiple':
29
+ formData[name] = Array.from(elem.selectedOptions, option => option.value);
30
+ break;
31
+ case 'button':
32
+ case 'submit':
33
+ case 'reset':
34
+ // Only include buttons if they have both name and value attributes
35
+ if (name && value) {
36
+ formData[name] = value;
37
+ }
38
+ break;
39
+ default:
40
+ formData[name] = value;
41
+ }
42
+ };
43
+
44
+ // If container is a form, use elements property
45
+ if (container instanceof HTMLFormElement) {
46
+ Array.from(container.elements).forEach(processElement);
47
+ }
48
+ // Otherwise, process container itself and then query for elements with name attribute
49
+ else {
50
+ // Process container element if it has a name attribute
51
+ if (container.hasAttribute('name')) {
52
+ processElement(container);
53
+ }
54
+ // Process all child elements with name attributes
55
+ const elements = container.querySelectorAll('[name]');
56
+ elements.forEach(processElement);
57
+ }
58
+
59
+ return formData;
60
+ }
@@ -0,0 +1,28 @@
1
+ export default function insertStyleTag(href) {
2
+ // First check if there's already a link with this exact href
3
+ if (document.querySelector(`link[href="${href}"]`)) {
4
+ return;
5
+ }
6
+
7
+ // Extract a more reliable identifier from the URL
8
+ let identifier;
9
+ try {
10
+ const url = new URL(href, window.location.href);
11
+ // Get the filename from the path
12
+ identifier = url.pathname.split('/').pop();
13
+ } catch (e) {
14
+ // Fallback to using the href itself if URL parsing fails
15
+ identifier = href;
16
+ }
17
+
18
+ // Look for any link that contains this identifier
19
+ if (identifier && document.querySelector(`link[href*="${identifier}"]`)) {
20
+ return;
21
+ }
22
+
23
+ // If no duplicate found, add the stylesheet
24
+ const link = document.createElement('link');
25
+ link.rel = 'stylesheet';
26
+ link.href = href;
27
+ document.head.appendChild(link);
28
+ }
@@ -0,0 +1,7 @@
1
+ export default function onDomReady (callback) {
2
+ if (document.readyState === 'loading') {
3
+ document.addEventListener('DOMContentLoaded', callback);
4
+ } else {
5
+ callback();
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ export default function onLoad (callback) {
2
+ if (document.readyState === "complete") {
3
+ callback();
4
+ } else {
5
+ window.addEventListener("load", callback);
6
+ }
7
+ }