@briannorman9/eli-utils 1.0.0 → 1.1.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 +177 -35
- package/index.js +1100 -11
- package/package.json +17 -13
package/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// ELI Utils - Utility functions for web experiments
|
|
2
|
-
// This
|
|
3
|
-
// Then imported in variant code using: import utils from '@eli/utils';
|
|
4
|
-
// (The server automatically resolves @eli/utils to the @briannorman9/eli-utils package)
|
|
2
|
+
// This file can be imported in variant code using: import utils from '@eli/utils';
|
|
5
3
|
|
|
6
4
|
export default {
|
|
7
5
|
/**
|
|
8
|
-
* Wait for
|
|
6
|
+
* Wait for the first element matching the selector to appear in the DOM
|
|
7
|
+
* Note: This only matches the first element. Use waitForElements() to match all elements.
|
|
9
8
|
* @param {string|Element} selector - CSS selector or element
|
|
10
|
-
* @returns {Promise<Element>} Promise that resolves with the element (waits indefinitely)
|
|
9
|
+
* @returns {Promise<Element>} Promise that resolves with the first matching element (waits indefinitely)
|
|
10
|
+
* @example
|
|
11
|
+
* utils.waitForElement('.product-card').then(element => {
|
|
12
|
+
* element.style.border = '2px solid red';
|
|
13
|
+
* });
|
|
11
14
|
*/
|
|
12
15
|
waitForElement: function(selector) {
|
|
13
16
|
return new Promise((resolve) => {
|
|
@@ -84,11 +87,13 @@ export default {
|
|
|
84
87
|
* @param {string} value - Cookie value
|
|
85
88
|
* @param {number} days - Number of days until expiration (default: 365)
|
|
86
89
|
* @param {string} path - Cookie path (default: '/')
|
|
90
|
+
* @param {string} domain - Cookie domain (optional)
|
|
87
91
|
*/
|
|
88
|
-
setCookie: function(name, value, days = 365, path = '/') {
|
|
92
|
+
setCookie: function(name, value, days = 365, path = '/', domain = '') {
|
|
89
93
|
const expires = new Date();
|
|
90
94
|
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
91
|
-
|
|
95
|
+
const domainStr = domain ? `domain=${domain};` : '';
|
|
96
|
+
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=${path};${domainStr}`;
|
|
92
97
|
},
|
|
93
98
|
|
|
94
99
|
/**
|
|
@@ -127,13 +132,39 @@ export default {
|
|
|
127
132
|
},
|
|
128
133
|
|
|
129
134
|
/**
|
|
130
|
-
*
|
|
135
|
+
* Wait for all elements matching the selector to appear in the DOM
|
|
136
|
+
* Note: This matches all elements. Use waitForElement() to match only the first element.
|
|
131
137
|
* @param {string} selector - CSS selector
|
|
132
138
|
* @param {Element} context - Context element (defaults to document)
|
|
133
|
-
* @returns {NodeList}
|
|
139
|
+
* @returns {Promise<NodeList>} Promise that resolves with all matching elements (waits indefinitely)
|
|
140
|
+
* @example
|
|
141
|
+
* utils.waitForElements('.product-card').then(elements => {
|
|
142
|
+
* elements.forEach(card => card.style.border = '2px solid red');
|
|
143
|
+
* });
|
|
134
144
|
*/
|
|
135
|
-
|
|
136
|
-
return
|
|
145
|
+
waitForElements: function(selector, context = document) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
// Check if elements already exist
|
|
148
|
+
const elements = context.querySelectorAll(selector);
|
|
149
|
+
if (elements.length > 0) {
|
|
150
|
+
resolve(elements);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Set up MutationObserver to watch for elements
|
|
155
|
+
const observer = new MutationObserver((mutations, obs) => {
|
|
156
|
+
const foundElements = context.querySelectorAll(selector);
|
|
157
|
+
if (foundElements.length > 0) {
|
|
158
|
+
obs.disconnect();
|
|
159
|
+
resolve(foundElements);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
observer.observe(context === document ? document.body : context, {
|
|
164
|
+
childList: true,
|
|
165
|
+
subtree: true
|
|
166
|
+
});
|
|
167
|
+
});
|
|
137
168
|
},
|
|
138
169
|
|
|
139
170
|
/**
|
|
@@ -257,6 +288,1064 @@ export default {
|
|
|
257
288
|
scrollIntoView: function(element, options = { behavior: 'smooth', block: 'center' }) {
|
|
258
289
|
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
259
290
|
if (el) el.scrollIntoView(options);
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Observe mutations on the first element matching the selector
|
|
295
|
+
* The callback is called every time the element is mutated (attributes, children, text, etc.)
|
|
296
|
+
* Note: This only observes the first matching element. Use observeSelectors() to observe all elements.
|
|
297
|
+
*
|
|
298
|
+
* @param {string|Element} selector - CSS selector or element to observe
|
|
299
|
+
* @param {Function} callback - Callback function that receives (element, mutationRecord, observer)
|
|
300
|
+
* @param {Object} options - Options object
|
|
301
|
+
* @param {Array<string>} options.mutations - Array of mutation types to observe. Options:
|
|
302
|
+
* - 'childList' - Watch for child nodes being added/removed (default: true)
|
|
303
|
+
* - 'attributes' - Watch for attribute changes (default: true)
|
|
304
|
+
* - 'characterData' - Watch for text content changes (default: false)
|
|
305
|
+
* - 'subtree' - Watch all descendants, not just direct children (default: true)
|
|
306
|
+
* - 'attributeOldValue' - Include old attribute value in mutation record (default: false)
|
|
307
|
+
* - 'characterDataOldValue' - Include old text value in mutation record (default: false)
|
|
308
|
+
* - 'attributeFilter' - Array of attribute names to observe (only these attributes will trigger)
|
|
309
|
+
* @param {number} options.timeout - Timeout in milliseconds (optional)
|
|
310
|
+
* @param {Function} options.onTimeout - Function to call on timeout (optional)
|
|
311
|
+
* @returns {Function} Function to stop observing
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* // Observe attribute changes on a button
|
|
315
|
+
* const stopObserving = utils.observeSelector('#myButton', (element, mutation) => {
|
|
316
|
+
* console.log('Button mutated:', mutation.type);
|
|
317
|
+
* if (mutation.type === 'attributes') {
|
|
318
|
+
* console.log('Attribute changed:', mutation.attributeName);
|
|
319
|
+
* }
|
|
320
|
+
* }, {
|
|
321
|
+
* mutations: ['attributes'],
|
|
322
|
+
* attributeFilter: ['class', 'disabled']
|
|
323
|
+
* });
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* // Observe when children are added/removed
|
|
327
|
+
* utils.observeSelector('.product-list', (element, mutation) => {
|
|
328
|
+
* if (mutation.type === 'childList') {
|
|
329
|
+
* console.log('Children changed:', mutation.addedNodes.length, 'added');
|
|
330
|
+
* }
|
|
331
|
+
* }, {
|
|
332
|
+
* mutations: ['childList', 'subtree']
|
|
333
|
+
* });
|
|
334
|
+
*/
|
|
335
|
+
observeSelector: function(selector, callback, options = {}) {
|
|
336
|
+
const {
|
|
337
|
+
mutations = ['childList', 'attributes', 'subtree'],
|
|
338
|
+
timeout = null,
|
|
339
|
+
onTimeout = null,
|
|
340
|
+
attributeFilter = null
|
|
341
|
+
} = options;
|
|
342
|
+
|
|
343
|
+
// Build MutationObserver options
|
|
344
|
+
const observerOptions = {
|
|
345
|
+
childList: mutations.includes('childList'),
|
|
346
|
+
attributes: mutations.includes('attributes'),
|
|
347
|
+
characterData: mutations.includes('characterData'),
|
|
348
|
+
subtree: mutations.includes('subtree'),
|
|
349
|
+
attributeOldValue: mutations.includes('attributeOldValue'),
|
|
350
|
+
characterDataOldValue: mutations.includes('characterDataOldValue')
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (attributeFilter && Array.isArray(attributeFilter)) {
|
|
354
|
+
observerOptions.attributeFilter = attributeFilter;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let observer = null;
|
|
358
|
+
let timeoutId = null;
|
|
359
|
+
let isStopped = false;
|
|
360
|
+
|
|
361
|
+
// Get or wait for element
|
|
362
|
+
const element = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
363
|
+
|
|
364
|
+
function startObserving(el) {
|
|
365
|
+
if (!el || isStopped) return;
|
|
366
|
+
|
|
367
|
+
observer = new MutationObserver((mutationRecords, obs) => {
|
|
368
|
+
if (isStopped) return;
|
|
369
|
+
mutationRecords.forEach(mutation => {
|
|
370
|
+
callback(el, mutation, obs);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
observer.observe(el, observerOptions);
|
|
375
|
+
|
|
376
|
+
if (timeout) {
|
|
377
|
+
timeoutId = setTimeout(() => {
|
|
378
|
+
stopObserving();
|
|
379
|
+
if (onTimeout) onTimeout();
|
|
380
|
+
}, timeout);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function stopObserving() {
|
|
385
|
+
isStopped = true;
|
|
386
|
+
if (observer) {
|
|
387
|
+
observer.disconnect();
|
|
388
|
+
observer = null;
|
|
389
|
+
}
|
|
390
|
+
if (timeoutId) {
|
|
391
|
+
clearTimeout(timeoutId);
|
|
392
|
+
timeoutId = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (element) {
|
|
397
|
+
// Element exists, start observing immediately
|
|
398
|
+
startObserving(element);
|
|
399
|
+
} else if (typeof selector === 'string') {
|
|
400
|
+
// Element doesn't exist yet, wait for it
|
|
401
|
+
this.waitForElement(selector).then(el => {
|
|
402
|
+
if (!isStopped) {
|
|
403
|
+
startObserving(el);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return stopObserving;
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Observe mutations on all elements matching the selector
|
|
413
|
+
* The callback is called every time any matching element is mutated
|
|
414
|
+
* Note: This observes all matching elements. Use observeSelector() to observe only the first element.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} selector - CSS selector to observe
|
|
417
|
+
* @param {Function} callback - Callback function that receives (element, mutationRecord, observer)
|
|
418
|
+
* @param {Object} options - Options object
|
|
419
|
+
* @param {Array<string>} options.mutations - Array of mutation types to observe. Options:
|
|
420
|
+
* - 'childList' - Watch for child nodes being added/removed (default: true)
|
|
421
|
+
* - 'attributes' - Watch for attribute changes (default: true)
|
|
422
|
+
* - 'characterData' - Watch for text content changes (default: false)
|
|
423
|
+
* - 'subtree' - Watch all descendants, not just direct children (default: true)
|
|
424
|
+
* - 'attributeOldValue' - Include old attribute value in mutation record (default: false)
|
|
425
|
+
* - 'characterDataOldValue' - Include old text value in mutation record (default: false)
|
|
426
|
+
* - 'attributeFilter' - Array of attribute names to observe (only these attributes will trigger)
|
|
427
|
+
* @param {number} options.timeout - Timeout in milliseconds (optional)
|
|
428
|
+
* @param {Function} options.onTimeout - Function to call on timeout (optional)
|
|
429
|
+
* @returns {Function} Function to stop observing
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* // Observe all product cards for attribute changes
|
|
433
|
+
* const stopObserving = utils.observeSelectors('.product-card', (element, mutation) => {
|
|
434
|
+
* if (mutation.type === 'attributes' && mutation.attributeName === 'data-price') {
|
|
435
|
+
* console.log('Price changed on:', element);
|
|
436
|
+
* }
|
|
437
|
+
* }, {
|
|
438
|
+
* mutations: ['attributes'],
|
|
439
|
+
* attributeFilter: ['data-price', 'class']
|
|
440
|
+
* });
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* // Observe when children are added to any matching container
|
|
444
|
+
* utils.observeSelectors('.dynamic-list', (element, mutation) => {
|
|
445
|
+
* if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
|
446
|
+
* console.log('New items added to:', element);
|
|
447
|
+
* }
|
|
448
|
+
* }, {
|
|
449
|
+
* mutations: ['childList']
|
|
450
|
+
* });
|
|
451
|
+
*/
|
|
452
|
+
observeSelectors: function(selector, callback, options = {}) {
|
|
453
|
+
const {
|
|
454
|
+
mutations = ['childList', 'attributes', 'subtree'],
|
|
455
|
+
timeout = null,
|
|
456
|
+
onTimeout = null,
|
|
457
|
+
attributeFilter = null
|
|
458
|
+
} = options;
|
|
459
|
+
|
|
460
|
+
// Build MutationObserver options
|
|
461
|
+
const observerOptions = {
|
|
462
|
+
childList: mutations.includes('childList'),
|
|
463
|
+
attributes: mutations.includes('attributes'),
|
|
464
|
+
characterData: mutations.includes('characterData'),
|
|
465
|
+
subtree: mutations.includes('subtree'),
|
|
466
|
+
attributeOldValue: mutations.includes('attributeOldValue'),
|
|
467
|
+
characterDataOldValue: mutations.includes('characterDataOldValue')
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
if (attributeFilter && Array.isArray(attributeFilter)) {
|
|
471
|
+
observerOptions.attributeFilter = attributeFilter;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const observers = new Map(); // Map of element -> observer
|
|
475
|
+
let timeoutId = null;
|
|
476
|
+
|
|
477
|
+
// Function to observe a specific element
|
|
478
|
+
function observeElement(element) {
|
|
479
|
+
if (observers.has(element)) return; // Already observing
|
|
480
|
+
|
|
481
|
+
const observer = new MutationObserver((mutationRecords, obs) => {
|
|
482
|
+
mutationRecords.forEach(mutation => {
|
|
483
|
+
callback(element, mutation, obs);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
observer.observe(element, observerOptions);
|
|
488
|
+
observers.set(element, observer);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Function to find and observe all matching elements
|
|
492
|
+
function findAndObserveElements() {
|
|
493
|
+
const elements = document.querySelectorAll(selector);
|
|
494
|
+
elements.forEach(el => observeElement(el));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Observe existing elements
|
|
498
|
+
findAndObserveElements();
|
|
499
|
+
|
|
500
|
+
// Set up observer to watch for new elements being added
|
|
501
|
+
const rootObserver = new MutationObserver(() => {
|
|
502
|
+
findAndObserveElements();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
rootObserver.observe(document.body, {
|
|
506
|
+
childList: true,
|
|
507
|
+
subtree: true
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (timeout) {
|
|
511
|
+
timeoutId = setTimeout(() => {
|
|
512
|
+
stopObserving();
|
|
513
|
+
if (onTimeout) onTimeout();
|
|
514
|
+
}, timeout);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function stopObserving() {
|
|
518
|
+
rootObserver.disconnect();
|
|
519
|
+
observers.forEach(observer => observer.disconnect());
|
|
520
|
+
observers.clear();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return stopObserving;
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Poll - repeatedly execute a callback at specified intervals
|
|
528
|
+
* @param {Function} callback - Function to execute
|
|
529
|
+
* @param {number} delay - Delay in milliseconds between executions
|
|
530
|
+
* @returns {Function} Function to cancel polling
|
|
531
|
+
*/
|
|
532
|
+
poll: function(callback, delay) {
|
|
533
|
+
const intervalId = setInterval(callback, delay);
|
|
534
|
+
return () => clearInterval(intervalId);
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Get element text content
|
|
539
|
+
* @param {Element|string} element - Element or selector
|
|
540
|
+
* @returns {string} Text content
|
|
541
|
+
*/
|
|
542
|
+
getText: function(element) {
|
|
543
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
544
|
+
return el ? el.textContent : '';
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Set element text content
|
|
549
|
+
* @param {Element|string} element - Element or selector
|
|
550
|
+
* @param {string} text - Text to set
|
|
551
|
+
*/
|
|
552
|
+
setText: function(element, text) {
|
|
553
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
554
|
+
if (el) el.textContent = text;
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Get element inner HTML
|
|
559
|
+
* @param {Element|string} element - Element or selector
|
|
560
|
+
* @returns {string} HTML content
|
|
561
|
+
*/
|
|
562
|
+
getHTML: function(element) {
|
|
563
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
564
|
+
return el ? el.innerHTML : '';
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Set element inner HTML
|
|
569
|
+
* @param {Element|string} element - Element or selector
|
|
570
|
+
* @param {string} html - HTML to set
|
|
571
|
+
*/
|
|
572
|
+
setHTML: function(element, html) {
|
|
573
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
574
|
+
if (el) el.innerHTML = html;
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Get element attribute value
|
|
579
|
+
* @param {Element|string} element - Element or selector
|
|
580
|
+
* @param {string} attr - Attribute name
|
|
581
|
+
* @returns {string|null} Attribute value or null
|
|
582
|
+
*/
|
|
583
|
+
getAttr: function(element, attr) {
|
|
584
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
585
|
+
return el ? el.getAttribute(attr) : null;
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Set element attribute
|
|
590
|
+
* @param {Element|string} element - Element or selector
|
|
591
|
+
* @param {string} attr - Attribute name
|
|
592
|
+
* @param {string} value - Attribute value
|
|
593
|
+
*/
|
|
594
|
+
setAttr: function(element, attr, value) {
|
|
595
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
596
|
+
if (el) el.setAttribute(attr, value);
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Remove element attribute
|
|
601
|
+
* @param {Element|string} element - Element or selector
|
|
602
|
+
* @param {string} attr - Attribute name
|
|
603
|
+
*/
|
|
604
|
+
removeAttr: function(element, attr) {
|
|
605
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
606
|
+
if (el) el.removeAttribute(attr);
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Get data attribute value
|
|
611
|
+
* @param {Element|string} element - Element or selector
|
|
612
|
+
* @param {string} name - Data attribute name (without 'data-' prefix)
|
|
613
|
+
* @returns {string|null} Data attribute value or null
|
|
614
|
+
*/
|
|
615
|
+
getData: function(element, name) {
|
|
616
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
617
|
+
return el ? el.getAttribute(`data-${name}`) : null;
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Set data attribute
|
|
622
|
+
* @param {Element|string} element - Element or selector
|
|
623
|
+
* @param {string} name - Data attribute name (without 'data-' prefix)
|
|
624
|
+
* @param {string} value - Data attribute value
|
|
625
|
+
*/
|
|
626
|
+
setData: function(element, name, value) {
|
|
627
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
628
|
+
if (el) el.setAttribute(`data-${name}`, value);
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get computed style property value
|
|
633
|
+
* @param {Element|string} element - Element or selector
|
|
634
|
+
* @param {string} property - CSS property name
|
|
635
|
+
* @returns {string} Computed style value
|
|
636
|
+
*/
|
|
637
|
+
getStyle: function(element, property) {
|
|
638
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
639
|
+
if (!el) return '';
|
|
640
|
+
const computed = window.getComputedStyle(el);
|
|
641
|
+
return property ? computed.getPropertyValue(property) : computed;
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Set element style property
|
|
646
|
+
* @param {Element|string} element - Element or selector
|
|
647
|
+
* @param {string|Object} property - CSS property name or object of properties
|
|
648
|
+
* @param {string} value - CSS value (if property is string)
|
|
649
|
+
*/
|
|
650
|
+
setStyle: function(element, property, value) {
|
|
651
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
652
|
+
if (!el) return;
|
|
653
|
+
|
|
654
|
+
if (typeof property === 'object') {
|
|
655
|
+
Object.assign(el.style, property);
|
|
656
|
+
} else {
|
|
657
|
+
el.style[property] = value;
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Get or set element value (for form inputs)
|
|
663
|
+
* @param {Element|string} element - Element or selector
|
|
664
|
+
* @param {string} value - Value to set (optional)
|
|
665
|
+
* @returns {string|undefined} Current value if getting, undefined if setting
|
|
666
|
+
*/
|
|
667
|
+
val: function(element, value) {
|
|
668
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
669
|
+
if (!el) return undefined;
|
|
670
|
+
|
|
671
|
+
if (value === undefined) {
|
|
672
|
+
return el.value || '';
|
|
673
|
+
} else {
|
|
674
|
+
el.value = value;
|
|
675
|
+
// Trigger input event for React/Vue compatibility
|
|
676
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
677
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Check if element is visible (not hidden by display, visibility, or opacity)
|
|
683
|
+
* @param {Element|string} element - Element or selector
|
|
684
|
+
* @returns {boolean} True if element is visible
|
|
685
|
+
*/
|
|
686
|
+
isVisible: function(element) {
|
|
687
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
688
|
+
if (!el) return false;
|
|
689
|
+
|
|
690
|
+
const style = window.getComputedStyle(el);
|
|
691
|
+
return style.display !== 'none' &&
|
|
692
|
+
style.visibility !== 'hidden' &&
|
|
693
|
+
style.opacity !== '0' &&
|
|
694
|
+
el.offsetWidth > 0 &&
|
|
695
|
+
el.offsetHeight > 0;
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Show element (set display to block or original value)
|
|
700
|
+
* @param {Element|string} element - Element or selector
|
|
701
|
+
* @param {string} display - Display value (default: 'block')
|
|
702
|
+
*/
|
|
703
|
+
show: function(element, display = 'block') {
|
|
704
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
705
|
+
if (el) {
|
|
706
|
+
if (!el.dataset.originalDisplay) {
|
|
707
|
+
el.dataset.originalDisplay = window.getComputedStyle(el).display;
|
|
708
|
+
}
|
|
709
|
+
el.style.display = display;
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Hide element (set display to none)
|
|
715
|
+
* @param {Element|string} element - Element or selector
|
|
716
|
+
*/
|
|
717
|
+
hide: function(element) {
|
|
718
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
719
|
+
if (el) {
|
|
720
|
+
if (!el.dataset.originalDisplay) {
|
|
721
|
+
el.dataset.originalDisplay = window.getComputedStyle(el).display;
|
|
722
|
+
}
|
|
723
|
+
el.style.display = 'none';
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Get element dimensions and position
|
|
729
|
+
* @param {Element|string} element - Element or selector
|
|
730
|
+
* @returns {Object} Object with width, height, top, left, right, bottom
|
|
731
|
+
*/
|
|
732
|
+
getRect: function(element) {
|
|
733
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
734
|
+
if (!el) return null;
|
|
735
|
+
|
|
736
|
+
const rect = el.getBoundingClientRect();
|
|
737
|
+
return {
|
|
738
|
+
width: rect.width,
|
|
739
|
+
height: rect.height,
|
|
740
|
+
top: rect.top,
|
|
741
|
+
left: rect.left,
|
|
742
|
+
right: rect.right,
|
|
743
|
+
bottom: rect.bottom,
|
|
744
|
+
x: rect.x,
|
|
745
|
+
y: rect.y
|
|
746
|
+
};
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get element offset (position relative to document)
|
|
751
|
+
* @param {Element|string} element - Element or selector
|
|
752
|
+
* @returns {Object} Object with top and left offset
|
|
753
|
+
*/
|
|
754
|
+
getOffset: function(element) {
|
|
755
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
756
|
+
if (!el) return null;
|
|
757
|
+
|
|
758
|
+
const rect = el.getBoundingClientRect();
|
|
759
|
+
return {
|
|
760
|
+
top: rect.top + window.scrollY,
|
|
761
|
+
left: rect.left + window.scrollX
|
|
762
|
+
};
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Get or set URL query parameters
|
|
767
|
+
* @param {string} name - Parameter name (optional, if omitted returns all params)
|
|
768
|
+
* @param {string} value - Value to set (optional)
|
|
769
|
+
* @param {string} url - URL to use (defaults to current location)
|
|
770
|
+
* @returns {string|Object|null} Parameter value, all params object, or null
|
|
771
|
+
*/
|
|
772
|
+
queryParam: function(name, value, url = window.location.href) {
|
|
773
|
+
const urlObj = new URL(url);
|
|
774
|
+
|
|
775
|
+
if (name === undefined) {
|
|
776
|
+
// Return all params as object
|
|
777
|
+
const params = {};
|
|
778
|
+
urlObj.searchParams.forEach((val, key) => {
|
|
779
|
+
params[key] = val;
|
|
780
|
+
});
|
|
781
|
+
return params;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (value === undefined) {
|
|
785
|
+
// Get param
|
|
786
|
+
return urlObj.searchParams.get(name);
|
|
787
|
+
} else {
|
|
788
|
+
// Set param
|
|
789
|
+
urlObj.searchParams.set(name, value);
|
|
790
|
+
return urlObj.toString();
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Update URL without page reload
|
|
796
|
+
* @param {string} url - New URL
|
|
797
|
+
* @param {boolean} replace - If true, replace current history entry (default: false)
|
|
798
|
+
*/
|
|
799
|
+
updateURL: function(url, replace = false) {
|
|
800
|
+
if (replace) {
|
|
801
|
+
window.history.replaceState({}, '', url);
|
|
802
|
+
} else {
|
|
803
|
+
window.history.pushState({}, '', url);
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Debounce function - delays execution until after wait time
|
|
809
|
+
* @param {Function} func - Function to debounce
|
|
810
|
+
* @param {number} wait - Wait time in milliseconds
|
|
811
|
+
* @param {boolean} immediate - If true, trigger on leading edge (default: false)
|
|
812
|
+
* @returns {Function} Debounced function
|
|
813
|
+
*/
|
|
814
|
+
debounce: function(func, wait, immediate = false) {
|
|
815
|
+
let timeout;
|
|
816
|
+
return function executedFunction(...args) {
|
|
817
|
+
const later = () => {
|
|
818
|
+
timeout = null;
|
|
819
|
+
if (!immediate) func.apply(this, args);
|
|
820
|
+
};
|
|
821
|
+
const callNow = immediate && !timeout;
|
|
822
|
+
clearTimeout(timeout);
|
|
823
|
+
timeout = setTimeout(later, wait);
|
|
824
|
+
if (callNow) func.apply(this, args);
|
|
825
|
+
};
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Throttle function - limits execution to once per wait time
|
|
830
|
+
* @param {Function} func - Function to throttle
|
|
831
|
+
* @param {number} wait - Wait time in milliseconds
|
|
832
|
+
* @returns {Function} Throttled function
|
|
833
|
+
*/
|
|
834
|
+
throttle: function(func, wait) {
|
|
835
|
+
let inThrottle;
|
|
836
|
+
return function executedFunction(...args) {
|
|
837
|
+
if (!inThrottle) {
|
|
838
|
+
func.apply(this, args);
|
|
839
|
+
inThrottle = true;
|
|
840
|
+
setTimeout(() => inThrottle = false, wait);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Get localStorage item
|
|
847
|
+
* @param {string} key - Storage key
|
|
848
|
+
* @returns {string|null} Stored value or null
|
|
849
|
+
*/
|
|
850
|
+
getLocalStorage: function(key) {
|
|
851
|
+
try {
|
|
852
|
+
return localStorage.getItem(key);
|
|
853
|
+
} catch (e) {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Set localStorage item
|
|
860
|
+
* @param {string} key - Storage key
|
|
861
|
+
* @param {string} value - Value to store
|
|
862
|
+
*/
|
|
863
|
+
setLocalStorage: function(key, value) {
|
|
864
|
+
try {
|
|
865
|
+
localStorage.setItem(key, value);
|
|
866
|
+
} catch (e) {
|
|
867
|
+
console.warn('Failed to set localStorage:', e);
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Remove localStorage item
|
|
873
|
+
* @param {string} key - Storage key
|
|
874
|
+
*/
|
|
875
|
+
removeLocalStorage: function(key) {
|
|
876
|
+
try {
|
|
877
|
+
localStorage.removeItem(key);
|
|
878
|
+
} catch (e) {
|
|
879
|
+
console.warn('Failed to remove localStorage:', e);
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Get sessionStorage item
|
|
885
|
+
* @param {string} key - Storage key
|
|
886
|
+
* @returns {string|null} Stored value or null
|
|
887
|
+
*/
|
|
888
|
+
getSessionStorage: function(key) {
|
|
889
|
+
try {
|
|
890
|
+
return sessionStorage.getItem(key);
|
|
891
|
+
} catch (e) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Set sessionStorage item
|
|
898
|
+
* @param {string} key - Storage key
|
|
899
|
+
* @param {string} value - Value to store
|
|
900
|
+
*/
|
|
901
|
+
setSessionStorage: function(key, value) {
|
|
902
|
+
try {
|
|
903
|
+
sessionStorage.setItem(key, value);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
console.warn('Failed to set sessionStorage:', e);
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Remove sessionStorage item
|
|
911
|
+
* @param {string} key - Storage key
|
|
912
|
+
*/
|
|
913
|
+
removeSessionStorage: function(key) {
|
|
914
|
+
try {
|
|
915
|
+
sessionStorage.removeItem(key);
|
|
916
|
+
} catch (e) {
|
|
917
|
+
console.warn('Failed to remove sessionStorage:', e);
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Check if device is mobile
|
|
923
|
+
* @returns {boolean} True if mobile device
|
|
924
|
+
*/
|
|
925
|
+
isMobile: function() {
|
|
926
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
927
|
+
},
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Check if device is tablet
|
|
931
|
+
* @returns {boolean} True if tablet device
|
|
932
|
+
*/
|
|
933
|
+
isTablet: function() {
|
|
934
|
+
return /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768 && window.innerWidth <= 1024;
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Check if device is desktop
|
|
939
|
+
* @returns {boolean} True if desktop device
|
|
940
|
+
*/
|
|
941
|
+
isDesktop: function() {
|
|
942
|
+
return !this.isMobile() && !this.isTablet();
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Get device type
|
|
947
|
+
* @returns {string} 'mobile', 'tablet', or 'desktop'
|
|
948
|
+
*/
|
|
949
|
+
getDeviceType: function() {
|
|
950
|
+
if (this.isMobile()) return 'mobile';
|
|
951
|
+
if (this.isTablet()) return 'tablet';
|
|
952
|
+
return 'desktop';
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Get browser name
|
|
957
|
+
* @returns {string} Browser name (chrome, firefox, safari, edge, etc.)
|
|
958
|
+
*/
|
|
959
|
+
getBrowser: function() {
|
|
960
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
961
|
+
if (ua.includes('chrome') && !ua.includes('edg')) return 'chrome';
|
|
962
|
+
if (ua.includes('firefox')) return 'firefox';
|
|
963
|
+
if (ua.includes('safari') && !ua.includes('chrome')) return 'safari';
|
|
964
|
+
if (ua.includes('edg')) return 'edge';
|
|
965
|
+
if (ua.includes('opera') || ua.includes('opr')) return 'opera';
|
|
966
|
+
return 'unknown';
|
|
967
|
+
},
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Wait for DOM ready
|
|
971
|
+
* @returns {Promise} Promise that resolves when DOM is ready
|
|
972
|
+
*/
|
|
973
|
+
ready: function() {
|
|
974
|
+
return new Promise((resolve) => {
|
|
975
|
+
if (document.readyState === 'loading') {
|
|
976
|
+
document.addEventListener('DOMContentLoaded', resolve);
|
|
977
|
+
} else {
|
|
978
|
+
resolve();
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
},
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Wait for window load
|
|
985
|
+
* @returns {Promise} Promise that resolves when window is loaded
|
|
986
|
+
*/
|
|
987
|
+
load: function() {
|
|
988
|
+
return new Promise((resolve) => {
|
|
989
|
+
if (document.readyState === 'complete') {
|
|
990
|
+
resolve();
|
|
991
|
+
} else {
|
|
992
|
+
window.addEventListener('load', resolve);
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
},
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Create an element
|
|
999
|
+
* @param {string} tag - HTML tag name
|
|
1000
|
+
* @param {Object} attrs - Attributes object
|
|
1001
|
+
* @param {string|Element} content - Text content or child element
|
|
1002
|
+
* @returns {Element} Created element
|
|
1003
|
+
*/
|
|
1004
|
+
createElement: function(tag, attrs = {}, content = '') {
|
|
1005
|
+
const el = document.createElement(tag);
|
|
1006
|
+
|
|
1007
|
+
Object.keys(attrs).forEach(key => {
|
|
1008
|
+
if (key === 'class') {
|
|
1009
|
+
el.className = attrs[key];
|
|
1010
|
+
} else if (key === 'style' && typeof attrs[key] === 'object') {
|
|
1011
|
+
Object.assign(el.style, attrs[key]);
|
|
1012
|
+
} else {
|
|
1013
|
+
el.setAttribute(key, attrs[key]);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
if (typeof content === 'string') {
|
|
1018
|
+
el.textContent = content;
|
|
1019
|
+
} else if (content instanceof Element) {
|
|
1020
|
+
el.appendChild(content);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return el;
|
|
1024
|
+
},
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Append element to parent
|
|
1028
|
+
* @param {Element|string} parent - Parent element or selector
|
|
1029
|
+
* @param {Element|string} child - Child element or selector
|
|
1030
|
+
*/
|
|
1031
|
+
append: function(parent, child) {
|
|
1032
|
+
const parentEl = typeof parent === 'string' ? document.querySelector(parent) : parent;
|
|
1033
|
+
const childEl = typeof child === 'string' ? document.querySelector(child) : child;
|
|
1034
|
+
if (parentEl && childEl) {
|
|
1035
|
+
parentEl.appendChild(childEl);
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Prepend element to parent
|
|
1041
|
+
* @param {Element|string} parent - Parent element or selector
|
|
1042
|
+
* @param {Element|string} child - Child element or selector
|
|
1043
|
+
*/
|
|
1044
|
+
prepend: function(parent, child) {
|
|
1045
|
+
const parentEl = typeof parent === 'string' ? document.querySelector(parent) : parent;
|
|
1046
|
+
const childEl = typeof child === 'string' ? document.querySelector(child) : child;
|
|
1047
|
+
if (parentEl && childEl) {
|
|
1048
|
+
parentEl.insertBefore(childEl, parentEl.firstChild);
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Remove element from DOM
|
|
1054
|
+
* @param {Element|string} element - Element or selector
|
|
1055
|
+
*/
|
|
1056
|
+
remove: function(element) {
|
|
1057
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1058
|
+
if (el && el.parentNode) {
|
|
1059
|
+
el.parentNode.removeChild(el);
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Clone element
|
|
1065
|
+
* @param {Element|string} element - Element or selector
|
|
1066
|
+
* @param {boolean} deep - Deep clone (default: true)
|
|
1067
|
+
* @returns {Element} Cloned element
|
|
1068
|
+
*/
|
|
1069
|
+
clone: function(element, deep = true) {
|
|
1070
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1071
|
+
return el ? el.cloneNode(deep) : null;
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Get parent element
|
|
1076
|
+
* @param {Element|string} element - Element or selector
|
|
1077
|
+
* @param {string} selector - Optional selector to match parent
|
|
1078
|
+
* @returns {Element|null} Parent element or null
|
|
1079
|
+
*/
|
|
1080
|
+
parent: function(element, selector) {
|
|
1081
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1082
|
+
if (!el) return null;
|
|
1083
|
+
|
|
1084
|
+
if (selector) {
|
|
1085
|
+
return el.closest(selector);
|
|
1086
|
+
}
|
|
1087
|
+
return el.parentElement;
|
|
1088
|
+
},
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Get children elements
|
|
1092
|
+
* @param {Element|string} element - Element or selector
|
|
1093
|
+
* @param {string} selector - Optional selector to filter children
|
|
1094
|
+
* @returns {Array} Array of child elements
|
|
1095
|
+
*/
|
|
1096
|
+
children: function(element, selector) {
|
|
1097
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1098
|
+
if (!el) return [];
|
|
1099
|
+
|
|
1100
|
+
const children = Array.from(el.children);
|
|
1101
|
+
if (selector) {
|
|
1102
|
+
return children.filter(child => child.matches(selector));
|
|
1103
|
+
}
|
|
1104
|
+
return children;
|
|
1105
|
+
},
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Get siblings of element
|
|
1109
|
+
* @param {Element|string} element - Element or selector
|
|
1110
|
+
* @param {string} selector - Optional selector to filter siblings
|
|
1111
|
+
* @returns {Array} Array of sibling elements
|
|
1112
|
+
*/
|
|
1113
|
+
siblings: function(element, selector) {
|
|
1114
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1115
|
+
if (!el || !el.parentNode) return [];
|
|
1116
|
+
|
|
1117
|
+
const siblings = Array.from(el.parentNode.children).filter(child => child !== el);
|
|
1118
|
+
if (selector) {
|
|
1119
|
+
return siblings.filter(sibling => sibling.matches(selector));
|
|
1120
|
+
}
|
|
1121
|
+
return siblings;
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Find element within context
|
|
1126
|
+
* @param {Element|string} context - Context element or selector
|
|
1127
|
+
* @param {string} selector - CSS selector
|
|
1128
|
+
* @returns {Element|null} Found element or null
|
|
1129
|
+
*/
|
|
1130
|
+
find: function(context, selector) {
|
|
1131
|
+
const ctx = typeof context === 'string' ? document.querySelector(context) : context;
|
|
1132
|
+
return ctx ? ctx.querySelector(selector) : null;
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Find all elements within context
|
|
1137
|
+
* @param {Element|string} context - Context element or selector
|
|
1138
|
+
* @param {string} selector - CSS selector
|
|
1139
|
+
* @returns {NodeList} NodeList of found elements
|
|
1140
|
+
*/
|
|
1141
|
+
findAll: function(context, selector) {
|
|
1142
|
+
const ctx = typeof context === 'string' ? document.querySelector(context) : context;
|
|
1143
|
+
return ctx ? ctx.querySelectorAll(selector) : [];
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Check if element matches selector
|
|
1148
|
+
* @param {Element|string} element - Element or selector
|
|
1149
|
+
* @param {string} selector - CSS selector to match
|
|
1150
|
+
* @returns {boolean} True if element matches selector
|
|
1151
|
+
*/
|
|
1152
|
+
matches: function(element, selector) {
|
|
1153
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1154
|
+
return el ? el.matches(selector) : false;
|
|
1155
|
+
},
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Get closest ancestor matching selector
|
|
1159
|
+
* @param {Element|string} element - Element or selector
|
|
1160
|
+
* @param {string} selector - CSS selector
|
|
1161
|
+
* @returns {Element|null} Closest matching element or null
|
|
1162
|
+
*/
|
|
1163
|
+
closest: function(element, selector) {
|
|
1164
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1165
|
+
return el ? el.closest(selector) : null;
|
|
1166
|
+
},
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Track scroll depth
|
|
1170
|
+
* @param {Function} callback - Callback function that receives depth percentage
|
|
1171
|
+
* @param {Array} thresholds - Array of depth thresholds to trigger (default: [25, 50, 75, 100])
|
|
1172
|
+
* @returns {Function} Function to stop tracking
|
|
1173
|
+
*/
|
|
1174
|
+
trackScrollDepth: function(callback, thresholds = [25, 50, 75, 100]) {
|
|
1175
|
+
const triggered = new Set();
|
|
1176
|
+
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
|
|
1177
|
+
|
|
1178
|
+
const handleScroll = this.throttle(() => {
|
|
1179
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
1180
|
+
const depth = Math.round((scrollTop / maxScroll) * 100);
|
|
1181
|
+
|
|
1182
|
+
thresholds.forEach(threshold => {
|
|
1183
|
+
if (depth >= threshold && !triggered.has(threshold)) {
|
|
1184
|
+
triggered.add(threshold);
|
|
1185
|
+
callback(threshold, depth);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}, 100);
|
|
1189
|
+
|
|
1190
|
+
window.addEventListener('scroll', handleScroll);
|
|
1191
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
1192
|
+
},
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Track time on page
|
|
1196
|
+
* @param {Function} callback - Callback function that receives time in seconds
|
|
1197
|
+
* @param {number} interval - Check interval in milliseconds (default: 1000)
|
|
1198
|
+
* @returns {Function} Function to stop tracking
|
|
1199
|
+
*/
|
|
1200
|
+
trackTimeOnPage: function(callback, interval = 1000) {
|
|
1201
|
+
const startTime = Date.now();
|
|
1202
|
+
const intervalId = setInterval(() => {
|
|
1203
|
+
const timeOnPage = Math.floor((Date.now() - startTime) / 1000);
|
|
1204
|
+
callback(timeOnPage);
|
|
1205
|
+
}, interval);
|
|
1206
|
+
|
|
1207
|
+
return () => clearInterval(intervalId);
|
|
1208
|
+
},
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Track element visibility using Intersection Observer
|
|
1212
|
+
* @param {Element|string} element - Element or selector
|
|
1213
|
+
* @param {Function} callback - Callback function that receives visibility state
|
|
1214
|
+
* @param {Object} options - Intersection Observer options
|
|
1215
|
+
* @returns {Function} Function to stop observing
|
|
1216
|
+
*/
|
|
1217
|
+
trackVisibility: function(element, callback, options = {}) {
|
|
1218
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
1219
|
+
if (!el) return () => {};
|
|
1220
|
+
|
|
1221
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1222
|
+
entries.forEach(entry => {
|
|
1223
|
+
callback(entry.isIntersecting, entry.intersectionRatio, entry);
|
|
1224
|
+
});
|
|
1225
|
+
}, {
|
|
1226
|
+
threshold: options.threshold || 0,
|
|
1227
|
+
rootMargin: options.rootMargin || '0px',
|
|
1228
|
+
...options
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
observer.observe(el);
|
|
1232
|
+
return () => observer.disconnect();
|
|
1233
|
+
},
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Format number with commas
|
|
1237
|
+
* @param {number} num - Number to format
|
|
1238
|
+
* @returns {string} Formatted number string
|
|
1239
|
+
*/
|
|
1240
|
+
formatNumber: function(num) {
|
|
1241
|
+
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Format currency
|
|
1246
|
+
* @param {number} amount - Amount to format
|
|
1247
|
+
* @param {string} currency - Currency code (default: 'USD')
|
|
1248
|
+
* @param {string} locale - Locale (default: 'en-US')
|
|
1249
|
+
* @returns {string} Formatted currency string
|
|
1250
|
+
*/
|
|
1251
|
+
formatCurrency: function(amount, currency = 'USD', locale = 'en-US') {
|
|
1252
|
+
return new Intl.NumberFormat(locale, {
|
|
1253
|
+
style: 'currency',
|
|
1254
|
+
currency: currency
|
|
1255
|
+
}).format(amount);
|
|
1256
|
+
},
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Parse query string to object
|
|
1260
|
+
* @param {string} queryString - Query string (defaults to current search)
|
|
1261
|
+
* @returns {Object} Object with query parameters
|
|
1262
|
+
*/
|
|
1263
|
+
parseQuery: function(queryString = window.location.search) {
|
|
1264
|
+
const params = {};
|
|
1265
|
+
const urlParams = new URLSearchParams(queryString);
|
|
1266
|
+
urlParams.forEach((value, key) => {
|
|
1267
|
+
params[key] = value;
|
|
1268
|
+
});
|
|
1269
|
+
return params;
|
|
1270
|
+
},
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Build query string from object
|
|
1274
|
+
* @param {Object} params - Object with query parameters
|
|
1275
|
+
* @returns {string} Query string
|
|
1276
|
+
*/
|
|
1277
|
+
buildQuery: function(params) {
|
|
1278
|
+
const urlParams = new URLSearchParams();
|
|
1279
|
+
Object.keys(params).forEach(key => {
|
|
1280
|
+
if (params[key] !== null && params[key] !== undefined) {
|
|
1281
|
+
urlParams.append(key, params[key]);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
return urlParams.toString();
|
|
1285
|
+
},
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Get random number between min and max
|
|
1289
|
+
* @param {number} min - Minimum value
|
|
1290
|
+
* @param {number} max - Maximum value
|
|
1291
|
+
* @returns {number} Random number
|
|
1292
|
+
*/
|
|
1293
|
+
random: function(min, max) {
|
|
1294
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1295
|
+
},
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Get random item from array
|
|
1299
|
+
* @param {Array} array - Array to pick from
|
|
1300
|
+
* @returns {*} Random item
|
|
1301
|
+
*/
|
|
1302
|
+
randomItem: function(array) {
|
|
1303
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
1304
|
+
},
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Shuffle array
|
|
1308
|
+
* @param {Array} array - Array to shuffle
|
|
1309
|
+
* @returns {Array} Shuffled array (new array)
|
|
1310
|
+
*/
|
|
1311
|
+
shuffle: function(array) {
|
|
1312
|
+
const arr = [...array];
|
|
1313
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
1314
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1315
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1316
|
+
}
|
|
1317
|
+
return arr;
|
|
1318
|
+
},
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Deep clone object
|
|
1322
|
+
* @param {*} obj - Object to clone
|
|
1323
|
+
* @returns {*} Cloned object
|
|
1324
|
+
*/
|
|
1325
|
+
deepClone: function(obj) {
|
|
1326
|
+
return JSON.parse(JSON.stringify(obj));
|
|
1327
|
+
},
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Merge objects
|
|
1331
|
+
* @param {Object} target - Target object
|
|
1332
|
+
* @param {...Object} sources - Source objects
|
|
1333
|
+
* @returns {Object} Merged object
|
|
1334
|
+
*/
|
|
1335
|
+
merge: function(target, ...sources) {
|
|
1336
|
+
return Object.assign({}, target, ...sources);
|
|
1337
|
+
},
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Check if value is empty (null, undefined, empty string, empty array, empty object)
|
|
1341
|
+
* @param {*} value - Value to check
|
|
1342
|
+
* @returns {boolean} True if empty
|
|
1343
|
+
*/
|
|
1344
|
+
isEmpty: function(value) {
|
|
1345
|
+
if (value == null) return true;
|
|
1346
|
+
if (typeof value === 'string' || Array.isArray(value)) return value.length === 0;
|
|
1347
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
1348
|
+
return false;
|
|
260
1349
|
}
|
|
261
1350
|
};
|
|
262
1351
|
|