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.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/README.template.md +276 -0
- package/communication/behaviorCollector.js +230 -0
- package/communication/sendMessage.js +48 -0
- package/communication/uploadFile.js +348 -0
- package/core/adminContenteditable.js +36 -0
- package/core/adminInputs.js +58 -0
- package/core/adminOnClick.js +31 -0
- package/core/adminResources.js +33 -0
- package/core/adminSystem.js +15 -0
- package/core/editmode.js +8 -0
- package/core/editmodeSystem.js +18 -0
- package/core/enablePersistentFormInputValues.js +62 -0
- package/core/isAdminOfCurrentResource.js +13 -0
- package/core/optionVisibilityRuleGenerator.js +160 -0
- package/core/savePage.js +196 -0
- package/core/savePageCore.js +236 -0
- package/core/setPageTypeOnDocumentElement.js +23 -0
- package/custom-attributes/ajaxElements.js +94 -0
- package/custom-attributes/autosize.js +17 -0
- package/custom-attributes/domHelpers.js +175 -0
- package/custom-attributes/events.js +15 -0
- package/custom-attributes/inputHelpers.js +11 -0
- package/custom-attributes/onclickaway.js +27 -0
- package/custom-attributes/onclone.js +35 -0
- package/custom-attributes/onpagemutation.js +20 -0
- package/custom-attributes/onrender.js +30 -0
- package/custom-attributes/preventEnter.js +13 -0
- package/custom-attributes/sortable.js +76 -0
- package/dom-utilities/All.js +412 -0
- package/dom-utilities/getDataFromForm.js +60 -0
- package/dom-utilities/insertStyleTag.js +28 -0
- package/dom-utilities/onDomReady.js +7 -0
- package/dom-utilities/onLoad.js +7 -0
- package/hyperclay.js +465 -0
- package/module-dependency-graph.json +612 -0
- package/package.json +95 -0
- package/string-utilities/copy-to-clipboard.js +35 -0
- package/string-utilities/emmet-html.js +54 -0
- package/string-utilities/query.js +1 -0
- package/string-utilities/slugify.js +21 -0
- package/ui/info.js +39 -0
- package/ui/prompts.js +179 -0
- package/ui/theModal.js +677 -0
- package/ui/toast.js +273 -0
- package/utilities/cookie.js +45 -0
- package/utilities/debounce.js +12 -0
- package/utilities/mutation.js +403 -0
- package/utilities/nearest.js +97 -0
- package/utilities/pipe.js +1 -0
- package/utilities/throttle.js +21 -0
- package/vendor/Sortable.js +3351 -0
- package/vendor/idiomorph.min.js +8 -0
- package/vendor/tailwind-base.css +1471 -0
- package/vendor/tailwind-play.js +169 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Lightweight wrapper around MutationObserver that provides methods to watch DOM changes.
|
|
4
|
+
* Uses a single observer instance internally to improve performance.
|
|
5
|
+
*
|
|
6
|
+
* // Watch for any DOM changes (additions, removals, attribute changes)
|
|
7
|
+
* Mutation.onAnyChange({ debounce: 200 }, changes => {
|
|
8
|
+
* changes.forEach(change => {
|
|
9
|
+
* if (change.type === 'add') {
|
|
10
|
+
* console.log('Added:', change.element);
|
|
11
|
+
* console.log('To parent:', change.parent);
|
|
12
|
+
* console.log('Between:', change.previousSibling, change.nextSibling);
|
|
13
|
+
* }
|
|
14
|
+
* if (change.type === 'remove') {
|
|
15
|
+
* console.log('Removed:', change.element);
|
|
16
|
+
* console.log('From parent:', change.parent);
|
|
17
|
+
* }
|
|
18
|
+
* if (change.type === 'attribute') {
|
|
19
|
+
* console.log('Changed:', change.attribute);
|
|
20
|
+
* console.log('From:', change.oldValue, 'to:', change.newValue);
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Watch for element additions or removals
|
|
26
|
+
* Mutation.onAddOrRemove({ debounce: 200 }, changes => {
|
|
27
|
+
* changes.forEach(change => {
|
|
28
|
+
* const action = change.type === 'add' ? 'added to' : 'removed from';
|
|
29
|
+
* console.log(`${change.element.tagName} ${action} ${change.parent.tagName}`);
|
|
30
|
+
* });
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // Watch for element additions
|
|
34
|
+
* Mutation.onAddElement({ debounce: 200 }, changes => {
|
|
35
|
+
* changes.forEach(({ element, parent }) => {
|
|
36
|
+
* console.log(`${element.tagName} added to ${parent.tagName}`);
|
|
37
|
+
* });
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Watch for element removals with location info
|
|
41
|
+
* Mutation.onRemoveElement({ debounce: 200 }, changes => {
|
|
42
|
+
* changes.forEach(({ element, parent, previousSibling, nextSibling }) => {
|
|
43
|
+
* console.log(`${element.tagName} removed from ${parent.tagName}`);
|
|
44
|
+
* console.log('Was between:', previousSibling?.tagName, nextSibling?.tagName);
|
|
45
|
+
* });
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Watch for attribute changes
|
|
49
|
+
* Mutation.onAttribute({ debounce: 200 }, changes => {
|
|
50
|
+
* // Debounce collects multiple changes into an array
|
|
51
|
+
* changes.forEach(({ element, attribute, oldValue, newValue: newValue }) => {
|
|
52
|
+
* console.log(`${element.tagName} ${attribute} changed from ${oldValue} to ${newValue}`);
|
|
53
|
+
* });
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const dummyElem = document.createElement("div");
|
|
59
|
+
|
|
60
|
+
const Mutation = {
|
|
61
|
+
_callbacks: {
|
|
62
|
+
anyChange: [],
|
|
63
|
+
addOrRemove: [],
|
|
64
|
+
addElement: [],
|
|
65
|
+
removeElement: [],
|
|
66
|
+
attribute: []
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
_observing: false,
|
|
70
|
+
debug: false,
|
|
71
|
+
|
|
72
|
+
_log(message, data = null, type = 'log') {
|
|
73
|
+
if (!this.debug) return;
|
|
74
|
+
|
|
75
|
+
const timestamp = new Date().toISOString();
|
|
76
|
+
const prefix = `[Mutation ${timestamp}]`;
|
|
77
|
+
|
|
78
|
+
if (data) {
|
|
79
|
+
console[type](prefix, message, data);
|
|
80
|
+
} else {
|
|
81
|
+
console[type](prefix, message);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
_notify(type, changes) {
|
|
86
|
+
this._log(`Notifying ${this._callbacks[type].length} callbacks of type "${type}"`, { changes });
|
|
87
|
+
|
|
88
|
+
for (const callback of this._callbacks[type]) {
|
|
89
|
+
const { fn, debounce = 0, selectorFilter, omitChangeDetails } = callback;
|
|
90
|
+
|
|
91
|
+
// Apply filtering if there's a selector filter
|
|
92
|
+
let filteredChanges = changes;
|
|
93
|
+
if (selectorFilter) {
|
|
94
|
+
this._log('Applying selector filter', { selectorFilter });
|
|
95
|
+
filteredChanges = changes.filter(change => {
|
|
96
|
+
if (typeof selectorFilter === 'string') {
|
|
97
|
+
const matches = change.element.matches?.(selectorFilter) || false;
|
|
98
|
+
this._log(`Selector "${selectorFilter}" match:`, { element: change.element, matches });
|
|
99
|
+
return matches;
|
|
100
|
+
}
|
|
101
|
+
if (typeof selectorFilter === 'function') {
|
|
102
|
+
const matches = selectorFilter(change.element);
|
|
103
|
+
this._log('Custom filter match:', { element: change.element, matches });
|
|
104
|
+
return matches;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
});
|
|
108
|
+
this._log('Changes after filtering:', { filteredChanges });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Skip if we have a selector filter and no matching changes
|
|
112
|
+
if (selectorFilter && !filteredChanges.length) {
|
|
113
|
+
this._log('No changes passed the filter, skipping callback');
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle debouncing and callback execution
|
|
118
|
+
if (debounce === 0) {
|
|
119
|
+
// No debounce, execute immediately
|
|
120
|
+
try {
|
|
121
|
+
if (omitChangeDetails) {
|
|
122
|
+
fn();
|
|
123
|
+
} else {
|
|
124
|
+
fn(filteredChanges);
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
this._log('Error in callback execution:', e, 'error');
|
|
128
|
+
console.error('Error in Mutation callback:', e);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Clear any existing timeout
|
|
132
|
+
if (callback.timeout) {
|
|
133
|
+
clearTimeout(callback.timeout);
|
|
134
|
+
callback.timeout = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (omitChangeDetails) {
|
|
138
|
+
// For omitChangeDetails, just reset the timer
|
|
139
|
+
callback.timeout = setTimeout(() => {
|
|
140
|
+
callback.timeout = null;
|
|
141
|
+
try {
|
|
142
|
+
this._log('Executing debounced callback (no details)');
|
|
143
|
+
fn();
|
|
144
|
+
} catch (e) {
|
|
145
|
+
this._log('Error in callback execution:', e, 'error');
|
|
146
|
+
console.error('Error in Mutation callback:', e);
|
|
147
|
+
}
|
|
148
|
+
}, debounce);
|
|
149
|
+
} else {
|
|
150
|
+
// For callbacks with change details, accumulate changes
|
|
151
|
+
if (!callback.pendingChanges) {
|
|
152
|
+
callback.pendingChanges = [];
|
|
153
|
+
}
|
|
154
|
+
callback.pendingChanges.push(...filteredChanges);
|
|
155
|
+
|
|
156
|
+
// Reset the timer
|
|
157
|
+
callback.timeout = setTimeout(() => {
|
|
158
|
+
const changes = callback.pendingChanges;
|
|
159
|
+
callback.pendingChanges = null; // Reset to null, not empty array
|
|
160
|
+
callback.timeout = null; // Clear the timeout reference
|
|
161
|
+
try {
|
|
162
|
+
this._log('Executing debounced callback with changes:', { changes });
|
|
163
|
+
if (changes && changes.length > 0) {
|
|
164
|
+
fn(changes);
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
this._log('Error in callback execution:', e, 'error');
|
|
168
|
+
console.error('Error in Mutation callback:', e);
|
|
169
|
+
}
|
|
170
|
+
}, debounce);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
_shouldIgnore(element) {
|
|
177
|
+
while (element && element.nodeType === 1) {
|
|
178
|
+
if (element.hasAttribute?.('mutations-ignore') || element.hasAttribute?.('save-ignore')) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
element = element.parentElement;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
_handleMutations(mutations) {
|
|
187
|
+
this._log(`Processing ${mutations.length} mutations`, { mutations });
|
|
188
|
+
|
|
189
|
+
const changes = [];
|
|
190
|
+
const changesByType = {
|
|
191
|
+
add: [],
|
|
192
|
+
remove: [],
|
|
193
|
+
attribute: [],
|
|
194
|
+
characterData: []
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
for (const mutation of mutations) {
|
|
198
|
+
// Check if the target or any parent has mutations-ignore attribute
|
|
199
|
+
if (this._shouldIgnore(mutation.target)) {
|
|
200
|
+
this._log('Ignoring mutation due to mutations-ignore attribute', { mutation });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (mutation.type === 'characterData') {
|
|
205
|
+
this._log('Processing characterData mutation', {
|
|
206
|
+
element: mutation.target.parentElement,
|
|
207
|
+
oldValue: mutation.oldValue,
|
|
208
|
+
newValue: mutation.target.textContent
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const change = {
|
|
212
|
+
type: 'characterData',
|
|
213
|
+
element: mutation.target.parentElement ?? dummyElem, // hacky, but ensures we always pass an element in the callback
|
|
214
|
+
oldValue: mutation.oldValue,
|
|
215
|
+
newValue: mutation.target.textContent
|
|
216
|
+
};
|
|
217
|
+
changes.push(change);
|
|
218
|
+
changesByType.characterData.push(change);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (mutation.type === 'childList') {
|
|
222
|
+
this._log('Processing childList mutation', {
|
|
223
|
+
addedNodes: mutation.addedNodes,
|
|
224
|
+
removedNodes: mutation.removedNodes
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
for (const node of mutation.addedNodes) {
|
|
228
|
+
if (node.nodeType === 1 && !this._shouldIgnore(node)) {
|
|
229
|
+
const addedNodes = [node, ...node.querySelectorAll('*')];
|
|
230
|
+
this._log(`Processing ${addedNodes.length} added nodes`, { addedNodes });
|
|
231
|
+
|
|
232
|
+
for (const element of addedNodes) {
|
|
233
|
+
const change = {
|
|
234
|
+
type: 'add',
|
|
235
|
+
element,
|
|
236
|
+
parent: mutation.target,
|
|
237
|
+
previousSibling: mutation.previousSibling,
|
|
238
|
+
nextSibling: mutation.nextSibling
|
|
239
|
+
};
|
|
240
|
+
changes.push(change);
|
|
241
|
+
changesByType.add.push(change);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const node of mutation.removedNodes) {
|
|
247
|
+
if (node.nodeType === 1 && !node.hasAttribute?.('save-ignore') && !node.hasAttribute?.('mutations-ignore')) {
|
|
248
|
+
const removedNodes = [node, ...node.querySelectorAll('*')];
|
|
249
|
+
this._log(`Processing ${removedNodes.length} removed nodes`, { removedNodes });
|
|
250
|
+
|
|
251
|
+
for (const element of removedNodes) {
|
|
252
|
+
const change = {
|
|
253
|
+
type: 'remove',
|
|
254
|
+
element,
|
|
255
|
+
parent: mutation.target,
|
|
256
|
+
previousSibling: mutation.previousSibling,
|
|
257
|
+
nextSibling: mutation.nextSibling
|
|
258
|
+
};
|
|
259
|
+
changes.push(change);
|
|
260
|
+
changesByType.remove.push(change);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (mutation.type === 'attributes') {
|
|
267
|
+
this._log('Processing attribute mutation', {
|
|
268
|
+
element: mutation.target,
|
|
269
|
+
attribute: mutation.attributeName,
|
|
270
|
+
oldValue: mutation.oldValue,
|
|
271
|
+
newValue: mutation.target.getAttribute(mutation.attributeName)
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const change = {
|
|
275
|
+
type: 'attribute',
|
|
276
|
+
element: mutation.target,
|
|
277
|
+
attribute: mutation.attributeName,
|
|
278
|
+
oldValue: mutation.oldValue,
|
|
279
|
+
newValue: mutation.target.getAttribute(mutation.attributeName)
|
|
280
|
+
};
|
|
281
|
+
changes.push(change);
|
|
282
|
+
changesByType.attribute.push(change);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (changes.length) {
|
|
287
|
+
this._log('Processing collected changes', {
|
|
288
|
+
total: changes.length,
|
|
289
|
+
byType: {
|
|
290
|
+
add: changesByType.add.length,
|
|
291
|
+
remove: changesByType.remove.length,
|
|
292
|
+
attribute: changesByType.attribute.length
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
this._notify('anyChange', changes);
|
|
297
|
+
|
|
298
|
+
const addOrRemove = [...changesByType.add, ...changesByType.remove];
|
|
299
|
+
if (addOrRemove.length) {
|
|
300
|
+
this._notify('addOrRemove', addOrRemove);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (changesByType.add.length) {
|
|
304
|
+
this._notify('addElement', changesByType.add);
|
|
305
|
+
}
|
|
306
|
+
if (changesByType.remove.length) {
|
|
307
|
+
this._notify('removeElement', changesByType.remove);
|
|
308
|
+
}
|
|
309
|
+
if (changesByType.attribute.length) {
|
|
310
|
+
this._notify('attribute', changesByType.attribute);
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
this._log('No changes to process after filtering');
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
_observer: null,
|
|
318
|
+
|
|
319
|
+
_initializeObserver() {
|
|
320
|
+
if (!this._observer) {
|
|
321
|
+
this._log('Initializing MutationObserver');
|
|
322
|
+
this._observer = new MutationObserver(this._handleMutations.bind(this));
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
_addCallback(type, options = {}, callback) {
|
|
327
|
+
this._log('Adding callback', { type, options });
|
|
328
|
+
|
|
329
|
+
if (options.debug) {
|
|
330
|
+
this.debug = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const cb = {
|
|
334
|
+
fn: callback,
|
|
335
|
+
debounce: options.debounce || 0,
|
|
336
|
+
selectorFilter: options.selectorFilter,
|
|
337
|
+
omitChangeDetails: options.omitChangeDetails,
|
|
338
|
+
timeout: null,
|
|
339
|
+
pendingChanges: null
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
this._callbacks[type].push(cb);
|
|
343
|
+
this._log(`Added callback to ${type}. Total callbacks:`, {
|
|
344
|
+
[type]: this._callbacks[type].length
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
this._startObserving();
|
|
348
|
+
|
|
349
|
+
return () => {
|
|
350
|
+
this._log('Removing callback', { type });
|
|
351
|
+
const index = this._callbacks[type].indexOf(cb);
|
|
352
|
+
if (index !== -1) {
|
|
353
|
+
clearTimeout(cb.timeout);
|
|
354
|
+
cb.pendingChanges = null;
|
|
355
|
+
this._callbacks[type].splice(index, 1);
|
|
356
|
+
this._log(`Removed callback from ${type}. Remaining callbacks:`, {
|
|
357
|
+
[type]: this._callbacks[type].length
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
_startObserving() {
|
|
364
|
+
if (this._observing) {
|
|
365
|
+
this._log('Already observing, skipping initialization');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this._log('Starting observation');
|
|
370
|
+
this._initializeObserver();
|
|
371
|
+
this._observer.observe(document.body, {
|
|
372
|
+
childList: true,
|
|
373
|
+
attributes: true,
|
|
374
|
+
subtree: true,
|
|
375
|
+
characterData: true,
|
|
376
|
+
attributeOldValue: true
|
|
377
|
+
});
|
|
378
|
+
this._observing = true;
|
|
379
|
+
this._log('Observation started');
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
onAnyChange(options = {}, callback) {
|
|
383
|
+
return this._addCallback('anyChange', options, callback);
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
onAddOrRemove(options = {}, callback) {
|
|
387
|
+
return this._addCallback('addOrRemove', options, callback);
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
onAddElement(options = {}, callback) {
|
|
391
|
+
return this._addCallback('addElement', options, callback);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
onRemoveElement(options = {}, callback) {
|
|
395
|
+
return this._addCallback('removeElement', options, callback);
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
onAttribute(options = {}, callback) {
|
|
399
|
+
return this._addCallback('attribute', options, callback);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
export default Mutation;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Searches for elements matching a CSS selector by exploring the DOM tree outward from a starting point,
|
|
3
|
+
* checking nearby elements first before moving to more distant parts of the document.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `element.closest()` which only searches ancestors, this explores the DOM tree in a
|
|
6
|
+
* unique pattern designed to find nearby elements in the visual layout:
|
|
7
|
+
*
|
|
8
|
+
* TRAVERSAL ORDER (for each level):
|
|
9
|
+
* 1. Current element
|
|
10
|
+
* 2. All children of current element (deeply)
|
|
11
|
+
* 3. All previous siblings (right-to-left), exploring each fully with descendants
|
|
12
|
+
* 4. All next siblings (left-to-right), exploring each fully with descendants
|
|
13
|
+
* 5. Move to parent and repeat from step 1
|
|
14
|
+
*
|
|
15
|
+
* KEY FEATURES:
|
|
16
|
+
* - Global visited cache prevents revisiting nodes (big performance boost)
|
|
17
|
+
* - Searches "outward" from start position, checking nearby elements first
|
|
18
|
+
* - Explores each sibling's entire subtree before moving to next sibling
|
|
19
|
+
* - Continues up the ancestor chain until document.body
|
|
20
|
+
*
|
|
21
|
+
* USE CASES:
|
|
22
|
+
* - Finding related UI elements that might be siblings or cousins
|
|
23
|
+
* - Locating the "next" instance of something in reading order
|
|
24
|
+
* - Finding nearby form fields, buttons, or other interactive elements
|
|
25
|
+
*
|
|
26
|
+
* @param {Element} startElem - Starting element for the search
|
|
27
|
+
* @param {string} selector - CSS selector to match
|
|
28
|
+
* @param {Function} elementFoundReturnValue - Transform function for the found element
|
|
29
|
+
* @returns {*} Transformed element if found, null otherwise
|
|
30
|
+
*/
|
|
31
|
+
export default function nearest (startElem, selector, elementFoundReturnValue = x => x) {
|
|
32
|
+
const visited = new Set();
|
|
33
|
+
|
|
34
|
+
// Check node and its descendants using BFS
|
|
35
|
+
function checkDeep(root) {
|
|
36
|
+
if (!root || visited.has(root)) return null;
|
|
37
|
+
|
|
38
|
+
const queue = [root];
|
|
39
|
+
const localVisited = new Set(); // Prevent cycles within this BFS
|
|
40
|
+
|
|
41
|
+
while (queue.length > 0) {
|
|
42
|
+
const node = queue.shift();
|
|
43
|
+
if (!node || localVisited.has(node) || visited.has(node)) continue;
|
|
44
|
+
|
|
45
|
+
visited.add(node);
|
|
46
|
+
localVisited.add(node);
|
|
47
|
+
|
|
48
|
+
if (node.matches(selector)) {
|
|
49
|
+
return elementFoundReturnValue(node);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
queue.push(...node.children);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check siblings in a direction
|
|
58
|
+
function checkSiblings(start, direction) {
|
|
59
|
+
let sibling = start[direction];
|
|
60
|
+
while (sibling) {
|
|
61
|
+
const result = checkDeep(sibling);
|
|
62
|
+
if (result) return result;
|
|
63
|
+
sibling = sibling[direction];
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Main traversal
|
|
69
|
+
// check current → children → siblings → move up
|
|
70
|
+
let current = startElem;
|
|
71
|
+
|
|
72
|
+
while (current) {
|
|
73
|
+
// Check current node (shallow)
|
|
74
|
+
if (!visited.has(current)) {
|
|
75
|
+
visited.add(current);
|
|
76
|
+
if (current.matches(selector)) {
|
|
77
|
+
return elementFoundReturnValue(current);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check children deeply
|
|
82
|
+
for (const child of current.children) {
|
|
83
|
+
const result = checkDeep(child);
|
|
84
|
+
if (result) return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check siblings deeply
|
|
88
|
+
let result = checkSiblings(current, 'previousElementSibling') ||
|
|
89
|
+
checkSiblings(current, 'nextElementSibling');
|
|
90
|
+
if (result) return result;
|
|
91
|
+
|
|
92
|
+
// Move up to parent
|
|
93
|
+
current = current.parentElement;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default (...fns) => x => fns.reduce((v, f) => f(v), x);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default function throttle(callback, delay, executeFirst = true) {
|
|
2
|
+
let lastCall = executeFirst ? 0 : Date.now();
|
|
3
|
+
let timeoutId = null;
|
|
4
|
+
|
|
5
|
+
return function (...args) {
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
const remaining = delay - (now - lastCall);
|
|
8
|
+
|
|
9
|
+
if (remaining <= 0) {
|
|
10
|
+
clearTimeout(timeoutId);
|
|
11
|
+
lastCall = now;
|
|
12
|
+
return callback.apply(this, args);
|
|
13
|
+
} else if (!timeoutId) {
|
|
14
|
+
timeoutId = setTimeout(() => {
|
|
15
|
+
lastCall = Date.now();
|
|
16
|
+
timeoutId = null;
|
|
17
|
+
callback.apply(this, args);
|
|
18
|
+
}, remaining);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|