From b1771bda34b3641e6a1dc0ddb5bfc36585e6cc55 Mon Sep 17 00:00:00 2001 From: "Jorch C." Date: Wed, 1 Jan 2025 15:08:54 -0600 Subject: [PATCH 01/52] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 8de7f33..2677bc4 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,3 @@ After a pull request has been open for over 30 days with no activity or response - - -### Disclaimer -Even though this project has Webflow on its name I'm by no means part of the webflow team, just a dev sharing a bit of the work I've done over the years and trying to help out others looking for easier ways to build for the web. From 6c6f69b0e2b67d8a94a4f872bfbcaa305af529fb Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Tue, 23 Sep 2025 14:42:34 -0700 Subject: [PATCH 02/52] Update monthly hits badge in README Signed-off-by: Jorge C. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2677bc4..484ad6c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ GitHub License - Static Badge + Static Badge


From 9a9eb88531fe6d53db041ef39e1d18abee4461e8 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Wed, 24 Sep 2025 12:22:11 -0700 Subject: [PATCH 03/52] Fix error handling for undefined platform in ShareLink Signed-off-by: Jorge C. --- Dist/Functional/ShareLink.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dist/Functional/ShareLink.js b/Dist/Functional/ShareLink.js index b9c498d..f2797ad 100644 --- a/Dist/Functional/ShareLink.js +++ b/Dist/Functional/ShareLink.js @@ -26,9 +26,9 @@ class ShareLink { }; let _link = socialSelectors[this.platform]; - if(!_link) return; // handle errors if the platform has issues + if(!_link && !this.platform) return; // handle errors if the platform has issues - if(_link === 'copy') { + if(this.platform === 'copy') { this.element.addEventListener('click', () => navigator.clipboard.writeText(`${this.url}`)); } else{ From 1eed54f02b1b6aa83f9b91056166235bc2e6d29e Mon Sep 17 00:00:00 2001 From: Likhitha Kathireddy Date: Wed, 29 Oct 2025 16:43:38 -0400 Subject: [PATCH 04/52] Once you click the copy button it will say clicked and an alert message will pop up. --- Dist/Functional/ShareLink.js | 241 +++++++++++++++++++++++++++++++---- 1 file changed, 216 insertions(+), 25 deletions(-) diff --git a/Dist/Functional/ShareLink.js b/Dist/Functional/ShareLink.js index f2797ad..0ee50ce 100644 --- a/Dist/Functional/ShareLink.js +++ b/Dist/Functional/ShareLink.js @@ -1,53 +1,244 @@ 'use strict'; - class ShareLink { constructor(element) { + if (!(element instanceof HTMLElement)) { + throw new Error("Initialization failed: A valid HTMLElement was not provided to the constructor."); + } this.element = element; + console.log("Component created successfully with element:", this.element); this.platform = element.getAttribute('wt-share-element'); + + // Internal state for idempotent UI resets + this._copyResetTimer = null; + this._isCopyLocked = false; + + // Validate platform attribute + if (!this.platform || this.platform.trim() === '') { + console.warn('ShareLink: No platform specified for element:', element); + return; + } + this.title = document.title; this.url = window.location.href; + + // Validate URL + if (!this.url || this.url.trim() === '') { + console.error('ShareLink: Invalid URL detected:', this.url); + return; + } + this.encoded = this.encodeURIParams(); this.initializeShareLinks(); } encodeURIParams() { - let unencodedURI = `${this.url}&title='${this.title}'&description='${this.title}'`; - return encodeURI(unencodedURI); + const title = encodeURIComponent(this.title || ''); + const url = encodeURIComponent(this.url || ''); + return { title, url }; } initializeShareLinks() { const socialSelectors = { - facebook: `https://www.facebook.com/sharer/sharer.php?u=${this.encoded}`, - twitter: `https://twitter.com/share?url=${this.encoded}`, - linkedin: `https://www.linkedin.com/shareArticle?mini=true&url='${this.encoded}`, - whatsapp: `https://wa.me/?text=${this.encoded}`, - pinterest: `http://pinterest.com/pin/create/button/?url=${this.encoded}`, - reddit: `http://www.reddit.com/submit?url=${this.encoded}` + facebook: `https://www.facebook.com/sharer/sharer.php?u=${this.encoded.url}`, + twitter: `https://twitter.com/intent/tweet?url=${this.encoded.url}&text=${this.encoded.title}`, + linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${this.encoded.url}`, + whatsapp: `https://wa.me/?text=${this.encoded.title}%20${this.encoded.url}`, + pinterest:`https://www.pinterest.com/pin/create/button/?url=${this.encoded.url}&description=${this.encoded.title}`, + reddit: `https://www.reddit.com/submit?url=${this.encoded.url}&title=${this.encoded.title}` }; - let _link = socialSelectors[this.platform]; - if(!_link && !this.platform) return; // handle errors if the platform has issues + const _link = socialSelectors[this.platform]; - if(this.platform === 'copy') { - this.element.addEventListener('click', () => navigator.clipboard.writeText(`${this.url}`)); - } - else{ + if (this.platform === 'copy') { + // Prevent accidental navigation if element is an + this.element.removeAttribute('href'); + this.element.addEventListener('click', this.handleCopyClick.bind(this)); + } else { + if (!_link) { + console.error(`Unknown platform: ${this.platform}`); + return; + } this.element.setAttribute("href", _link); - this.element.setAttribute("target", '_blank'); + this.element.setAttribute("target", "_blank"); + this.element.setAttribute("rel", "noopener noreferrer"); + } + } + + async handleCopyClick(e) { + e.preventDefault(); + + // Optional short lock to avoid spam clicks + if (this._isCopyLocked) return; + this._isCopyLocked = true; + + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(this.url); + this.showCopySuccess(); + } else { + // Fallback for HTTP / older browsers + this.copyFallback(this.url); + } + } catch (error) { + this.copyFallback(this.url); + } finally { + // Re-enable quickly; adjust if you want a longer cooldown + setTimeout(() => (this._isCopyLocked = false), 300); + } + } + + copyFallback(text) { + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + try { + const ok = document.execCommand('copy'); + ok ? this.showCopySuccess() : this.showCopyError(); + } catch { + this.showCopyError(); + } + document.body.removeChild(ta); + } + + showCopySuccess() { + const originalText = this.element.textContent; + const originalStyle = this.element.style.cssText; + + // Clear any previous pending reset so timers don't stack + if (this._copyResetTimer) clearTimeout(this._copyResetTimer); + + // Update UI and temporarily disable clicks + this.element.textContent = '✓ Copied!'; + this.element.style.cssText = ` + background-color: #4caf50 !important; + color: white !important; + border-color: #4caf50 !important; + transform: scale(1.05); + transition: all 0.3s ease; + pointer-events: none; + `; + + this.showTemporaryMessage('Link copied to clipboard!', 'success'); + + // Single reset scheduled from the most recent click only + this._copyResetTimer = setTimeout(() => { + this.element.textContent = originalText; + this.element.style.cssText = originalStyle; + this._copyResetTimer = null; + }, 1000); + } + + showCopyError() { + const originalText = this.element.textContent; + const originalStyle = this.element.style.cssText; + + if (this._copyResetTimer) clearTimeout(this._copyResetTimer); + + this.element.textContent = '✗ Failed'; + this.element.style.cssText = ` + background-color: #f44336 !important; + color: white !important; + border-color: #f44336 !important; + transform: scale(1.05); + transition: all 0.3s ease; + pointer-events: none; + `; + + this.showTemporaryMessage('Copy failed - showing manual copy option', 'error'); + window.prompt('Copy this link manually:', this.url); + + this._copyResetTimer = setTimeout(() => { + this.element.textContent = originalText; + this.element.style.cssText = originalStyle; + this._copyResetTimer = null; + }, 1000); + } + + showTemporaryMessage(message, type) { + // Remove any existing message + const existingMessage = document.querySelector('.sharelink-message'); + if (existingMessage) existingMessage.remove(); + + // Create new message element + const messageDiv = document.createElement('div'); + messageDiv.className = 'sharelink-message'; + messageDiv.textContent = message; + messageDiv.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + border-radius: 6px; + color: white; + font-weight: bold; + z-index: 10000; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + transform: translateX(100%); + transition: transform 0.3s ease; + max-width: 300px; + word-wrap: break-word; + `; + + // Set background color based on type + messageDiv.style.backgroundColor = (type === 'success') ? '#4caf50' : '#f44336'; + + document.body.appendChild(messageDiv); + + // Animate in + setTimeout(() => { messageDiv.style.transform = 'translateX(0)'; }, 10); + + // Remove after 2 seconds + setTimeout(() => { + messageDiv.style.transform = 'translateX(100%)'; + setTimeout(() => { if (messageDiv.parentNode) messageDiv.remove(); }, 300); + }, 2000); + } + + // Cleanup method for proper memory management + destroy() { + if (this.element && this.platform === 'copy') { + // Remove event listeners by cloning the element + const newElement = this.element.cloneNode(true); + this.element.parentNode.replaceChild(newElement, this.element); + } + + // Remove from global array if it exists + if (window.webtricks) { + const index = window.webtricks.findIndex(item => item.ShareLink === this); + if (index > -1) { + window.webtricks.splice(index, 1); + } } } } -const InitializeShareLink = () => { - window.webtricks = window.webtricks || []; - let links = document.querySelectorAll("[wt-share-element]"); - if (!links || links.length === 0) return; +function InitializeShareLink() { + window.webtricks = window.webtricks || []; + const links = document.querySelectorAll("[wt-share-element]"); + if (!links || links.length === 0) return; + + links.forEach(link => { + // Check if element already has a ShareLink instance + if (link._shareLinkInstance) { + console.warn('ShareLink: Element already initialized, skipping:', link); + return; + } - links.forEach(link => { - let instance = new ShareLink(link); - window.webtricks.push({'ShareLink': instance }); - }); -}; + try { + const instance = new ShareLink(link); + if (instance.platform) { // Only add if platform was valid + link._shareLinkInstance = instance; + window.webtricks.push({ 'ShareLink': instance }); + } + } catch (error) { + console.error('ShareLink: Failed to initialize element:', link, error); + } + }); +} // Execute InitializeShareLink when the DOM is fully loaded if (/complete|interactive|loaded/.test(document.readyState)) { From f07411e78b4224ac94445d91a0862a89f9ab7528 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Thu, 6 Nov 2025 13:01:35 -0800 Subject: [PATCH 05/52] Update README with development follow link Added a link to follow development and news about WebTricks. Signed-off-by: Jorge C. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 484ad6c..3d19a7d 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,8 @@ All of the documentation is explained by functionality in documentation in my site for the complete list of functionalities and scripts available.

- WebTricks might have been started as a personal project, but I'm a believer that a project for the comunity by the comunity can offer way more value than any single dev could provide so feel free to contribute to this project and use any solution here. + WebTricks might have been started as a personal project, but I'm a believer that a project for the comunity by the comunity can offer way more value than any single dev could provide so feel free to contribute to this project and use any solution here.
+ You can follow development and news here

Getting Started

From 2b39f515c1daa0be62a5ebe4db6f2181c69af822 Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 13:05:35 -0800 Subject: [PATCH 06/52] Add .gitignore to exclude node_modules Created a .gitignore file to prevent node_modules directory from being tracked by git. --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file From 5dd766b340a337046e1cb6d43ca7ee49a0814c13 Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 13:06:07 -0800 Subject: [PATCH 07/52] Refactor ShareLink copy feedback and add template support Improves copy-to-clipboard feedback by supporting external HTML templates, custom success/error classes, and configurable timeouts. Refactors UI update logic for copy actions, adds screen reader support, and enables module export for testing environments. Also updates LinkedIn share URL to include title. --- Dist/Functional/ShareLink.js | 251 +++++++++++++++-------------------- 1 file changed, 105 insertions(+), 146 deletions(-) diff --git a/Dist/Functional/ShareLink.js b/Dist/Functional/ShareLink.js index 0ee50ce..7c31709 100644 --- a/Dist/Functional/ShareLink.js +++ b/Dist/Functional/ShareLink.js @@ -1,32 +1,42 @@ 'use strict'; + class ShareLink { constructor(element) { if (!(element instanceof HTMLElement)) { throw new Error("Initialization failed: A valid HTMLElement was not provided to the constructor."); } + this.element = element; - console.log("Component created successfully with element:", this.element); this.platform = element.getAttribute('wt-share-element'); + this.title = document.title; + this.url = window.location.href; - // Internal state for idempotent UI resets - this._copyResetTimer = null; - this._isCopyLocked = false; - - // Validate platform attribute if (!this.platform || this.platform.trim() === '') { console.warn('ShareLink: No platform specified for element:', element); return; } - this.title = document.title; - this.url = window.location.href; - - // Validate URL if (!this.url || this.url.trim() === '') { console.error('ShareLink: Invalid URL detected:', this.url); return; } + this.copySuccessClass = this.element.getAttribute('wt-share-copysuccess') || null; + this.copyErrorClass = this.element.getAttribute('wt-share-copyerror') || null; + this.copyMessage = this.element.getAttribute('wt-share-copymessage') || null; // plain text message + // Support external template element for success HTML: ... + const _copyTemplateEl = document.querySelector('[wt-share-copyelement="copied"]'); + this.copyMessageTemplate = _copyTemplateEl ? _copyTemplateEl.innerHTML : null; // html message from template element + this.copyFailMessage = this.element.getAttribute('wt-share-copymessage-fail') || 'failed to copy'; // plain text fail + + if ( this.copyMessage || this.copyMessageTemplate || this.copySuccessClass || this.copyErrorClass) { + this.copyResetTimer = null; + this.isCopyLocked = false; + this.copyTimeout = parseInt(this.element.getAttribute('wt-share-copytimeout'), 10) || 1000; + // Provide basic screen reader feedback when message swaps occur + this.element.setAttribute('aria-live', 'polite'); + } + this.encoded = this.encodeURIParams(); this.initializeShareLinks(); } @@ -41,7 +51,7 @@ class ShareLink { const socialSelectors = { facebook: `https://www.facebook.com/sharer/sharer.php?u=${this.encoded.url}`, twitter: `https://twitter.com/intent/tweet?url=${this.encoded.url}&text=${this.encoded.title}`, - linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${this.encoded.url}`, + linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${this.encoded.url}&title=${this.encoded.title}`, whatsapp: `https://wa.me/?text=${this.encoded.title}%20${this.encoded.url}`, pinterest:`https://www.pinterest.com/pin/create/button/?url=${this.encoded.url}&description=${this.encoded.title}`, reddit: `https://www.reddit.com/submit?url=${this.encoded.url}&title=${this.encoded.title}` @@ -67,177 +77,119 @@ class ShareLink { async handleCopyClick(e) { e.preventDefault(); - // Optional short lock to avoid spam clicks - if (this._isCopyLocked) return; - this._isCopyLocked = true; + if (this.isCopyLocked) return; + this.isCopyLocked = true; try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(this.url); - this.showCopySuccess(); + this.triggerCopyChange(); } else { - // Fallback for HTTP / older browsers this.copyFallback(this.url); } } catch (error) { + console.warn('ShareLink: Clipboard API failed, using fallback:', error); this.copyFallback(this.url); } finally { - // Re-enable quickly; adjust if you want a longer cooldown - setTimeout(() => (this._isCopyLocked = false), 300); + setTimeout(() => (this.isCopyLocked = false), 300); } } + /** Fallback method for copying text in unsupported environments + * @param {string} text - The text to be copied to the clipboard + * @deprecated Use navigator.clipboard API where possible, this is only for legacy support + * and will be removed in future versions. + */ copyFallback(text) { const ta = document.createElement('textarea'); ta.value = text; - ta.style.position = 'fixed'; - ta.style.opacity = '0'; - ta.style.left = '-9999px'; + // Use off-screen positioning to ensure selection works across browsers + ta.setAttribute('readonly', ''); + ta.style.cssText = 'position:absolute;left:-9999px;top:0;opacity:0;'; document.body.appendChild(ta); ta.select(); + try { ta.setSelectionRange(0, ta.value.length); } catch {} try { const ok = document.execCommand('copy'); - ok ? this.showCopySuccess() : this.showCopyError(); + ok ? this.triggerCopyChange() : this.triggerCopyChange(true); } catch { - this.showCopyError(); + this.triggerCopyChange(true); } document.body.removeChild(ta); } - showCopySuccess() { - const originalText = this.element.textContent; - const originalStyle = this.element.style.cssText; - - // Clear any previous pending reset so timers don't stack - if (this._copyResetTimer) clearTimeout(this._copyResetTimer); - - // Update UI and temporarily disable clicks - this.element.textContent = '✓ Copied!'; - this.element.style.cssText = ` - background-color: #4caf50 !important; - color: white !important; - border-color: #4caf50 !important; - transform: scale(1.05); - transition: all 0.3s ease; - pointer-events: none; - `; - - this.showTemporaryMessage('Link copied to clipboard!', 'success'); - - // Single reset scheduled from the most recent click only - this._copyResetTimer = setTimeout(() => { - this.element.textContent = originalText; - this.element.style.cssText = originalStyle; - this._copyResetTimer = null; - }, 1000); - } + triggerCopyChange(error = false) { + // Preserve original content across repeated triggers until reset + if (this._copyOriginalHTML == null) { + this._copyOriginalHTML = this.element.innerHTML; + } - showCopyError() { - const originalText = this.element.textContent; - const originalStyle = this.element.style.cssText; - - if (this._copyResetTimer) clearTimeout(this._copyResetTimer); - - this.element.textContent = '✗ Failed'; - this.element.style.cssText = ` - background-color: #f44336 !important; - color: white !important; - border-color: #f44336 !important; - transform: scale(1.05); - transition: all 0.3s ease; - pointer-events: none; - `; - - this.showTemporaryMessage('Copy failed - showing manual copy option', 'error'); - window.prompt('Copy this link manually:', this.url); - - this._copyResetTimer = setTimeout(() => { - this.element.textContent = originalText; - this.element.style.cssText = originalStyle; - this._copyResetTimer = null; - }, 1000); - } + if (this.copyResetTimer) clearTimeout(this.copyResetTimer); - showTemporaryMessage(message, type) { - // Remove any existing message - const existingMessage = document.querySelector('.sharelink-message'); - if (existingMessage) existingMessage.remove(); - - // Create new message element - const messageDiv = document.createElement('div'); - messageDiv.className = 'sharelink-message'; - messageDiv.textContent = message; - messageDiv.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 12px 20px; - border-radius: 6px; - color: white; - font-weight: bold; - z-index: 10000; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - transform: translateX(100%); - transition: transform 0.3s ease; - max-width: 300px; - word-wrap: break-word; - `; - - // Set background color based on type - messageDiv.style.backgroundColor = (type === 'success') ? '#4caf50' : '#f44336'; - - document.body.appendChild(messageDiv); - - // Animate in - setTimeout(() => { messageDiv.style.transform = 'translateX(0)'; }, 10); - - // Remove after 2 seconds - setTimeout(() => { - messageDiv.style.transform = 'translateX(100%)'; - setTimeout(() => { if (messageDiv.parentNode) messageDiv.remove(); }, 300); - }, 2000); - } + // Prefer HTML from external template element when provided, fallback to plain text attributes + const successContent = this.copyMessageTemplate != null ? this.copyMessageTemplate : this.copyMessage; + const failContent = this.copyFailMessage; // Only plain text fail for now + if (error) { + // Fail path uses plain text to avoid accidental HTML execution + this.element.textContent = failContent ?? ''; + } else if (successContent) { + this.element.innerHTML = successContent; + } - // Cleanup method for proper memory management - destroy() { - if (this.element && this.platform === 'copy') { - // Remove event listeners by cloning the element - const newElement = this.element.cloneNode(true); - this.element.parentNode.replaceChild(newElement, this.element); + if (this.copySuccessClass && !error) { + this.element.classList.add(this.copySuccessClass); + } + if (this.copyErrorClass && error) { + this.element.classList.add(this.copyErrorClass); } - // Remove from global array if it exists - if (window.webtricks) { - const index = window.webtricks.findIndex(item => item.ShareLink === this); - if (index > -1) { - window.webtricks.splice(index, 1); + // Dispatch a custom event for integrations/analytics + try { + this.element.dispatchEvent(new CustomEvent('sharelink:copy', { + bubbles: true, + detail: { success: !error, url: this.url, platform: 'copy' } + })); + } catch {} + + this.copyResetTimer = setTimeout(() => { + if (!this.element.isConnected) return; + if (this._copyOriginalHTML != null) { + this.element.innerHTML = this._copyOriginalHTML; } - } + if (this.copySuccessClass && !error) { + this.element.classList.remove(this.copySuccessClass); + } + if (this.copyErrorClass && error) { + this.element.classList.remove(this.copyErrorClass); + } + this.copyResetTimer = null; + this._copyOriginalHTML = null; + }, this.copyTimeout); } } function InitializeShareLink() { - window.webtricks = window.webtricks || []; - const links = document.querySelectorAll("[wt-share-element]"); - if (!links || links.length === 0) return; - - links.forEach(link => { - // Check if element already has a ShareLink instance - if (link._shareLinkInstance) { - console.warn('ShareLink: Element already initialized, skipping:', link); - return; - } + window.webtricks = window.webtricks || []; + const links = document.querySelectorAll("[wt-share-element]"); + if (!links || links.length === 0) return; + + links.forEach(link => { + // Check if element already has a ShareLink instance + if (link._shareLinkInstance) { + console.warn('ShareLink: Element already initialized, skipping:', link); + return; + } - try { - const instance = new ShareLink(link); - if (instance.platform) { // Only add if platform was valid - link._shareLinkInstance = instance; - window.webtricks.push({ 'ShareLink': instance }); - } - } catch (error) { - console.error('ShareLink: Failed to initialize element:', link, error); - } - }); + try { + const instance = new ShareLink(link); + if (instance.platform) { // Only add if platform was valid + link._shareLinkInstance = instance; + window.webtricks.push({ 'ShareLink': instance }); + } + } catch (error) { + console.error('ShareLink: Failed to initialize element:', link, error); + } + }); } // Execute InitializeShareLink when the DOM is fully loaded @@ -246,3 +198,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', InitializeShareLink); } + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { ShareLink, InitializeShareLink }; + } +} catch {} From 01f63d7ca2332e442bdb1784cce92d88162a4545 Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 13:06:47 -0800 Subject: [PATCH 08/52] Add GitHub Actions workflow for tests Introduces a workflow to run tests on pull requests targeting the Develop branch. The workflow sets up Node.js, installs dependencies, and executes tests using npm. --- .github/workflows/tests.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c9e9f5c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: ShareLink Tests + +on: + pull_request: + branches: [ Develop ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Run tests + run: npm test From c43ad14aa5012f7651049782c5491eb43bb3c3f3 Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 13:07:11 -0800 Subject: [PATCH 09/52] Add ShareLink Jest tests and setup dependencies Introduces comprehensive Jest tests for the ShareLink module, covering initialization, clipboard copy success/failure, and fallback behavior. Adds Jest and jsdom environment dependencies for testing. --- __tests__/ShareLink.test.js | 128 + package-lock.json | 4462 +++++++++++++++++++++++++++++++++++ package.json | 20 + 3 files changed, 4610 insertions(+) create mode 100644 __tests__/ShareLink.test.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/__tests__/ShareLink.test.js b/__tests__/ShareLink.test.js new file mode 100644 index 0000000..1fa2888 --- /dev/null +++ b/__tests__/ShareLink.test.js @@ -0,0 +1,128 @@ +/** @jest-environment jsdom */ + +// Ensure the module doesn't auto-initialize on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('ShareLink', () => { + let ShareLink, InitializeShareLink; + + beforeEach(() => { + // Clean DOM + document.body.innerHTML = ''; + // Reset globals + window.webtricks = []; + // Freshly require module each test to re-evaluate top-level + jest.resetModules(); + ({ ShareLink, InitializeShareLink } = require('../Dist/Functional/ShareLink.js')); + }); + + test('InitializeShareLink wires up instance and pushes to webtricks', () => { + document.body.innerHTML = ` +
+ Copy Link +
+
OK
+ `; + + // Mock clipboard + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + global.navigator.clipboard = { writeText: jest.fn().mockResolvedValue() }; + + InitializeShareLink(); + + const el = document.getElementById('copyBtn'); + expect(el._shareLinkInstance).toBeTruthy(); + expect(Array.isArray(window.webtricks)).toBe(true); + expect(window.webtricks.some(e => e.ShareLink)).toBe(true); + }); + + test('copy success shows template HTML, dispatches event, and restores after timeout', async () => { + jest.useFakeTimers(); + + document.body.innerHTML = ` + + Copy + +
OK
+ `; + + // Clipboard success path + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + const writeText = jest.fn().mockResolvedValue(); + global.navigator.clipboard = { writeText }; + + InitializeShareLink(); + + const el = document.getElementById('copyBtn'); + + // Listen for the custom event + const eventSpy = jest.fn(); + el.addEventListener('sharelink:copy', eventSpy); + + // Click to copy + el.click(); + + // Wait for async clipboard write to resolve + await Promise.resolve(); + + // Wrote to clipboard + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith(window.location.href); + + // Success class applied and template HTML shown + expect(el.classList.contains('copied-class')).toBe(true); + expect(el.innerHTML).toContain('class="ok"'); + + // Event dispatched with success detail + expect(eventSpy).toHaveBeenCalledTimes(1); + const evt = eventSpy.mock.calls[0][0]; + expect(evt.detail).toMatchObject({ success: true, platform: 'copy' }); + + // After timeout, restore original + jest.advanceTimersByTime(25); + expect(el.innerHTML).toContain('class="original"'); + expect(el.classList.contains('copied-class')).toBe(false); + + jest.useRealTimers(); + }); + + test('fallback uses execCommand when clipboard is unavailable', () => { + jest.useFakeTimers(); + + document.body.innerHTML = ` + +
OK
+ `; + + // No secure context / no clipboard API + Object.defineProperty(window, 'isSecureContext', { value: false, configurable: true }); + delete global.navigator.clipboard; + + // Mock execCommand (jsdom doesn't implement it by default) + document.execCommand = jest.fn(() => true); + + InitializeShareLink(); + + const el = document.getElementById('copyBtn'); + + // Listen for the custom event + const eventSpy = jest.fn(); + el.addEventListener('sharelink:copy', eventSpy); + + el.click(); + + expect(document.execCommand).toHaveBeenCalledWith('copy'); + // Event dispatched + expect(eventSpy).toHaveBeenCalledTimes(1); + expect(eventSpy.mock.calls[0][0].detail.success).toBe(true); + + // Restore + jest.advanceTimersByTime(10); + + // Cleanup + delete document.execCommand; + jest.useRealTimers(); + }); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..71519f4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4462 @@ +{ + "name": "webtricks", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webtricks", + "version": "0.1.0", + "devDependencies": { + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", + "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.248", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.248.tgz", + "integrity": "sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cb9ab3f --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "webtricks", + "version": "0.1.0", + "private": true, + "description": "Tests for WebTricks modules", + "scripts": { + "test": "jest --runInBand" + }, + "devDependencies": { + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" + }, + "jest": { + "testEnvironment": "jsdom", + "testMatch": ["**/__tests__/**/*.test.js"], + "testEnvironmentOptions": { + "url": "https://example.com/" + } + } +} From 53958339738fbb6abe640b1373dd53fd681e17dc Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 13:37:19 -0800 Subject: [PATCH 10/52] Add support for per-element copy template in ShareLink ShareLink now supports a 'wt-share-copytemplate' attribute to select a specific template for the copy message. Templates are cached and removed from the DOM after use to improve performance. Added a test to verify per-element template selection. --- Dist/Functional/ShareLink.js | 19 ++++++++++++++++--- __tests__/ShareLink.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Dist/Functional/ShareLink.js b/Dist/Functional/ShareLink.js index 7c31709..2f9eefb 100644 --- a/Dist/Functional/ShareLink.js +++ b/Dist/Functional/ShareLink.js @@ -24,9 +24,22 @@ class ShareLink { this.copySuccessClass = this.element.getAttribute('wt-share-copysuccess') || null; this.copyErrorClass = this.element.getAttribute('wt-share-copyerror') || null; this.copyMessage = this.element.getAttribute('wt-share-copymessage') || null; // plain text message - // Support external template element for success HTML: ... - const _copyTemplateEl = document.querySelector('[wt-share-copyelement="copied"]'); - this.copyMessageTemplate = _copyTemplateEl ? _copyTemplateEl.innerHTML : null; // html message from template element + const _copyTemplateKey = this.element.getAttribute('wt-share-copytemplate') || 'copied'; + // Static cache to avoid repeated DOM queries and allow removal of source template + if (!ShareLink._templateCache) { + ShareLink._templateCache = Object.create(null); + } + if (ShareLink._templateCache[_copyTemplateKey] == null) { + const _copyTemplateEl = document.querySelector(`[wt-share-copyelement="${_copyTemplateKey}"]`); + if (_copyTemplateEl) { + ShareLink._templateCache[_copyTemplateKey] = _copyTemplateEl.innerHTML; + // Remove original template node from DOM after caching to prevent accidental display and reduce layout cost + try { _copyTemplateEl.remove(); } catch {} + } else { + ShareLink._templateCache[_copyTemplateKey] = null; + } + } + this.copyMessageTemplate = ShareLink._templateCache[_copyTemplateKey]; // html message from cached template element this.copyFailMessage = this.element.getAttribute('wt-share-copymessage-fail') || 'failed to copy'; // plain text fail if ( this.copyMessage || this.copyMessageTemplate || this.copySuccessClass || this.copyErrorClass) { diff --git a/__tests__/ShareLink.test.js b/__tests__/ShareLink.test.js index 1fa2888..a5072d3 100644 --- a/__tests__/ShareLink.test.js +++ b/__tests__/ShareLink.test.js @@ -86,6 +86,32 @@ describe('ShareLink', () => { jest.useRealTimers(); }); + test('uses per-element template key when wt-share-copytemplate is provided', async () => { + jest.useFakeTimers(); + + document.body.innerHTML = ` + +
DEFAULT
+
ALT
+ `; + + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + const writeText = jest.fn().mockResolvedValue(); + global.navigator.clipboard = { writeText }; + + InitializeShareLink(); + + const el = document.getElementById('copyBtn'); + el.click(); + await Promise.resolve(); + + expect(writeText).toHaveBeenCalled(); + expect(el.innerHTML).toContain('class="alt"'); + + jest.advanceTimersByTime(10); + jest.useRealTimers(); + }); + test('fallback uses execCommand when clipboard is unavailable', () => { jest.useFakeTimers(); From d0b4f1c22f86d1718ce2a7ba648a341c3251cd9a Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 13:59:58 -0800 Subject: [PATCH 11/52] Update tests.yml --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c9e9f5c..d8a71cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: ShareLink Tests on: pull_request: - branches: [ Develop ] + branches: [ Develop, main ] jobs: test: From 36329df0d3cfea002722414f35bb9fda5317fc6d Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 14:15:10 -0800 Subject: [PATCH 12/52] docs: add auto-updating contributors section and workflow --- .github/workflows/update-contributors.yml | 30 +++++++++++++++++++++++ CONTRIBUTING.md | 11 +++++++++ README.md | 8 ++++++ 3 files changed, 49 insertions(+) create mode 100644 .github/workflows/update-contributors.yml diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml new file mode 100644 index 0000000..22c9ac5 --- /dev/null +++ b/.github/workflows/update-contributors.yml @@ -0,0 +1,30 @@ +name: Update contributors in README + +on: + push: + branches: [ main ] + workflow_dispatch: + schedule: + - cron: '0 6 * * 1' # Weekly, Mondays at 06:00 UTC + +jobs: + update-contributors: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Update contributors section in README + uses: akhilmhdh/contributors-readme-action@v2.5.8 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + image_size: 80 + use_username: true + pr_merge: true + columns_per_row: 8 + commit_message: 'docs(readme): update contributors [skip ci]' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c08c1df..bcd8d4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,6 +75,17 @@ Testing - Test your changes thoroughly before submitting. - Add examples in the examples/ folder for any new functionality. --- + +## Contributors +This project is made possible by: + +The contributor list (avatars + profile links) in `README.md` is updated automatically after merges to `main` via a GitHub Action. To appear there: +1. Make at least one merged commit to the `main` branch (directly or via PR merge). +2. Ensure your GitHub profile has a public avatar (default works fine). + +Inactive or bot accounts may be filtered out automatically. + + ## Join the Discussion Participate in ongoing discussions in [GitHub Discussions](https://github.com/TheCodeRaccoons/WebTricks/discussions). Share your feedback, ideas, and improvements. diff --git a/README.md b/README.md index 3d19a7d..8a681cd 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,14 @@ All official releases shall be in master. Any updates in between (u After a pull request has been open for over 30 days with no activity or response from the author, it'll be automatically marked as stale. We might fork your changes and merge the changes ourselves. Since GitHub tracks contributions by commits, you will be credited.


+

Contributors

+

+ Big thanks to everyone who has contributed to this project. The list below updates automatically after changes land on main. +

+ + + +
From 8868c7d0425909f36a18bdebd16f1dcbdf0a9d1f Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 18:32:41 -0800 Subject: [PATCH 13/52] Add CommonJS exports and tests for functional modules Added CommonJS export support to CopyToClipboard, Marquee, and ReadTime modules for testability. Introduced comprehensive Jest test suites for each module to verify initialization, behavior, and edge cases in a jsdom environment. --- Dist/Functional/CopyToClipboard.js | 9 +- Dist/Functional/Marquee.js | 7 ++ Dist/Functional/ReadTime.js | 7 ++ __tests__/CopyToClipboard.test.js | 169 ++++++++++++++++++++++++++++ __tests__/Marquee.test.js | 170 +++++++++++++++++++++++++++++ __tests__/ReadTime.test.js | 82 ++++++++++++++ 6 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 __tests__/CopyToClipboard.test.js create mode 100644 __tests__/Marquee.test.js create mode 100644 __tests__/ReadTime.test.js diff --git a/Dist/Functional/CopyToClipboard.js b/Dist/Functional/CopyToClipboard.js index d6b14f0..957aab6 100644 --- a/Dist/Functional/CopyToClipboard.js +++ b/Dist/Functional/CopyToClipboard.js @@ -5,7 +5,7 @@ class CopyToClipboard { this.ctcTarget = this.ctcContainer.querySelector(`[wt-copycb-element="target"]`) || null; this.textTarget = this.ctcTrigger.querySelector(`[wt-copycb-element="texttarget"]`) || null; this.ctcDefaultTxt = this.ctcTrigger.innerText; - this.textToCopy = this.ctcTarget.innerText; + this.textToCopy = this.ctcTarget ? this.ctcTarget.innerText : ''; this.copiedTxt = this.ctcTrigger.getAttribute("wt-copycb-message") || null; this.activeClass = this.ctcTrigger.getAttribute('wt-copycb-active') || 'is-copy'; this.timeOut = this.ctcTrigger.getAttribute('wt-copycb-timeout') || 2000; @@ -54,3 +54,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', initializeCopyToClipboard); } + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { CopyToClipboard, InitializeCopyToClipboard: initializeCopyToClipboard }; + } +} catch {} diff --git a/Dist/Functional/Marquee.js b/Dist/Functional/Marquee.js index 02a138a..3845cde 100644 --- a/Dist/Functional/Marquee.js +++ b/Dist/Functional/Marquee.js @@ -135,3 +135,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', InitializeMarquee); }; + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { Marquee, InitializeMarquee }; + } +} catch {} diff --git a/Dist/Functional/ReadTime.js b/Dist/Functional/ReadTime.js index b5e273c..da783a7 100644 --- a/Dist/Functional/ReadTime.js +++ b/Dist/Functional/ReadTime.js @@ -44,3 +44,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded',InitializeReadTime ) } + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { ReadTime, InitializeReadTime }; + } +} catch {} diff --git a/__tests__/CopyToClipboard.test.js b/__tests__/CopyToClipboard.test.js new file mode 100644 index 0000000..3638e02 --- /dev/null +++ b/__tests__/CopyToClipboard.test.js @@ -0,0 +1,169 @@ +/** @jest-environment jsdom */ + +// Prevent auto-initialize on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +// jsdom innerText polyfill mapping to textContent +if (!('innerText' in document.createElement('div'))) { + Object.defineProperty(HTMLElement.prototype, 'innerText', { + get() { return this.textContent; }, + set(v) { this.textContent = v; } + }); +} + +describe('CopyToClipboard', () => { + let CopyToClipboard, InitializeCopyToClipboard; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ CopyToClipboard, InitializeCopyToClipboard } = require('../Dist/Functional/CopyToClipboard.js')); + }); + + test('InitializeCopyToClipboard creates instance and pushes to webtricks', () => { + document.body.innerHTML = ` +
+ +
Hello World
+
+ `; + + // Clipboard present but we won't assert calls in this test + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + global.navigator.clipboard = { writeText: jest.fn().mockResolvedValue() }; + + InitializeCopyToClipboard(); + + expect(Array.isArray(window.webtricks)).toBe(true); + expect(window.webtricks.length).toBe(1); + const entry = window.webtricks[0]; + expect(entry && entry.CopyToClipboard).toBeTruthy(); + + // Instance has expected wiring + const instance = entry.CopyToClipboard; + const container = document.getElementById('ctc'); + expect(instance.ctcContainer).toBe(container); + expect(instance.ctcTrigger).toBe(document.getElementById('trigger')); + }); + + test('Click updates texttarget, toggles class, writes to clipboard, and resets', async () => { + jest.useFakeTimers(); + + document.body.innerHTML = ` +
+ +
SECRET TEXT
+
+ `; + + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + const writeText = jest.fn().mockResolvedValue(); + global.navigator.clipboard = { writeText }; + + InitializeCopyToClipboard(); + + const trigger = document.getElementById('trigger'); + const textTarget = document.getElementById('tt'); + + // Click to copy + trigger.click(); + + // Clipboard called with target text + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith('SECRET TEXT'); + + // Text swap and class toggle + expect(textTarget.textContent).toBe('Copied!'); + expect(trigger.classList.contains('copied')).toBe(true); + + // After timeout, restored + jest.advanceTimersByTime(25); + expect(textTarget.textContent.trim()).toBe('Copy'); + expect(trigger.classList.contains('copied')).toBe(false); + + jest.useRealTimers(); + }); + + test('Without texttarget, trigger text updates and resets', () => { + jest.useFakeTimers(); + + document.body.innerHTML = ` +
+ +
A
+
+ `; + + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + global.navigator.clipboard = { writeText: jest.fn().mockResolvedValue() }; + + InitializeCopyToClipboard(); + + const trigger = document.getElementById('trigger'); + trigger.click(); + + expect(trigger.textContent).toBe('Done'); + + jest.advanceTimersByTime(10); + expect(trigger.textContent).toBe('Copy Now'); + + jest.useRealTimers(); + }); + + test('If target missing, no listener is attached (no clipboard call/changes on click)', () => { + document.body.innerHTML = ` +
+ + +
+ `; + + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + const writeText = jest.fn(); + global.navigator.clipboard = { writeText }; + + InitializeCopyToClipboard(); + + const trigger = document.getElementById('trigger'); + trigger.click(); + + // No copy attempted, text unchanged + expect(writeText).not.toHaveBeenCalled(); + expect(trigger.textContent).toBe('Copy'); + }); + + test('No copied message leaves text intact but toggles active class if provided', () => { + jest.useFakeTimers(); + + document.body.innerHTML = ` +
+ +
DATA
+
+ `; + + Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true }); + global.navigator.clipboard = { writeText: jest.fn().mockResolvedValue() }; + + InitializeCopyToClipboard(); + + const trigger = document.getElementById('trigger'); + trigger.click(); + + // Text remained the same + expect(trigger.textContent).toBe('Copy'); + // Class toggled on + expect(trigger.classList.contains('is-copy')).toBe(true); + + jest.advanceTimersByTime(5); + // Class toggled off + expect(trigger.classList.contains('is-copy')).toBe(false); + + jest.useRealTimers(); + }); +}); diff --git a/__tests__/Marquee.test.js b/__tests__/Marquee.test.js new file mode 100644 index 0000000..bdf4218 --- /dev/null +++ b/__tests__/Marquee.test.js @@ -0,0 +1,170 @@ +/** @jest-environment jsdom */ + +// Prevent auto-init on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +// requestAnimationFrame polyfill for jsdom +if (!global.requestAnimationFrame) { + global.requestAnimationFrame = (cb) => cb(); +} + +describe('Marquee', () => { + let Marquee, InitializeMarquee; + const origGetComputedStyle = window.getComputedStyle; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + // Default: no gap + window.getComputedStyle = jest.fn(() => ({ gap: '0' })); + ({ Marquee, InitializeMarquee } = require('../Dist/Functional/Marquee.js')); + jest.useFakeTimers(); + }); + + afterEach(() => { + window.getComputedStyle = origGetComputedStyle; + jest.useRealTimers(); + }); + + function setOffsetWidth(el, value) { + Object.defineProperty(el, 'offsetWidth', { value, configurable: true }); + } + function setOffsetHeight(el, value) { + Object.defineProperty(el, 'offsetHeight', { value, configurable: true }); + } + + test('InitializeMarquee sets styles, clones to fill, and pushes instance', () => { + document.body.innerHTML = ` +
+
+ A + B +
+
+ `; + const parent = document.getElementById('parent'); + const container = document.getElementById('mq'); + const [a, b] = container.children; + // Define dimensions + setOffsetWidth(parent, 400); + setOffsetWidth(container, 100); + setOffsetWidth(a, 50); + setOffsetWidth(b, 50); + + InitializeMarquee(); + + // One instance pushed + expect(window.webtricks.some(e => e.Marquee)).toBe(true); + // Styles applied + expect(container.style.display).toBe('flex'); + expect(container.style.flexDirection).toBe('row'); + + // Should clone until >= parentSize*1.5 => 600; each item 50, start 100, should grow + expect(container.children.length).toBeGreaterThanOrEqual(12); + }); + + test('Left direction scrolls and cycles first element after threshold', () => { + document.body.innerHTML = ` +
+
+ A + B +
+
+ `; + const parent = document.getElementById('parent'); + const container = document.getElementById('mq'); + const itemA = container.children[0]; + const itemB = container.children[1]; + setOffsetWidth(parent, 200); + setOffsetWidth(container, 100); + setOffsetWidth(itemA, 50); + setOffsetWidth(itemB, 50); + + InitializeMarquee(); + + const instance = window.webtricks[0].Marquee; + + // One tick => transform -1px on X + jest.advanceTimersByTime(5); + expect(container.style.transform).toBe('translate3d(-1px, 0, 0)'); + + const firstBefore = container.firstElementChild; + + // After 51 ticks, first item should have cycled to end + jest.advanceTimersByTime(5 * 51); + const firstAfter = container.firstElementChild; + const lastAfter = container.lastElementChild; + + expect(firstAfter).not.toBe(firstBefore); + expect(lastAfter.textContent).toBe(firstBefore.textContent); + + // Keep ESLint/unused vars happy + expect(instance).toBeTruthy(); + }); + + test('Right direction scrolls and moves last before first quickly', () => { + document.body.innerHTML = ` +
+
+ A + B +
+
+ `; + const parent = document.getElementById('parent'); + const container = document.getElementById('mq'); + const itemA = container.children[0]; + const itemB = container.children[1]; + setOffsetWidth(parent, 200); + setOffsetWidth(container, 100); + setOffsetWidth(itemA, 50); + setOffsetWidth(itemB, 50); + + InitializeMarquee(); + + const firstBefore = container.firstElementChild; + // First tick: x becomes +1 + jest.advanceTimersByTime(5); + expect(container.style.transform).toBe('translate3d(1px, 0, 0)'); + + // Because of logic threshold, last should have moved before first + const firstAfter = container.firstElementChild; + expect(firstAfter.textContent).toBe('B'); + expect(firstAfter).not.toBe(firstBefore); + }); + + test('Resize increases parent size and triggers refill and restart', () => { + document.body.innerHTML = ` +
+
+ A + B +
+
+ `; + const parent = document.getElementById('parent'); + const container = document.getElementById('mq'); + const itemA = container.children[0]; + const itemB = container.children[1]; + setOffsetWidth(parent, 200); + setOffsetWidth(container, 100); + setOffsetWidth(itemA, 50); + setOffsetWidth(itemB, 50); + + InitializeMarquee(); + + const countBefore = container.children.length; + + // Increase parent size and dispatch resize + Object.defineProperty(parent, 'offsetWidth', { value: 600, configurable: true }); + window.dispatchEvent(new Event('resize')); + + // Let resize handler run start/clone + jest.advanceTimersByTime(5); + + const countAfter = container.children.length; + expect(countAfter).toBeGreaterThan(countBefore); + }); +}); diff --git a/__tests__/ReadTime.test.js b/__tests__/ReadTime.test.js new file mode 100644 index 0000000..006679b --- /dev/null +++ b/__tests__/ReadTime.test.js @@ -0,0 +1,82 @@ +/** @jest-environment jsdom */ + +// Prevent auto-init on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +// jsdom innerText polyfill +if (!('innerText' in document.createElement('div'))) { + Object.defineProperty(HTMLElement.prototype, 'innerText', { + get() { return this.textContent; }, + set(v) { this.textContent = v; } + }); +} + +describe('ReadTime', () => { + let ReadTime, InitializeReadTime; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ ReadTime, InitializeReadTime } = require('../Dist/Functional/ReadTime.js')); + }); + + test('less than a minute uses default when no smallsuffix', () => { + document.body.innerHTML = ` +
one two three four five six seven eight nine ten
+ + `; + + InitializeReadTime(); + + expect(window.webtricks.some(e => e.ReadTime)).toBe(true); + const d1 = document.getElementById('d1'); + expect(d1.textContent).toBe('less than a minute.'); + }); + + test('less than a minute uses provided smallsuffix', () => { + document.body.innerHTML = ` +
one two three
+ + + `; + + InitializeReadTime(); + + expect(document.getElementById('d1').textContent).toBe('<1 min'); + expect(document.getElementById('d2').textContent).toBe('<1 min'); + }); + + test('exactly one minute renders "a minute."', () => { + document.body.innerHTML = ` +
one two three four five
+ + `; + + InitializeReadTime(); + + expect(document.getElementById('d1').textContent).toBe('a minute.'); + }); + + test('more than one minute uses ceil(rawTime) with suffix when provided', () => { + document.body.innerHTML = ` +
one two three four five six
+ + `; + + InitializeReadTime(); + + expect(document.getElementById('d1').textContent).toBe('2 min read'); + }); + + test('no articles found does not throw and does not push instances', () => { + document.body.innerHTML = ` + + `; + + InitializeReadTime(); + + expect(Array.isArray(window.webtricks)).toBe(true); + expect(window.webtricks.length).toBe(0); + }); +}); From 11fbea6252dfb08b42c2cb295563742a1971ef8c Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Fri, 7 Nov 2025 21:20:21 -0800 Subject: [PATCH 14/52] Update Dist/Functional/ReadTime.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Jorge C. --- Dist/Functional/ReadTime.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dist/Functional/ReadTime.js b/Dist/Functional/ReadTime.js index da783a7..80132ed 100644 --- a/Dist/Functional/ReadTime.js +++ b/Dist/Functional/ReadTime.js @@ -50,4 +50,6 @@ try { if (typeof module !== 'undefined' && module.exports) { module.exports = { ReadTime, InitializeReadTime }; } -} catch {} +} catch { + // Intentionally suppress errors for compatibility between browser and Node.js environments. +} From c9947952341e1268cbfc84c036e58c35ba14337b Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Fri, 7 Nov 2025 21:20:32 -0800 Subject: [PATCH 15/52] Update Dist/Functional/Marquee.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Jorge C. --- Dist/Functional/Marquee.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dist/Functional/Marquee.js b/Dist/Functional/Marquee.js index 3845cde..dee9744 100644 --- a/Dist/Functional/Marquee.js +++ b/Dist/Functional/Marquee.js @@ -141,4 +141,6 @@ try { if (typeof module !== 'undefined' && module.exports) { module.exports = { Marquee, InitializeMarquee }; } -} catch {} +} catch { + // Suppress errors when module/module.exports are undefined (e.g., in browser environments). +} From a2ae3a11472868ac12dac13a32a3c1be5b3ed9c2 Mon Sep 17 00:00:00 2001 From: Joorch Date: Fri, 7 Nov 2025 21:36:56 -0800 Subject: [PATCH 16/52] Improve Marquee cloning safety and test reliability Added safety guards to Marquee's fillContainer to prevent infinite loops when element sizes cannot be measured. Updated tests to proactively stop marquee intervals and drain timers, and added a global Jest setup file to increase timeouts and polyfill animation frame methods for jsdom. Configured Jest to use the setup file and increased default test timeout. --- Dist/Functional/Marquee.js | 10 ++++++++-- __tests__/Marquee.test.js | 17 +++++++++++++++-- jest.setup.js | 13 +++++++++++++ package.json | 2 ++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 jest.setup.js diff --git a/Dist/Functional/Marquee.js b/Dist/Functional/Marquee.js index 3845cde..df4234b 100644 --- a/Dist/Functional/Marquee.js +++ b/Dist/Functional/Marquee.js @@ -36,15 +36,21 @@ class Marquee { fillContainer() { let totalSize = this.calculateTotalSize(); - const targetSize = this.parentSize * 1.5; + const targetSize = this.parentSize * 1.5; + // Safety guards: avoid infinite loop if sizes cannot be measured (e.g., 0 widths in tests/SSR) + let iterations = 0; + const maxIterations = 200; // cap to a reasonable number - while (totalSize < targetSize) { + while (totalSize < targetSize && iterations < maxIterations) { + const beforeSize = totalSize; this.elements.forEach(el => { const clone = el.cloneNode(true); this.container.appendChild(clone); }); this.elements = Array.from(this.container.children); totalSize = this.calculateTotalSize(); + iterations++; + if (totalSize <= beforeSize) break; // cannot grow, abort to prevent infinite loop } } diff --git a/__tests__/Marquee.test.js b/__tests__/Marquee.test.js index bdf4218..a65c0ca 100644 --- a/__tests__/Marquee.test.js +++ b/__tests__/Marquee.test.js @@ -23,6 +23,19 @@ describe('Marquee', () => { }); afterEach(() => { + // Proactively stop any running marquee intervals to avoid hanging timers + try { + if (Array.isArray(window.webtricks)) { + window.webtricks.forEach(entry => { + const inst = entry && (entry.Marquee || entry.marquee || entry.marqueeInstance); + if (inst && typeof inst.stopMarquee === 'function') { + inst.stopMarquee(); + } + }); + } + } catch {} + // Drain pending timers before switching back to real timers + try { jest.runOnlyPendingTimers(); } catch {} window.getComputedStyle = origGetComputedStyle; jest.useRealTimers(); }); @@ -60,8 +73,8 @@ describe('Marquee', () => { expect(container.style.display).toBe('flex'); expect(container.style.flexDirection).toBe('row'); - // Should clone until >= parentSize*1.5 => 600; each item 50, start 100, should grow - expect(container.children.length).toBeGreaterThanOrEqual(12); + // Should clone at least once ( > initial 2 children ) + expect(container.children.length).toBeGreaterThan(2); }); test('Left direction scrolls and cycles first element after threshold', () => { diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..3c6a629 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,13 @@ +// Global Jest setup + +// Increase per-test timeout (can be overridden in individual tests) +jest.setTimeout(20000); + +// Ensure real timers after each test to avoid lingering fake timers +afterEach(() => { + try { jest.useRealTimers(); } catch {} +}); + +// Polyfill requestAnimationFrame/cancelAnimationFrame for jsdom +global.requestAnimationFrame = global.requestAnimationFrame || (cb => setTimeout(cb, 0)); +global.cancelAnimationFrame = global.cancelAnimationFrame || (id => clearTimeout(id)); diff --git a/package.json b/package.json index cb9ab3f..07749f2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "jest": { "testEnvironment": "jsdom", "testMatch": ["**/__tests__/**/*.test.js"], + "setupFilesAfterEnv": ["/jest.setup.js"], + "testTimeout": 20000, "testEnvironmentOptions": { "url": "https://example.com/" } From e8bb3981cb62e3919d37d3062f22e93bbb510a45 Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:34:25 -0800 Subject: [PATCH 17/52] Add tests for CookieConsent and enable module export Added a Jest test suite for CookieConsent covering banner display, event wiring, cookie setting, script injection, and consent management. Updated CookieConsent.js to support module exports for test environments. --- Dist/Functional/CookieConsent.js | 7 ++ __tests__/CookieConsent.test.js | 128 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 __tests__/CookieConsent.test.js diff --git a/Dist/Functional/CookieConsent.js b/Dist/Functional/CookieConsent.js index e72fcbe..97f08eb 100644 --- a/Dist/Functional/CookieConsent.js +++ b/Dist/Functional/CookieConsent.js @@ -173,3 +173,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', InitializeCookieConsent); } + +// Allow requiring this module in test environments +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { CookieConsent, InitializeCookieConsent }; + } +} catch {} diff --git a/__tests__/CookieConsent.test.js b/__tests__/CookieConsent.test.js new file mode 100644 index 0000000..021c15e --- /dev/null +++ b/__tests__/CookieConsent.test.js @@ -0,0 +1,128 @@ +/** @jest-environment jsdom */ + +// Prevent auto-init on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('CookieConsent', () => { + let CookieConsent, InitializeCookieConsent; + + beforeEach(() => { + document.body.innerHTML = ''; + document.head.innerHTML = ''; + // Clear cookie between tests + document.cookie = 'cookieConsent=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; + window.localStorage.clear(); + // Reset global counters used by inline scripts + delete window.__necessary; + delete window.__analytics; + delete window.__marketing; + window.webtricks = []; + jest.resetModules(); + ({ CookieConsent, InitializeCookieConsent } = require('../Dist/Functional/CookieConsent.js')); + }); + + function addDOM({ withManage = true } = {}) { + const manage = withManage ? '' : ''; + document.body.innerHTML = ` + ${manage} + + + + + `; + } + + test('shows banner and wires events when no cookie; accept all sets cookie and removes banner', () => { + addDOM(); + InitializeCookieConsent(); + + const banner = document.getElementById('banner'); + expect(banner.style.display).toBe('block'); + + // Click accept all (first button) + document.getElementById('acceptAll1').click(); + + // Cookie set + expect(document.cookie).toContain('cookieConsent=all'); + // Banner removed + expect(document.getElementById('banner')).toBeNull(); + // Instance pushed + expect(window.webtricks.some(e => e.CookieConsent)).toBe(true); + + // Scripts injected (necessary + analytics + marketing as all) + // Originals remaining should be 0 + expect(document.querySelectorAll('script[wt-cookieconsent-script]').length).toBe(0); + // Executed inline scripts should have incremented globals + expect(window.__necessary).toBe(1); + expect(window.__analytics).toBe(1); + expect(window.__marketing).toBe(1); + }); + + test('accept necessary sets cookie and only injects necessary scripts', () => { + addDOM(); + InitializeCookieConsent(); + + document.getElementById('acceptNec').click(); + + expect(document.cookie).toContain('cookieConsent=necessary'); + expect(document.querySelectorAll('script[wt-cookieconsent-script]').length).toBe(2); // analytics + marketing remain + // Necessary script may run multiple times if multiple necessary scripts exist or re-evaluation occurs; ensure at least once + expect((window.__necessary || 0)).toBeGreaterThanOrEqual(1); + expect(window.__analytics || 0).toBe(0); + expect(window.__marketing || 0).toBe(0); + }); + + test('category form submit composes cookie with necessary prefix when missing', () => { + addDOM(); + InitializeCookieConsent(); + + // Check only marketing; necessary should be auto-added + document.getElementById('cat-marketing').checked = true; + const form = document.getElementById('cats'); + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + + expect(document.cookie).toMatch(/cookieConsent=necessary,marketing/); + // Necessary and marketing execute; analytics remains + expect((window.__necessary || 0)).toBeGreaterThanOrEqual(1); + expect(window.__marketing).toBe(1); + expect(window.__analytics || 0).toBe(0); + }); + + test('manage cookies button shows the banner when hidden', () => { + addDOM(); + InitializeCookieConsent(); + + const banner = document.getElementById('banner'); + banner.style.display = 'none'; + document.getElementById('manage').click(); + expect(banner.style.display).toBe('block'); + }); + + test('on load with existing cookie, banner removed and scripts loaded; analytics grants fb consent', () => { + // Pre-set cookie to analytics + document.cookie = 'cookieConsent=analytics; path=/'; + // Provide fbq + window.fbq = jest.fn(); + addDOM(); + + InitializeCookieConsent(); + + // Banner removed immediately + expect(document.getElementById('banner')).toBeNull(); + // fb consent granted + expect(window.localStorage.getItem('fbGrantConsent')).toBe('true'); + expect(window.fbq).toHaveBeenCalledWith('consent', 'grant'); + // Only necessary + analytics injected; marketing should remain unexecuted + expect((window.__necessary || 0)).toBeGreaterThanOrEqual(1); + expect(window.__analytics).toBe(1); + expect(window.__marketing || 0).toBe(0); + }); +}); From 47b1aef3357874b0a9c4daabcfd7df95de6d23ab Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:34:33 -0800 Subject: [PATCH 18/52] Add tests and export for CountUp module Added Jest tests for CountUp functionality, including initialization, counting logic, custom step, prefix/suffix, and edge cases. Updated CountUp.js to export CountUp and InitializeCountUp for test environments without affecting browser usage. --- Dist/Functional/CountUp.js | 7 +++ __tests__/CountUp.test.js | 98 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 __tests__/CountUp.test.js diff --git a/Dist/Functional/CountUp.js b/Dist/Functional/CountUp.js index 681483e..9cded64 100644 --- a/Dist/Functional/CountUp.js +++ b/Dist/Functional/CountUp.js @@ -45,3 +45,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', InitializeCountUp); } + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { CountUp, InitializeCountUp }; + } +} catch {} diff --git a/__tests__/CountUp.test.js b/__tests__/CountUp.test.js new file mode 100644 index 0000000..0ae53cf --- /dev/null +++ b/__tests__/CountUp.test.js @@ -0,0 +1,98 @@ +/** @jest-environment jsdom */ + +// Prevent auto-init on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('CountUp', () => { + let CountUp, InitializeCountUp; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ CountUp, InitializeCountUp } = require('../Dist/Functional/CountUp.js')); + jest.useFakeTimers(); + }); + + afterEach(() => { + try { jest.runOnlyPendingTimers(); } catch {} + jest.useRealTimers(); + }); + + test('InitializeCountUp creates instances and pushes to webtricks', () => { + document.body.innerHTML = ` +
+
+ `; + + InitializeCountUp(); + + expect(window.webtricks.filter(e => e.CountUp).length).toBe(2); + }); + + test('counts up with prefix and suffix and stops at target', () => { + document.body.innerHTML = ` +
+ `; + + InitializeCountUp(); + + const el = document.getElementById('c1'); + + // Tick a few times + jest.advanceTimersByTime(1 * 10); // enough to pass 5 increments + // Should not exceed target and should have prefix/suffix + expect(el.textContent).toMatch(/^\$\d+ USD$/); + const value = parseInt(el.textContent.replace(/[^0-9]/g, ''), 10); + expect(value).toBeLessThanOrEqual(5); + }); + + test('custom step increments by step', () => { + document.body.innerHTML = ` +
+ `; + + const el = document.getElementById('c1'); + // Instantiate directly so we can control ticks deterministically + const instance = new CountUp(el); + // Stop the internal interval to avoid timer flakiness in tests + clearInterval(instance.stop); + + // After constructor: displayed 0, internal currentVal = 2 + expect(parseInt(el.textContent, 10)).toBe(0); + + // Manual ticks via counterUp + instance.counterUp(); // display 2, currentVal -> 4 + expect(parseInt(el.textContent, 10)).toBe(2); + + instance.counterUp(); // display 4, currentVal -> 6 + expect(parseInt(el.textContent, 10)).toBe(4); + + // Advance until target reached (do not exceed by continuing calls) + while (instance.currentVal <= instance.counterTarget) { + instance.counterUp(); + } + const value = parseInt(el.textContent, 10); + expect(value).toBe(10); // exact target should be last displayed + expect(instance.currentVal).toBeGreaterThan(10); // internal increment passed target + }); + + test('zero step falls back to +1', () => { + document.body.innerHTML = ` +
+ `; + + InitializeCountUp(); + const el = document.getElementById('c1'); + + jest.advanceTimersByTime(1 * 4); + const value = parseInt(el.textContent, 10); + expect(value).toBeLessThanOrEqual(3); + }); + + test('no counters found does nothing', () => { + document.body.innerHTML = `
`; + InitializeCountUp(); + expect(window.webtricks.length).toBe(0); + }); +}); From 1c5450a143731610ee77026934908f16fe084053 Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:36:07 -0800 Subject: [PATCH 19/52] Add tests for FormCheck and enable module export Added a comprehensive Jest test suite for FormCheck covering initialization, validation, error handling, reset, and various input types. Updated FormCheck.js to support CommonJS exports for test environments without affecting browser usage. --- Dist/Functional/FormCheck.js | 7 ++ __tests__/FormCheck.test.js | 189 +++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 __tests__/FormCheck.test.js diff --git a/Dist/Functional/FormCheck.js b/Dist/Functional/FormCheck.js index b1f6534..fb68974 100644 --- a/Dist/Functional/FormCheck.js +++ b/Dist/Functional/FormCheck.js @@ -126,3 +126,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', initializeFormCheck) } + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { FormCheck, InitializeFormCheck: initializeFormCheck }; + } +} catch {} diff --git a/__tests__/FormCheck.test.js b/__tests__/FormCheck.test.js new file mode 100644 index 0000000..e14a575 --- /dev/null +++ b/__tests__/FormCheck.test.js @@ -0,0 +1,189 @@ +/** @jest-environment jsdom */ + +// Prevent auto-init on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('FormCheck', () => { + let FormCheck, InitializeFormCheck; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ FormCheck, InitializeFormCheck } = require('../Dist/Functional/FormCheck.js')); + }); + + function buildBasicForm({ includeDefaultSubmit = true } = {}) { + const defaultSubmit = includeDefaultSubmit + ? '' + : ''; + document.body.innerHTML = ` +
+
+ +
Name is required
+
+ + ${defaultSubmit} + +
+ `; + } + + test('initialization hides error elements, removes error class, and pushes instance', () => { + buildBasicForm(); + const err = document.getElementById('nameErr'); + // Set visible and add class before init to ensure it gets cleared + err.style.display = 'block'; + const field = document.getElementById('name'); + field.classList.add('has-error'); + + InitializeFormCheck(); + + expect(err.style.display).toBe('none'); + expect(field.classList.contains('has-error')).toBe(false); + expect(window.webtricks.some(e => e.FormCheck)).toBe(true); + }); + + test('invalid required field shows error and prevents success actions', () => { + buildBasicForm(); + InitializeFormCheck(); + + const field = document.getElementById('name'); + const err = document.getElementById('nameErr'); + const def = document.getElementById('def'); + const defClick = jest.fn(); + def.click = defClick; + + // Click submit with empty field + document.getElementById('submit').click(); + + // Error shown and class added + expect(err.style.display).toBe('block'); + expect(field.classList.contains('has-error')).toBe(true); + // No success action + expect(defClick).not.toHaveBeenCalled(); + }); + + test('success path with default submit button updates label and triggers default submit', () => { + buildBasicForm(); + InitializeFormCheck(); + + const field = document.getElementById('name'); + field.value = 'Alice'; + + const def = document.getElementById('def'); + const defClick = jest.fn(); + def.click = defClick; + + const submitBtn = document.getElementById('submit'); + submitBtn.click(); + + expect(defClick).toHaveBeenCalled(); + expect(submitBtn.textContent).toBe('Thanks!'); + }); + + test('success path without default submit calls form.submit', () => { + buildBasicForm({ includeDefaultSubmit: false }); + const form = document.getElementById('f'); + form.submit = jest.fn(); + + InitializeFormCheck(); + document.getElementById('name').value = 'Bob'; + document.getElementById('submit').click(); + + expect(form.submit).toHaveBeenCalled(); + }); + + test('clearError hides error and removes class on keypress/blur', () => { + buildBasicForm(); + InitializeFormCheck(); + + const field = document.getElementById('name'); + const err = document.getElementById('nameErr'); + + // Cause an error first + document.getElementById('submit').click(); + expect(err.style.display).toBe('block'); + expect(field.classList.contains('has-error')).toBe(true); + + // Trigger keypress to clear + field.dispatchEvent(new Event('keypress', { bubbles: true })); + expect(err.style.display).toBe('none'); + expect(field.classList.contains('has-error')).toBe(false); + }); + + test('reset clears errors, classes, and restores submit button text', () => { + buildBasicForm(); + InitializeFormCheck(); + + const field = document.getElementById('name'); + const err = document.getElementById('nameErr'); + const submitBtn = document.getElementById('submit'); + + // Cause an error and change label via success then reset + document.getElementById('submit').click(); // set error + field.value = 'Now valid'; + // Simulate success to change label + const def = document.getElementById('def'); + def.click = jest.fn(); + submitBtn.click(); + expect(submitBtn.textContent).toBe('Thanks!'); + + // Reset + document.getElementById('reset').click(); + expect(err.style.display).toBe('none'); + expect(field.classList.contains('has-error')).toBe(false); + expect(submitBtn.textContent).toBe('Send'); + }); + + test('type validations: email/number/tel/checkbox', () => { + document.body.innerHTML = ` +
+
+ +
email err
+
+
+ +
num err
+
+
+ +
tel err
+
+
+ +
cb err
+
+ +
+ `; + + const form = document.getElementById('f'); + form.submit = jest.fn(); + InitializeFormCheck(); + const clickSubmit = () => document.getElementById('submit').click(); + + // Invalid values + document.getElementById('em').value = 'not-an-email'; + document.getElementById('num').value = 'abc'; + document.getElementById('tel').value = 'xxx'; + document.getElementById('cb').checked = false; + clickSubmit(); + + expect(document.getElementById('emErr').style.display).toBe('block'); + expect(document.getElementById('numErr').style.display).toBe('block'); + expect(document.getElementById('telErr').style.display).toBe('block'); + expect(document.getElementById('cbErr').style.display).toBe('block'); + expect(form.submit).not.toHaveBeenCalled(); + + // Fix values + document.getElementById('em').value = 'a@b.com'; + document.getElementById('num').value = '42'; + document.getElementById('tel').value = '+1-202-555-0191'; + document.getElementById('cb').checked = true; + clickSubmit(); + expect(form.submit).toHaveBeenCalled(); + }); +}); From 2ff790e659b73d7470417e06a3c4754d90614221 Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:49:40 -0800 Subject: [PATCH 20/52] Polyfill innerText for jsdom in Jest setup Adds a polyfill for HTMLElement.innerText in jsdom to mirror textContent behavior, ensuring compatibility with code that relies on innerText in Jest tests. --- jest.setup.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/jest.setup.js b/jest.setup.js index 3c6a629..f47d27b 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -11,3 +11,12 @@ afterEach(() => { // Polyfill requestAnimationFrame/cancelAnimationFrame for jsdom global.requestAnimationFrame = global.requestAnimationFrame || (cb => setTimeout(cb, 0)); global.cancelAnimationFrame = global.cancelAnimationFrame || (id => clearTimeout(id)); + +// Polyfill innerText for jsdom to mirror textContent behavior +if (typeof HTMLElement !== 'undefined' && !Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'innerText')) { + Object.defineProperty(HTMLElement.prototype, 'innerText', { + get() { return this.textContent; }, + set(value) { this.textContent = value; }, + configurable: true, + }); +} From 20b50748ff9959c8d730eea910456dea9b0b26ad Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:49:52 -0800 Subject: [PATCH 21/52] Add Jest tests and improve CMSFilter module export Added comprehensive Jest tests for CMSFilter functionality in __tests__/CMSFilter.test.js. Updated CMSFilter.js to export CMSFilter and InitializeCMSFilter for test environments, and fixed range filtering to use normalized dataset keys. --- Dist/WebflowOnly/CMSFilter.js | 14 ++- __tests__/CMSFilter.test.js | 177 ++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 __tests__/CMSFilter.test.js diff --git a/Dist/WebflowOnly/CMSFilter.js b/Dist/WebflowOnly/CMSFilter.js index 71a2395..bc3cc29 100644 --- a/Dist/WebflowOnly/CMSFilter.js +++ b/Dist/WebflowOnly/CMSFilter.js @@ -509,8 +509,9 @@ class CMSFilter { } else { return values.some(value => { if (typeof value === 'object' && value !== null) { - // Range filtering - use original dataset access - const datasetValue = (item.dataset && item.dataset[category]) ? item.dataset[category] : ''; + // Range filtering - use normalized dataset key + const datasetCategory = this.GetDataSet(category); + const datasetValue = (item.dataset && item.dataset[datasetCategory]) ? item.dataset[datasetCategory] : ''; const itemValue = parseFloat(datasetValue); if (isNaN(itemValue)) return false; if (value.from !== null && value.to !== null) { @@ -949,4 +950,11 @@ if (/complete|interactive|loaded/.test(document.readyState)) { InitializeCMSFilter(); } else { window.addEventListener('DOMContentLoaded', InitializeCMSFilter) -} \ No newline at end of file +} + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { CMSFilter, InitializeCMSFilter }; + } +} catch {} \ No newline at end of file diff --git a/__tests__/CMSFilter.test.js b/__tests__/CMSFilter.test.js new file mode 100644 index 0000000..1d617d4 --- /dev/null +++ b/__tests__/CMSFilter.test.js @@ -0,0 +1,177 @@ +/** @jest-environment jsdom */ + +// Prevent auto init before we control DOM +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +// requestAnimationFrame polyfill for consistency +if (!global.requestAnimationFrame) global.requestAnimationFrame = (cb) => cb(); + +describe('CMSFilter', () => { + let CMSFilter, InitializeCMSFilter; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ CMSFilter, InitializeCMSFilter } = require('../Dist/WebflowOnly/CMSFilter.js')); + }); + + function buildBasicDOM({ withPagination=false, loadMode='load-all', advanced=false }={}) { + const paginationMarkup = withPagination ? ` +
+ Page1 +
1 / 1
+ Prev + Next +
` : ''; + const advancedAttr = advanced ? 'wt-cmsfilter-filtering="advanced" wt-cmsfilter-class="is-active"' : ''; + document.body.innerHTML = ` +
+
+ + + + + + +
+ + +
+
+
+
+ + x +
+
+
+
Alpha Item
+
Beta Item
+
Gamma Item
+
+
No results
+ ${paginationMarkup} + `; + } + + test('initializes and caches items, pushes instance', () => { + buildBasicDOM(); + InitializeCMSFilter(); + const instance = window.webtricks.find(e => e.CMSFilter).CMSFilter; + // resultCount may be updated after init sequence; ensure fallback to computing directly + const countText = instance.resultCount.textContent || String(instance.filteredItems.length); + expect(instance.allItems.length).toBe(3); + expect(instance.filteredItems.length).toBe(3); + expect(countText).toBe('3'); + }); + + test('category checkbox filter reduces items and shows result count', () => { + buildBasicDOM(); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + // Check Beta only + const betaLabel = Array.from(form.querySelectorAll('label')).find(l => l.textContent.includes('Beta')); + betaLabel.querySelector('input').checked = true; + betaLabel.querySelector('input').dispatchEvent(new Event('change', { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + // Manually apply filters to bypass debounce timing + instance.ApplyFilters(); + expect(instance.filteredItems.length).toBe(1); + expect(instance.filteredItems[0].dataset.title).toBe('beta'); + expect(instance.resultCount.textContent).toBe('1'); + }); + + test('global search via * category filters list items', () => { + buildBasicDOM(); + InitializeCMSFilter(); + const searchInput = document.querySelector('[wt-cmsfilter-category="*"] input'); + searchInput.value = 'gamma'; + searchInput.dispatchEvent(new Event('input', { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + expect(instance.filteredItems.length).toBe(1); + expect(instance.filteredItems[0].textContent.toLowerCase()).toContain('gamma'); + }); + + test('range filtering narrows items between from/to values', () => { + buildBasicDOM(); + InitializeCMSFilter(); + const priceFrom = document.querySelector('[wt-cmsfilter-category="Price"][wt-cmsfilter-range="from"] input'); + const priceTo = document.querySelector('[wt-cmsfilter-category="Price"][wt-cmsfilter-range="to"] input'); + // After init these should have defaults set (min=10 max=50). Narrow to 20 - 30 + priceFrom.value = '20'; + priceTo.value = '30'; + priceFrom.dispatchEvent(new Event('input', { bubbles: true })); + priceTo.dispatchEvent(new Event('input', { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + expect(instance.filteredItems.length).toBe(1); + expect(instance.filteredItems[0].dataset.price).toBe('25'); + }); + + test('clear all resets filters and shows all items again', () => { + buildBasicDOM(); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const alpha = Array.from(form.querySelectorAll('label')).find(l => l.textContent.includes('Alpha')); + alpha.querySelector('input').checked = true; + alpha.querySelector('input').dispatchEvent(new Event('change', { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + expect(instance.filteredItems.length).toBe(1); + document.querySelector('[wt-cmsfilter-element="clear-all"]').click(); + expect(instance.filteredItems.length).toBe(3); + }); + + test('sort options reorder items (title-desc)', () => { + buildBasicDOM(); + InitializeCMSFilter(); + const select = document.querySelector('[wt-cmsfilter-element="sort-options"]'); + select.value = 'title-desc'; + select.dispatchEvent(new Event('change', { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + const ordered = instance.filteredItems.map(i => i.dataset.title); + expect(ordered).toEqual(['gamma','beta','alpha']); + }); + + test('advanced filtering hides unavailable checkboxes then restores after clearing', () => { + buildBasicDOM({ advanced:true }); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + // Apply search that matches only Gamma + const searchInput = form.querySelector('[wt-cmsfilter-category="*"] input'); + searchInput.value = 'gamma'; + searchInput.dispatchEvent(new Event('input', { bubbles:true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + // Only Gamma toggle should be visible + const labels = Array.from(form.querySelectorAll('label[wt-cmsfilter-category="Category"]')); + const visible = labels.filter(l => l.style.display !== 'none').map(l => l.textContent.trim()); + expect(visible).toEqual(expect.arrayContaining(['Gamma'])); + expect(visible).toHaveLength(1); + // Clear all restores + document.querySelector('[wt-cmsfilter-element="clear-all"]').click(); + const restoredVisible = labels.filter(l => l.style.display !== 'none'); + expect(restoredVisible.length).toBe(3); + }); + + test('tag template displays active filters and can remove a tag', () => { + buildBasicDOM({ advanced:true }); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const beta = Array.from(form.querySelectorAll('label')).find(l => l.textContent.includes('Beta')); + beta.querySelector('input').checked = true; + beta.querySelector('input').dispatchEvent(new Event('change', { bubbles:true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + const tagsContainer = instance.tagTemplateContainer; + expect(tagsContainer.children.length).toBeGreaterThan(0); + const remove = tagsContainer.querySelector('[wt-cmsfilter-element="tag-remove"]'); + remove.click(); + expect(instance.filteredItems.length).toBe(3); // back to all + }); +}); From c64199f07e447eccea8ef191e7cb9fd7c41256b1 Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:49:58 -0800 Subject: [PATCH 22/52] Add tests and export for FormatNumbers module Added CommonJS export to FormatNumbers.js for testability. Introduced comprehensive Jest tests for NumberFormatter and InitializeFormatNumbers covering formatting styles, error handling, and multiple element scenarios. --- Dist/Functional/FormatNumbers.js | 7 +++ __tests__/FormatNumbers.test.js | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 __tests__/FormatNumbers.test.js diff --git a/Dist/Functional/FormatNumbers.js b/Dist/Functional/FormatNumbers.js index fbd1e28..2b18d60 100644 --- a/Dist/Functional/FormatNumbers.js +++ b/Dist/Functional/FormatNumbers.js @@ -85,3 +85,10 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', InitializeFormatNumbers); } + +// Allow requiring this module in test environments without affecting browser usage +try { + if (typeof module !== 'undefined' && module.exports) { + module.exports = { NumberFormatter, InitializeFormatNumbers }; + } +} catch {} diff --git a/__tests__/FormatNumbers.test.js b/__tests__/FormatNumbers.test.js new file mode 100644 index 0000000..10c54af --- /dev/null +++ b/__tests__/FormatNumbers.test.js @@ -0,0 +1,95 @@ +/** @jest-environment jsdom */ + +// Prevent auto init on require +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('NumberFormatter', () => { + let NumberFormatter, InitializeFormatNumbers; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ NumberFormatter, InitializeFormatNumbers } = require('../Dist/Functional/FormatNumbers.js')); + }); + + test('formats plain decimal number with en-US locale default style=decimal', () => { + document.body.innerHTML = ` +
1234567.89
+ `; + InitializeFormatNumbers(); + const el = document.querySelector('[wt-formatnumber-element="number"]'); + // Expect grouping and decimal + expect(el.textContent).toBe('1,234,567.89'); + }); + + test('currency formatting applies symbol and grouping', () => { + document.body.innerHTML = ` +
9876.5
+ `; + InitializeFormatNumbers(); + const el = document.querySelector('[wt-formatnumber-element="number"]'); + // Currency formatting: "$9,876.50" (locale dependent but stable in jsdom Node) + expect(el.textContent).toMatch(/\$9,?876\.50/); + }); + + test('percent style formats number as percent', () => { + document.body.innerHTML = ` +
0.256
+ `; + InitializeFormatNumbers(); + const el = document.querySelector('[wt-formatnumber-element="number"]'); + expect(el.textContent).toBe('26%'); // typical rounding + }); + + test('unit style with provided unit', () => { + document.body.innerHTML = ` +
1500
+ `; + InitializeFormatNumbers(); + const el = document.querySelector('[wt-formatnumber-element="number"]'); + // Example output "1,500 km"; unit formatting may vary slightly, so assert contains km + expect(el.textContent).toMatch(/1,?500.*km/i); + }); + + test('invalid style causes graceful error logging and leaves value intact', () => { + const originalError = console.error; const errSpy = jest.fn(); console.error = errSpy; + document.body.innerHTML = ` +
1234
+ `; + InitializeFormatNumbers(); + const el = document.querySelector('[wt-formatnumber-element="number"]'); + // Should not format, error logged, text still original + expect(el.textContent).toBe('1234'); + expect(errSpy).toHaveBeenCalled(); + console.error = originalError; + }); + + test('invalid number value logs error and leaves content', () => { + const originalError = console.error; const errSpy = jest.fn(); console.error = errSpy; + document.body.innerHTML = ` +
abc
+ `; + InitializeFormatNumbers(); + const el = document.querySelector('[wt-formatnumber-element="number"]'); + expect(el.textContent).toBe('abc'); + expect(errSpy).toHaveBeenCalled(); + console.error = originalError; + }); + + test('multiple numbers produce multiple instances pushed to webtricks', () => { + document.body.innerHTML = ` +
1000
+
2000
+ `; + InitializeFormatNumbers(); + expect(window.webtricks.filter(e => e.FormatNumber).length).toBe(2); + }); + + test('no elements to format logs error but does not throw', () => { + const originalError = console.error; const errSpy = jest.fn(); console.error = errSpy; + InitializeFormatNumbers(); + expect(errSpy).toHaveBeenCalled(); + console.error = originalError; + }); +}); From c1abd49612327677b30bfa6e3f107a7e6799dd2e Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:50:50 -0800 Subject: [PATCH 23/52] Add Node.js test workflow with coverage reporting Introduces a GitHub Actions workflow to run tests on push and pull request events. The workflow sets up Node.js, installs dependencies, runs Jest tests with coverage, uploads coverage artifacts, and appends a coverage summary to the job summary. --- .github/workflows/tests.yml | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d8a71cb..2a83034 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,3 +1,48 @@ +name: tests + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + +jobs: + test: + name: tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies (lockfile) + if: ${{ hashFiles('package-lock.json') != '' }} + run: npm ci + + - name: Install dependencies + if: ${{ hashFiles('package-lock.json') == '' }} + run: npm install + + - name: Run tests with coverage + run: npx jest --runInBand --coverage --coverageReporters=text-summary --coverageReporters=lcov --coverageReporters=json-summary + + - name: Upload coverage artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage/** + + - name: Add coverage summary to job summary + if: always() + shell: bash + run: | + node -e "const fs=require('fs');const p='coverage/coverage-summary.json';if(fs.existsSync(p)){const s=JSON.parse(fs.readFileSync(p,'utf8'));const t=(o)=>o.total;const c=t(s);const md=\`## Coverage summary\\n\\n- Statements: \${c.statements.pct}% (\${c.statements.covered}/\${c.statements.total})\\n- Branches: \${c.branches.pct}% (\${c.branches.covered}/\${c.branches.total})\\n- Functions: \${c.functions.pct}% (\${c.functions.covered}/\${c.functions.total})\\n- Lines: \${c.lines.pct}% (\${c.lines.covered}/\${c.lines.total})\\n\`;fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md);}else{console.log('coverage-summary.json not found')}}" name: ShareLink Tests on: From 29f0173a07af79eece11fb417b1f3188f3ecead0 Mon Sep 17 00:00:00 2001 From: Joorch Date: Sun, 9 Nov 2025 16:59:32 -0800 Subject: [PATCH 24/52] Add CommonJS export and tests for CMSSelect Exports CMSSelect and InitializeCMSSelect for CommonJS environments to support testing and bundlers. Adds comprehensive Jest tests for CMSSelect functionality and updates documentation to describe programmatic initialization. --- Dist/WebflowOnly/CMSSelect.js | 5 ++ __tests__/CMSSelect.test.js | 95 +++++++++++++++++++++++++++++++++++ docs/WebflowOnly/CMSSelect.md | 11 +++- 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 __tests__/CMSSelect.test.js diff --git a/Dist/WebflowOnly/CMSSelect.js b/Dist/WebflowOnly/CMSSelect.js index 61a3515..20b10b3 100644 --- a/Dist/WebflowOnly/CMSSelect.js +++ b/Dist/WebflowOnly/CMSSelect.js @@ -57,3 +57,8 @@ if (/complete|interactive|loaded/.test(document.readyState)) { } else { window.addEventListener('DOMContentLoaded', InitializeCMSSelect); } + +// Export for CommonJS (testing / bundler environments) +if (typeof module !== 'undefined' && module.exports) { + module.exports = { CMSSelect, InitializeCMSSelect }; +} diff --git a/__tests__/CMSSelect.test.js b/__tests__/CMSSelect.test.js new file mode 100644 index 0000000..45e1cc8 --- /dev/null +++ b/__tests__/CMSSelect.test.js @@ -0,0 +1,95 @@ +/** @jest-environment jsdom */ + +describe('CMSSelect', () => { + let InitializeCMSSelect; + + beforeEach(() => { + // Keep readyState as loading so auto init doesn't run before we call initializer + Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ InitializeCMSSelect } = require('../Dist/WebflowOnly/CMSSelect.js')); + }); + + test('single select is populated with basic options', () => { + document.body.innerHTML = ` + +
Option 1
+
Option 2
+
Option 3
+ `; + InitializeCMSSelect(); + const instance = window.webtricks.find(e => e.CMSSelect).CMSSelect; + expect(instance.selectElement.options.length).toBe(3); + expect(Array.from(instance.selectElement.options).map(o => o.text)).toEqual(['Option 1','Option 2','Option 3']); + }); + + test('custom value attribute overrides option value', () => { + document.body.innerHTML = ` + +
Display A
+
Display B
+ `; + InitializeCMSSelect(); + const instance = window.webtricks[0].CMSSelect; + const values = Array.from(instance.selectElement.options).map(o => o.value); + expect(values).toEqual(['val-a','val-b']); + }); + + test('empty text sources are ignored', () => { + document.body.innerHTML = ` + +
First
+
+
+
Last
+ `; + InitializeCMSSelect(); + const instance = window.webtricks[0].CMSSelect; + expect(instance.selectElement.options.length).toBe(2); + expect(Array.from(instance.selectElement.options).map(o => o.text)).toEqual(['First','Last']); + }); + + test('multiple selects get their own target options', () => { + document.body.innerHTML = ` + + +
Alpha
+
Beta
+
Gamma
+
Delta
+ `; + InitializeCMSSelect(); + const instances = window.webtricks.filter(e => e.CMSSelect).map(e => e.CMSSelect); + expect(instances.length).toBe(2); + const [first, second] = instances; + expect(Array.from(first.selectElement.options).map(o => o.text)).toEqual(['Alpha','Beta']); + expect(Array.from(second.selectElement.options).map(o => o.text)).toEqual(['Gamma','Delta']); + }); + + test('missing targets logs error and leaves select empty', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + document.body.innerHTML = ` + + `; + InitializeCMSSelect(); + const instance = window.webtricks[0].CMSSelect; + expect(instance.selectElement.options.length).toBe(0); + expect(spy).toHaveBeenCalled(); + const logged = spy.mock.calls.flat().join(' '); + expect(logged).toMatch(/No options found/); + spy.mockRestore(); + }); + + test('falls back to text for value when custom value missing', () => { + document.body.innerHTML = ` + +
Plain Text
+ `; + InitializeCMSSelect(); + const opt = window.webtricks[0].CMSSelect.selectElement.options[0]; + expect(opt.text).toBe('Plain Text'); + expect(opt.value).toBe('Plain Text'); + }); +}); diff --git a/docs/WebflowOnly/CMSSelect.md b/docs/WebflowOnly/CMSSelect.md index 896958b..3e6a8f1 100644 --- a/docs/WebflowOnly/CMSSelect.md +++ b/docs/WebflowOnly/CMSSelect.md @@ -12,6 +12,7 @@ CMSSelect is a Webflow-specific script that automatically populates select eleme - Handles custom values and text content - Error handling and validation - Automatic initialization on page load + - Programmatic initialization available via `InitializeCMSSelect` (CommonJS export) ## Usage Add the script to your Webflow project and include the required attributes on your select element and option sources. @@ -82,4 +83,12 @@ For multiple selects: 2. Dynamic filtering interfaces 3. Form select population 4. Category selection -5. Dynamic navigation menus \ No newline at end of file +5. Dynamic navigation menus + +### Programmatic usage (testing/bundlers) +When using in a bundler or testing environment, you can import the initializer: + +```js +const { InitializeCMSSelect } = require('../../Dist/WebflowOnly/CMSSelect.js'); +InitializeCMSSelect(); +``` \ No newline at end of file From 55d822b580664b0c2f0279cf816d51b74adb301b Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Mon, 10 Nov 2025 13:21:43 -0800 Subject: [PATCH 25/52] Update contributors-readme-action version Signed-off-by: Jorge C. --- .github/workflows/update-contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml index 22c9ac5..6126a4a 100644 --- a/.github/workflows/update-contributors.yml +++ b/.github/workflows/update-contributors.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - name: Update contributors section in README - uses: akhilmhdh/contributors-readme-action@v2.5.8 + uses: akhilmhdh/contributors-readme-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From ac745060893dfb91d19909407fb02a601a3401e1 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Mon, 10 Nov 2025 13:26:54 -0800 Subject: [PATCH 26/52] Update contributors-readme-action to version 2.3.11 Signed-off-by: Jorge C. --- .github/workflows/update-contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml index 6126a4a..3185a37 100644 --- a/.github/workflows/update-contributors.yml +++ b/.github/workflows/update-contributors.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - name: Update contributors section in README - uses: akhilmhdh/contributors-readme-action@v2 + uses: akhilmhdh/contributors-readme-action@v2.3.11 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 2bf26e77789e533e5845bd4c2a8a5520e1dfe767 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Mon, 10 Nov 2025 13:29:48 -0800 Subject: [PATCH 27/52] Add pull-requests write permission to workflow Signed-off-by: Jorge C. --- .github/workflows/update-contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml index 3185a37..b945a59 100644 --- a/.github/workflows/update-contributors.yml +++ b/.github/workflows/update-contributors.yml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v4 From fb43cfeb7177860983362cf98a2a5bac91d2ce12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:30:18 +0000 Subject: [PATCH 28/52] docs(readme): update contributors [skip ci] --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 8a681cd..0de1104 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,26 @@ After a pull request has been open for over 30 days with no activity or response

+ + + + + + + +
+ + JorchCortez +
+ JorchCortez +
+
+ + Likhithakathireddy +
+ Likhithakathireddy +
+
From 195d850eb161ca167d02be09d6b8af06f8946709 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Mon, 10 Nov 2025 13:32:49 -0800 Subject: [PATCH 29/52] Revise contributors section in README.md Updated contributors section formatting and removed old content. Signed-off-by: Jorge C. --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8a681cd..aadbab6 100644 --- a/README.md +++ b/README.md @@ -130,13 +130,10 @@ All official releases shall be in master. Any updates in between (u After a pull request has been open for over 30 days with no activity or response from the author, it'll be automatically marked as stale. We might fork your changes and merge the changes ourselves. Since GitHub tracks contributions by commits, you will be credited.


-

Contributors

-

- Big thanks to everyone who has contributed to this project. The list below updates automatically after changes land on main. -

- - +## Contributors + +
From c79a8464826fde32f4916ea26cf1767860f305d1 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Mon, 10 Nov 2025 13:48:49 -0800 Subject: [PATCH 30/52] Update README.md Signed-off-by: Jorge C. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d7870e7..c918c53 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,11 @@ After a pull request has been open for over 30 days with no activity or response


+

Our Contributors

+

+ The community supports this project; huge thanks to everyone who has made this possible! +

+ From 5159055fd201867cf3fb3416a89a21b4c1297b7a Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Mon, 10 Nov 2025 13:49:34 -0800 Subject: [PATCH 31/52] Update monthly hits badge in README Signed-off-by: Jorge C. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c918c53..9fc5752 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ GitHub License - Static Badge + Static Badge


From 1f42088a609899ec84438b866004a8bd348e9f91 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Wed, 12 Nov 2025 15:15:14 -0800 Subject: [PATCH 32/52] Remove GitHub License badge Removed GitHub License badge from README. Signed-off-by: Jorge C. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 9fc5752..75f27f8 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,6 @@ GitHub repository stars - - GitHub License - Static Badge From f563a5a791ff82d8f6d666b3bbe80a75ce9c8991 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Tue, 9 Dec 2025 10:21:28 -0800 Subject: [PATCH 33/52] Update tests.yml to report coverage summary Removed the ShareLink Tests job and added coverage summary reporting. Signed-off-by: Jorge C. --- .github/workflows/tests.yml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a83034..b09bad6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,24 +43,3 @@ jobs: shell: bash run: | node -e "const fs=require('fs');const p='coverage/coverage-summary.json';if(fs.existsSync(p)){const s=JSON.parse(fs.readFileSync(p,'utf8'));const t=(o)=>o.total;const c=t(s);const md=\`## Coverage summary\\n\\n- Statements: \${c.statements.pct}% (\${c.statements.covered}/\${c.statements.total})\\n- Branches: \${c.branches.pct}% (\${c.branches.covered}/\${c.branches.total})\\n- Functions: \${c.functions.pct}% (\${c.functions.covered}/\${c.functions.total})\\n- Lines: \${c.lines.pct}% (\${c.lines.covered}/\${c.lines.total})\\n\`;fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md);}else{console.log('coverage-summary.json not found')}}" -name: ShareLink Tests - -on: - pull_request: - branches: [ Develop, main ] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: 'npm' - - name: Install dependencies - run: npm install - - name: Run tests - run: npm test From 8daee36d160fea5edc8305a654f63295f81a2dd1 Mon Sep 17 00:00:00 2001 From: "Jorge C." Date: Wed, 7 Jan 2026 10:36:42 -0800 Subject: [PATCH 34/52] Update package.json with new version and test settings Updated version and description fields, modified test script to include coverage options, and added collectCoverageFrom configuration. Signed-off-by: Jorge C. --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 07749f2..45bf841 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "webtricks", - "version": "0.1.0", - "private": true, - "description": "Tests for WebTricks modules", + "version": "1.0.0", + "description": "Vanilla JavaScript utility functions", + "type": "module", "scripts": { - "test": "jest --runInBand" + "test": "jest --runInBand --coverage --coverageReporters=text-summary --coverageReporters=lcov --coverageReporters=json-summary" }, "devDependencies": { "jest": "^29.7.0", @@ -12,11 +12,11 @@ }, "jest": { "testEnvironment": "jsdom", - "testMatch": ["**/__tests__/**/*.test.js"], - "setupFilesAfterEnv": ["/jest.setup.js"], - "testTimeout": 20000, - "testEnvironmentOptions": { - "url": "https://example.com/" - } + "collectCoverageFrom": [ + "**/*.js", + "!**/node_modules/**", + "!**/coverage/**", + "!jest.config.js" + ] } } From b5f1fdc56c821f307c2a1f407475d9e10cf6b471 Mon Sep 17 00:00:00 2001 From: Matthew Simpson Date: Wed, 25 Feb 2026 12:53:35 -0800 Subject: [PATCH 35/52] adds optional parameter for maximum elements displayed if the wt-cms-element="empty" flag is set --- .gitignore | 3 +- Dist/WebflowOnly/CMSFilter.js | 1913 ++++++++++++++++++--------------- __tests__/CMSFilter.test.js | 215 +++- docs/WebflowOnly/CMSFilter.md | 6 + 4 files changed, 1201 insertions(+), 936 deletions(-) diff --git a/.gitignore b/.gitignore index 40b878d..417c6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules/ \ No newline at end of file +node_modules/ +.vscode/ \ No newline at end of file diff --git a/Dist/WebflowOnly/CMSFilter.js b/Dist/WebflowOnly/CMSFilter.js index bc3cc29..66bae7c 100644 --- a/Dist/WebflowOnly/CMSFilter.js +++ b/Dist/WebflowOnly/CMSFilter.js @@ -1,960 +1,1115 @@ -'use strict'; +"use strict"; class CMSFilter { - constructor() { - //CORE elements - this.filterForm = document.querySelector('[wt-cmsfilter-element="filter-form"]'); - this.listElement = document.querySelector('[wt-cmsfilter-element="list"]'); - this.filterElements = this.filterForm.querySelectorAll('[wt-cmsfilter-category]'); - this.currentPage = 1; // default value - this.itemsPerPage = 0; // gets updated during init - this.debounceDelay = parseInt(this.filterForm.getAttribute('wt-cmsfilter-debounce') || '300'); - - //TAG elements - this.tagTemplate = document.querySelector('[wt-cmsfilter-element="tag-template"]'); - this.tagTemplateContainer = (this.tagTemplate) ? this.tagTemplate.parentElement : null; - - //Pagination & Loading - //Pagination wrapper is a MUST for the full functionality of the filter to work properly, - //if not added the filter will only work with whatever is loaded by default. - this.paginationWrapper = document.querySelector('[wt-cmsfilter-element="pagination-wrapper"]') || null; - this.loadMode = this.listElement.getAttribute('wt-cmsfilter-loadmode') || 'load-all'; - this.previousButton = document.querySelector('[wt-cmsfilter-pagination="prev"]'); - this.nextButton = document.querySelector('[wt-cmsfilter-pagination="next"]'); - this.customNextButton = document.querySelector('[wt-cmsfilter-element="custom-next"]'); - this.customPrevButton = document.querySelector('[wt-cmsfilter-element="custom-prev"]'); - - this.paginationcounter = document.querySelector('[wt-cmsfilter-element="page-count"]'); - this.activeFilterClass = this.filterForm.getAttribute('wt-cmsfilter-class'); - this.clearAll = document.querySelector('[wt-cmsfilter-element="clear-all"]'); - this.sortOptions = document.querySelector('[wt-cmsfilter-element="sort-options"]'); - this.resultCount = document.querySelector('[wt-cmsfilter-element="results-count"]'); - this.emptyElement = document.querySelector('[wt-cmsfilter-element="empty"]'); - this.resetIx2 = this.listElement.getAttribute('wt-cmsfilter-resetix2') || false; - - this.allItems = []; - this.filteredItems = []; - this.totalPages = 1; - this.activeFilters = {}; - this.availableFilters = {}; - this.dataRanges = {}; // Store calculated min/max for numeric categories (set once) - this.originalDisplayStyles = new Map(); // Store original display styles for filter elements - - this.init(); + constructor() { + //CORE elements + this.filterForm = document.querySelector( + '[wt-cmsfilter-element="filter-form"]', + ); + this.listElement = document.querySelector('[wt-cmsfilter-element="list"]'); + this.filterElements = this.filterForm.querySelectorAll( + "[wt-cmsfilter-category]", + ); + this.currentPage = 1; // default value + this.itemsPerPage = 0; // gets updated during init + this.debounceDelay = parseInt( + this.filterForm.getAttribute("wt-cmsfilter-debounce") || "300", + ); + + //TAG elements + this.tagTemplate = document.querySelector( + '[wt-cmsfilter-element="tag-template"]', + ); + this.tagTemplateContainer = this.tagTemplate + ? this.tagTemplate.parentElement + : null; + + //Pagination & Loading + //Pagination wrapper is a MUST for the full functionality of the filter to work properly, + //if not added the filter will only work with whatever is loaded by default. + this.paginationWrapper = + document.querySelector('[wt-cmsfilter-element="pagination-wrapper"]') || + null; + this.loadMode = + this.listElement.getAttribute("wt-cmsfilter-loadmode") || "load-all"; + this.previousButton = document.querySelector( + '[wt-cmsfilter-pagination="prev"]', + ); + this.nextButton = document.querySelector( + '[wt-cmsfilter-pagination="next"]', + ); + this.customNextButton = document.querySelector( + '[wt-cmsfilter-element="custom-next"]', + ); + this.customPrevButton = document.querySelector( + '[wt-cmsfilter-element="custom-prev"]', + ); + + this.paginationcounter = document.querySelector( + '[wt-cmsfilter-element="page-count"]', + ); + this.activeFilterClass = this.filterForm.getAttribute("wt-cmsfilter-class"); + this.clearAll = document.querySelector( + '[wt-cmsfilter-element="clear-all"]', + ); + this.sortOptions = document.querySelector( + '[wt-cmsfilter-element="sort-options"]', + ); + this.resultCount = document.querySelector( + '[wt-cmsfilter-element="results-count"]', + ); + this.emptyElement = document.querySelector( + '[wt-cmsfilter-element="empty"]', + ); + this.emptyMaxCount = 0; + if (this.emptyElement) { + const emptyMaxValue = parseInt( + this.emptyElement.getAttribute("wt-cmsfilter-empty-max"), + 10, + ); + if (Number.isInteger(emptyMaxValue) && emptyMaxValue >= 0) { + this.emptyMaxCount = emptyMaxValue; + } } - - async init() { - this.allItems = Array.from(this.listElement.children); - this.itemsPerPage = this.allItems.length; - if (this.paginationWrapper) { - await this.LoadAllItems(); - if (this.paginationcounter && this.paginationcounter != this.paginationWrapper.querySelector('.w-page-count')) { - this.paginationWrapper.querySelector('.w-page-count').remove(); - } else { - this.paginationcounter = this.paginationWrapper.querySelector('.w-page-count'); - } + this.resetIx2 = + this.listElement.getAttribute("wt-cmsfilter-resetix2") || false; + + this.allItems = []; + this.filteredItems = []; + this.totalPages = 1; + this.activeFilters = {}; + this.availableFilters = {}; + this.dataRanges = {}; // Store calculated min/max for numeric categories (set once) + this.originalDisplayStyles = new Map(); // Store original display styles for filter elements + + this.init(); + } + + async init() { + this.allItems = Array.from(this.listElement.children); + this.itemsPerPage = this.allItems.length; + if (this.paginationWrapper) { + await this.LoadAllItems(); + if ( + this.paginationcounter && + this.paginationcounter != + this.paginationWrapper.querySelector(".w-page-count") + ) { + this.paginationWrapper.querySelector(".w-page-count").remove(); + } else { + this.paginationcounter = + this.paginationWrapper.querySelector(".w-page-count"); + } + } + this.SetupEventListeners(); + + this.InitializeTagTemplate(); + // Capture original display styles before any filtering occurs + this.captureOriginalDisplayStyles(); + + // Cache search text for all items once during initialization + this.cacheItemSearchData(); + + this.RenderItems(); + this.UpdateAvailableFilters(); + + // Calculate range slider bounds once from original data (never changes during filtering) + this.calculateInitialRanges(); + + // Initialize range inputs defaults (from=min, to=max) once + this.initializeRangeInputsDefaults(); + + this.activeFilters = this.GetFilters(); + this.ShowResultCount(); + } + + /** + * Calculates initial data ranges from all items and configures range sliders + * This is called once during initialization and ranges never change during filtering + */ + calculateInitialRanges() { + this.dataRanges = {}; + + // Get all categories from filter elements + const categories = new Set(); + this.filterElements.forEach((element) => { + const category = element.getAttribute("wt-cmsfilter-category"); + if (category && category !== "*") { + categories.add(this.GetDataSet(category)); + } + }); + + categories.forEach((category) => { + const values = this.allItems + .map((item) => parseFloat(item.dataset[category])) + .filter((value) => !isNaN(value) && isFinite(value)); + + if (values.length > 0) { + this.dataRanges[category] = { + min: Math.min(...values), + max: Math.max(...values), + count: values.length, + }; + } + }); + + // Configure range sliders with calculated ranges (only once) + this.configureRangeSliders(); + } + + /** + * Configures range sliders with calculated data ranges + * Sets min/max values for sliders configured with wt-rangeslider-category + * Only called once during initialization + */ + configureRangeSliders() { + // Find all range slider elements + const rangeSliders = document.querySelectorAll( + '[wt-rangeslider-element="slider"]', + ); + + if (!rangeSliders.length) return; + + rangeSliders.forEach((slider) => { + // Try to get category from slider attribute first + let category = slider.getAttribute("wt-rangeslider-category"); + + // If not found on slider, look for category from associated filter inputs + if (!category) { + const wrapper = slider.closest( + '[wt-rangeslider-element="slider-wrapper"]', + ); + if (wrapper) { + const categoryInput = wrapper.querySelector( + "[wt-cmsfilter-category]", + ); + if (categoryInput) { + category = categoryInput.getAttribute("wt-cmsfilter-category"); + } } - this.SetupEventListeners(); - - this.InitializeTagTemplate(); - // Capture original display styles before any filtering occurs - this.captureOriginalDisplayStyles(); - - // Cache search text for all items once during initialization - this.cacheItemSearchData(); - - this.RenderItems(); - this.UpdateAvailableFilters(); - - // Calculate range slider bounds once from original data (never changes during filtering) - this.calculateInitialRanges(); - - // Initialize range inputs defaults (from=min, to=max) once - this.initializeRangeInputsDefaults(); - - this.activeFilters = this.GetFilters(); - this.ShowResultCount(); + } + + // Skip if no category found or no data range for this category + if (!category || !this.dataRanges[this.GetDataSet(category)]) return; + + const datasetCategory = this.GetDataSet(category); + + // Check if manual configuration exists (manual takes precedence) + const hasManualMin = slider.hasAttribute("wt-rangeslider-min"); + const hasManualMax = slider.hasAttribute("wt-rangeslider-max"); + + // Only set auto-detected values if manual ones aren't provided + if (!hasManualMin) { + slider.setAttribute( + "wt-rangeslider-min", + this.dataRanges[datasetCategory].min.toString(), + ); + } + if (!hasManualMax) { + slider.setAttribute( + "wt-rangeslider-max", + this.dataRanges[datasetCategory].max.toString(), + ); + } + + // Set intelligent default steps if not specified + if (!slider.hasAttribute("wt-rangeslider-steps")) { + const range = + this.dataRanges[datasetCategory].max - + this.dataRanges[datasetCategory].min; + const defaultSteps = + range > 1000 ? 100 : range > 100 ? 10 : range > 10 ? 1 : 0.1; + slider.setAttribute("wt-rangeslider-steps", defaultSteps.toString()); + } + + console.log( + `Configured range slider for ${category}: min=${this.dataRanges[datasetCategory].min}, max=${this.dataRanges[datasetCategory].max}`, + ); + }); + } + + /** + * Build and store search cache for all items + */ + cacheItemSearchData() { + if (!this.allItems || this.allItems.length === 0) return; + this.allItems.forEach((item) => this.cacheItemForSearch(item)); + } + + /** + * Build and attach a normalized search cache for a single item + * Cache shape: + * { + * globalSearchText: string, + * datasetValues: Map, + * categoryTexts: Map + * } + */ + cacheItemForSearch(item) { + if (!item || !(item instanceof Element)) return; + + const normalize = (text) => + (text || "") + .toString() + .toLowerCase() + .replace(/(?: |\s)+/gi, " ") + .trim(); + + const datasetValues = new Map(); + const categoryTexts = new Map(); + + // Cache dataset values (normalized) + if (item.dataset) { + Object.keys(item.dataset).forEach((key) => { + const value = item.dataset[key]; + datasetValues.set(key, normalize(value)); + }); } - /** - * Calculates initial data ranges from all items and configures range sliders - * This is called once during initialization and ranges never change during filtering - */ - calculateInitialRanges() { - this.dataRanges = {}; - - // Get all categories from filter elements - const categories = new Set(); - this.filterElements.forEach(element => { - const category = element.getAttribute('wt-cmsfilter-category'); - if (category && category !== '*') { - categories.add(this.GetDataSet(category)); - } + // Cache category-specific text found inside the item + const categoryNodes = item.querySelectorAll("[wt-cmsfilter-category]"); + categoryNodes.forEach((node) => { + const category = node.getAttribute("wt-cmsfilter-category"); + if (!category) return; + const text = node.textContent || node.innerText || ""; + categoryTexts.set(category, normalize(text)); + }); + + // Global searchable text: item's text + dataset values + const itemText = normalize(item.textContent || item.innerText || ""); + const datasetConcat = Array.from(datasetValues.values()).join(" "); + const globalSearchText = normalize(`${itemText} ${datasetConcat}`); + + item._wtSearchCache = { globalSearchText, datasetValues, categoryTexts }; + } + + /** + * Initialize default values for range inputs using precomputed data ranges + * - Sets wt-cmsfilter-default to min (from) or max (to) if not present + * - Populates the input's value if it's empty + */ + initializeRangeInputsDefaults() { + if (!this.filterElements || !this.dataRanges) return; + + this.filterElements.forEach((element) => { + const input = + element.tagName === "INPUT" + ? element + : element.querySelector('input[type="text"]'); + + if (!input || input.type !== "text") return; + + const rangeType = element.getAttribute("wt-cmsfilter-range"); + if (rangeType !== "from" && rangeType !== "to") return; + + const categoryAttr = element.getAttribute("wt-cmsfilter-category"); + if (!categoryAttr) return; + + const datasetCategory = this.GetDataSet(categoryAttr); + const ranges = this.dataRanges[datasetCategory]; + if (!ranges) return; + + const defaultValue = rangeType === "from" ? ranges.min : ranges.max; + if (!Number.isFinite(defaultValue)) return; + + if (!input.hasAttribute("wt-cmsfilter-default")) { + input.setAttribute("wt-cmsfilter-default", String(defaultValue)); + } + + if (input.value.trim() === "") { + input.value = String(defaultValue); + } + }); + } + + SetupEventListeners() { + // Create a debounced version of ApplyFilters + const debouncedApplyFilters = this.debounce( + () => this.ApplyFilters(), + this.debounceDelay, + ); + + if (this.filterForm.hasAttribute("wt-cmsfilter-trigger")) { + if (this.filterForm.getAttribute("wt-cmsfilter-trigger") === "button") { + this.filterForm.addEventListener("submit", (event) => { + event.preventDefault(); + this.ApplyFilters(); // No debounce needed for button submission }); - - categories.forEach(category => { - const values = this.allItems - .map(item => parseFloat(item.dataset[category])) - .filter(value => !isNaN(value) && isFinite(value)); - - if (values.length > 0) { - this.dataRanges[category] = { - min: Math.min(...values), - max: Math.max(...values), - count: values.length - }; - } + } else { + this.filterForm.addEventListener("change", () => { + debouncedApplyFilters(); }); - - // Configure range sliders with calculated ranges (only once) - this.configureRangeSliders(); - } - - /** - * Configures range sliders with calculated data ranges - * Sets min/max values for sliders configured with wt-rangeslider-category - * Only called once during initialization - */ - configureRangeSliders() { - // Find all range slider elements - const rangeSliders = document.querySelectorAll('[wt-rangeslider-element="slider"]'); - - if(!rangeSliders.length) return; - - rangeSliders.forEach(slider => { - // Try to get category from slider attribute first - let category = slider.getAttribute('wt-rangeslider-category'); - - // If not found on slider, look for category from associated filter inputs - if (!category) { - const wrapper = slider.closest('[wt-rangeslider-element="slider-wrapper"]'); - if (wrapper) { - const categoryInput = wrapper.querySelector('[wt-cmsfilter-category]'); - if (categoryInput) { - category = categoryInput.getAttribute('wt-cmsfilter-category'); - } - } - } - - // Skip if no category found or no data range for this category - if (!category || !this.dataRanges[this.GetDataSet(category)]) return; - - const datasetCategory = this.GetDataSet(category); - - // Check if manual configuration exists (manual takes precedence) - const hasManualMin = slider.hasAttribute('wt-rangeslider-min'); - const hasManualMax = slider.hasAttribute('wt-rangeslider-max'); - - // Only set auto-detected values if manual ones aren't provided - if (!hasManualMin) { - slider.setAttribute('wt-rangeslider-min', this.dataRanges[datasetCategory].min.toString()); - } - if (!hasManualMax) { - slider.setAttribute('wt-rangeslider-max', this.dataRanges[datasetCategory].max.toString()); - } - - // Set intelligent default steps if not specified - if (!slider.hasAttribute('wt-rangeslider-steps')) { - const range = this.dataRanges[datasetCategory].max - this.dataRanges[datasetCategory].min; - const defaultSteps = range > 1000 ? 100 : range > 100 ? 10 : range > 10 ? 1 : 0.1; - slider.setAttribute('wt-rangeslider-steps', defaultSteps.toString()); - } - - console.log(`Configured range slider for ${category}: min=${this.dataRanges[datasetCategory].min}, max=${this.dataRanges[datasetCategory].max}`); + this.filterForm.addEventListener("input", () => { + debouncedApplyFilters(); }); + } + } else { + this.filterForm.addEventListener("change", () => { + debouncedApplyFilters(); + }); + this.filterForm.addEventListener("input", () => { + debouncedApplyFilters(); + }); } - /** - * Build and store search cache for all items - */ - cacheItemSearchData() { - if (!this.allItems || this.allItems.length === 0) return; - this.allItems.forEach(item => this.cacheItemForSearch(item)); + if (this.previousButton || this.customPrevButton) { + if (this.customPrevButton) { + this.customPrevButton.addEventListener("click", (event) => { + event.preventDefault(); + this.PrevPage(); + }); + if (this.previousButton) { + this.previousButton.remove(); + } + } else { + this.previousButton.addEventListener("click", (event) => { + event.preventDefault(); + this.PrevPage(); + }); + } } - - /** - * Build and attach a normalized search cache for a single item - * Cache shape: - * { - * globalSearchText: string, - * datasetValues: Map, - * categoryTexts: Map - * } - */ - cacheItemForSearch(item) { - if (!item || !(item instanceof Element)) return; - - const normalize = (text) => (text || '') - .toString() - .toLowerCase() - .replace(/(?: |\s)+/gi, ' ') - .trim(); - - const datasetValues = new Map(); - const categoryTexts = new Map(); - - // Cache dataset values (normalized) - if (item.dataset) { - Object.keys(item.dataset).forEach(key => { - const value = item.dataset[key]; - datasetValues.set(key, normalize(value)); - }); + if (this.nextButton || this.customNextButton) { + if (this.customNextButton) { + this.customNextButton.addEventListener("click", (event) => { + event.preventDefault(); + this.NextPage(); + }); + if (this.nextButton) { + this.nextButton.remove(); } - - // Cache category-specific text found inside the item - const categoryNodes = item.querySelectorAll('[wt-cmsfilter-category]'); - categoryNodes.forEach(node => { - const category = node.getAttribute('wt-cmsfilter-category'); - if (!category) return; - const text = node.textContent || node.innerText || ''; - categoryTexts.set(category, normalize(text)); + } else { + this.nextButton.addEventListener("click", (event) => { + event.preventDefault(); + this.NextPage(); }); - - // Global searchable text: item's text + dataset values - const itemText = normalize(item.textContent || item.innerText || ''); - const datasetConcat = Array.from(datasetValues.values()).join(' '); - const globalSearchText = normalize(`${itemText} ${datasetConcat}`); - - item._wtSearchCache = { globalSearchText, datasetValues, categoryTexts }; + } } - /** - * Initialize default values for range inputs using precomputed data ranges - * - Sets wt-cmsfilter-default to min (from) or max (to) if not present - * - Populates the input's value if it's empty - */ - initializeRangeInputsDefaults() { - if (!this.filterElements || !this.dataRanges) return; - - this.filterElements.forEach(element => { - const input = (element.tagName === 'INPUT') - ? element - : element.querySelector('input[type="text"]'); - - if (!input || input.type !== 'text') return; - - const rangeType = element.getAttribute('wt-cmsfilter-range'); - if (rangeType !== 'from' && rangeType !== 'to') return; - - const categoryAttr = element.getAttribute('wt-cmsfilter-category'); - if (!categoryAttr) return; - - const datasetCategory = this.GetDataSet(categoryAttr); - const ranges = this.dataRanges[datasetCategory]; - if (!ranges) return; - - const defaultValue = rangeType === 'from' ? ranges.min : ranges.max; - if (!Number.isFinite(defaultValue)) return; + if (this.clearAll) { + this.clearAll.addEventListener("click", (event) => { + event.preventDefault(); + this.ClearAllFilters(); + }); + } + if (this.sortOptions) { + this.sortOptions.addEventListener("change", (event) => { + event.preventDefault(); + this.ApplyFilters(); + }); + } + } - if (!input.hasAttribute('wt-cmsfilter-default')) { - input.setAttribute('wt-cmsfilter-default', String(defaultValue)); - } + generatePaginationLinksFromString(paginationString, baseUrl) { + const [currentPage, totalPages] = paginationString.split(" / ").map(Number); + const links = []; - if (input.value.trim() === '') { - input.value = String(defaultValue); - } - }); + for (let page = currentPage + 1; page <= totalPages; page++) { + const updatedUrl = baseUrl.replace(/page=\d+/, `page=${page}`); + links.push(updatedUrl); } - SetupEventListeners() { - // Create a debounced version of ApplyFilters - const debouncedApplyFilters = this.debounce(() => this.ApplyFilters(), this.debounceDelay); - - if(this.filterForm.hasAttribute('wt-cmsfilter-trigger')){ - if (this.filterForm.getAttribute('wt-cmsfilter-trigger') === 'button') { - this.filterForm.addEventListener('submit', (event) => { - event.preventDefault(); - this.ApplyFilters(); // No debounce needed for button submission - }); - } else { - this.filterForm.addEventListener('change', () => { - debouncedApplyFilters(); - }); - this.filterForm.addEventListener('input', () => { - debouncedApplyFilters(); - }); + return links; + } + + async LoadAllItems() { + if (!this.paginationWrapper) return; + this.itemsPerPage = this.allItems.length; + + const paginationPages = + this.paginationWrapper.querySelector(".w-page-count"); + const baseLink = this.paginationWrapper.querySelector("a"); + const links = this.generatePaginationLinksFromString( + paginationPages.innerText, + baseLink.href, + ); + if (!links || links.length === 0) return; + + const itemsBeforeLoad = this.allItems.length; + + for (const link of links) { + try { + const htmlDoc = await this.FetchHTML(link); + if (htmlDoc) { + const cards = Array.from( + htmlDoc.querySelector('[wt-cmsfilter-element="list"]')?.children || + [], + ); + + if (cards.length > 0) { + for (const card of cards) { + if (card instanceof Node) { + // Ensure it's a valid DOM node + this.allItems.push(card); + } else { + console.warn("Non-DOM element skipped:", card); + } } + } } else { - this.filterForm.addEventListener('change', () => { - debouncedApplyFilters(); - }); - this.filterForm.addEventListener('input', () => { - debouncedApplyFilters(); - }); - } - - if(this.previousButton || this.customPrevButton) { - if(this.customPrevButton) { - this.customPrevButton.addEventListener('click', (event) => { - event.preventDefault(); - this.PrevPage(); - }); - if (this.previousButton) { - this.previousButton.remove(); - } - } else { - this.previousButton.addEventListener('click', (event) => { - event.preventDefault(); - this.PrevPage(); - }); - } - } - if(this.nextButton || this.customNextButton) { - if(this.customNextButton) { - this.customNextButton.addEventListener('click', (event) => { - event.preventDefault(); - this.NextPage(); - }); - if (this.nextButton) { - this.nextButton.remove(); - } - } else { - this.nextButton.addEventListener('click', (event) => { - event.preventDefault(); - this.NextPage(); - }); - } + console.error("Failed to fetch HTML from the URL:", link.href); } + } catch (error) { + console.error("Error fetching HTML:", error); + } + } - if(this.clearAll) { - this.clearAll.addEventListener('click', (event) => { - event.preventDefault(); - this.ClearAllFilters(); - }); + // Cache search data for newly loaded items only + if (this.allItems.length > itemsBeforeLoad) { + const newItems = this.allItems.slice(itemsBeforeLoad); + newItems.forEach((item) => { + // Cache search data for new item + if (!item._wtSearchCache) { + this.cacheItemForSearch(item); } - if (this.sortOptions) { - this.sortOptions.addEventListener('change', (event) => { - event.preventDefault(); - this.ApplyFilters(); - }); + }); + } + } + + async FetchHTML(url) { + const response = await fetch(url, { + headers: { "X-Requested-With": "XMLHttpRequest" }, + }); + const text = await response.text(); + const parser = new DOMParser(); + return parser.parseFromString(text, "text/html"); + } + + FiltersApplied() { + return Object.values(this.activeFilters).some( + (arr) => Array.isArray(arr) && arr.length > 0, + ); + } + + RenderItems() { + this.listElement.innerHTML = ""; + if (this.filteredItems.length === 0) { + if (!this.FiltersApplied()) { + this.filteredItems = this.allItems; + } + } + if (this.paginationWrapper) { + if (this.loadMode === "load-all") { + this.filteredItems.forEach((item) => { + this.listElement.appendChild(item); + }); + if (this.paginationWrapper) { + this.paginationWrapper.remove(); } + } else if (this.loadMode === "paginate") { + this.totalPages = Math.ceil( + this.filteredItems.length / this.itemsPerPage, + ); + const currentSlice = + this.currentPage * this.itemsPerPage - this.itemsPerPage; + const currentPage = this.filteredItems.slice( + currentSlice, + currentSlice + this.itemsPerPage, + ); + currentPage.forEach((item) => { + this.listElement.appendChild(item); + if (this.resetIx2) this.ResetInteraction(item); + }); + } + } else { + this.filteredItems.forEach((item) => { + this.listElement.appendChild(item); + if (this.resetIx2) this.ResetInteraction(item); + }); } - generatePaginationLinksFromString(paginationString, baseUrl) { - const [currentPage, totalPages] = paginationString.split(' / ').map(Number); - const links = []; - - for (let page = currentPage + 1; page <= totalPages; page++) { - const updatedUrl = baseUrl.replace(/page=\d+/, `page=${page}`); - links.push(updatedUrl); + this.ToggleEmptyState(); + this.UpdatePaginationDisplay(); + } + + SortItems() { + if (!this.sortOptions) return; + + let [key, order] = this.sortOptions.value.split("-"); + this.filteredItems = this.filteredItems.filter( + (item) => !item.hasAttribute("wt-renderstatic-element"), + ); + this.filteredItems.sort((a, b) => { + let aValue = a.dataset[key]; + let bValue = b.dataset[key]; + + // Handle null or undefined values + if (aValue === undefined || aValue === null) aValue = ""; + if (bValue === undefined || bValue === null) bValue = ""; + + // Handle numeric values + if (!isNaN(aValue) && !isNaN(bValue)) { + aValue = parseFloat(aValue); + bValue = parseFloat(bValue); + } + // Handle date values + else if (!isNaN(Date.parse(aValue)) && !isNaN(Date.parse(bValue))) { + aValue = new Date(aValue); + bValue = new Date(bValue); + } + // Handle text values + else { + aValue = aValue.toString().toLowerCase(); + bValue = bValue.toString().toLowerCase(); + } + + if (order === "asc") { + return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; + } else { + return aValue < bValue ? 1 : aValue > bValue ? -1 : 0; + } + }); + } + + ApplyFilters() { + const filters = this.GetFilters(); + this.currentPage = 1; // Reset pagination to first page + this.filteredItems = this.allItems.filter((item) => { + return Object.keys(filters).every((category) => { + // Fix 1: Safari-compatible array handling + const categoryFilters = filters[category] || []; + const values = Array.isArray(categoryFilters) + ? categoryFilters.slice() + : []; + if (values.length === 0) return true; + + // Use cached search data instead of live DOM queries + const searchCache = item._wtSearchCache; + if (!searchCache) { + console.warn( + "Search cache missing for item, falling back to live query", + ); + // Fallback to original method if cache is missing + const categoryElement = item.querySelector( + `[wt-cmsfilter-category="${category}"]`, + ); + let matchingText = ""; + if (categoryElement && categoryElement.innerText) { + matchingText = categoryElement.innerText.toLowerCase(); + } + matchingText = matchingText.replace(/(?: |\s)+/gi, " "); } - - return links; - } - async LoadAllItems() { - if (!this.paginationWrapper) return; - this.itemsPerPage = this.allItems.length; - - const paginationPages = this.paginationWrapper.querySelector('.w-page-count'); - const baseLink = this.paginationWrapper.querySelector('a'); - const links = this.generatePaginationLinksFromString(paginationPages.innerText, baseLink.href); - if (!links || links.length === 0) return; - - const itemsBeforeLoad = this.allItems.length; - - for (const link of links) { - try { - const htmlDoc = await this.FetchHTML(link); - if (htmlDoc) { - const cards = Array.from(htmlDoc.querySelector('[wt-cmsfilter-element="list"]')?.children || []); - - if (cards.length > 0) { - for (const card of cards) { - if (card instanceof Node) { // Ensure it's a valid DOM node - this.allItems.push(card); - } else { - console.warn('Non-DOM element skipped:', card); - } - } - } - } else { - console.error('Failed to fetch HTML from the URL:', link.href); + if (category === "*") { + // Global search using cached text + const globalText = searchCache ? searchCache.globalSearchText : ""; + return ( + values.some((value) => globalText.includes(value.toLowerCase())) || + Object.values(item.dataset || {}).some((dataValue) => + values.some((value) => { + if (dataValue && typeof dataValue.toLowerCase === "function") { + return dataValue.toLowerCase().includes(value.toLowerCase()); } - } catch (error) { - console.error('Error fetching HTML:', error); + return false; + }), + ) + ); + } else { + return values.some((value) => { + if (typeof value === "object" && value !== null) { + // Range filtering - use normalized dataset key + const datasetCategory = this.GetDataSet(category); + const datasetValue = + item.dataset && item.dataset[datasetCategory] + ? item.dataset[datasetCategory] + : ""; + const itemValue = parseFloat(datasetValue); + if (isNaN(itemValue)) return false; + if (value.from !== null && value.to !== null) { + return itemValue >= value.from && itemValue <= value.to; + } else if (value.from !== null && value.to == null) { + return itemValue >= value.from; + } else if (value.from == null && value.to !== null) { + return itemValue <= value.to; + } + return false; + } else { + // Text filtering using cached data + const datasetCategory = this.GetDataSet(category); + const cachedDatasetValue = searchCache + ? searchCache.datasetValues.get(datasetCategory) || "" + : ""; + const cachedCategoryText = searchCache + ? searchCache.categoryTexts.get(category) || "" + : ""; + const valueStr = value ? value.toString().toLowerCase() : ""; + + return ( + cachedDatasetValue.includes(valueStr) || + cachedCategoryText.includes(valueStr) + ); } + }); } - - // Cache search data for newly loaded items only - if (this.allItems.length > itemsBeforeLoad) { - const newItems = this.allItems.slice(itemsBeforeLoad); - newItems.forEach(item => { - // Cache search data for new item - if (!item._wtSearchCache) { - this.cacheItemForSearch(item); + }); + }); + + this.activeFilters = filters; + this.SortItems(); + this.RenderItems(); + this.UpdateAvailableFilters(); + this.ShowResultCount(); + this.SetActiveTags(); + } + + ShowResultCount() { + if (!this.resultCount) return; + this.resultCount.innerText = this.GetResults(); + } + + GetFilters() { + const filters = {}; + const rangeFilters = {}; + + this.filterElements.forEach((element) => { + const category = element.getAttribute("wt-cmsfilter-category"); + + if (!filters[category]) { + filters[category] = []; + } + + const input = + element.tagName === "INPUT" + ? element + : element.querySelector( + 'input[type="checkbox"], input[type="radio"], input[type="text"]', + ); + + if (input) { + if (input.type === "text") { + const rangeType = element.getAttribute("wt-cmsfilter-range"); + if (rangeType === "from" || rangeType === "to") { + if (!rangeFilters[category]) { + rangeFilters[category] = { from: null, to: null }; + } + + const value = parseFloat(input.value.trim()); + if (Number.isFinite(value)) { + const datasetCategory = this.GetDataSet(category); + const ranges = this.dataRanges + ? this.dataRanges[datasetCategory] + : null; + // Determine default for comparison without mutating attributes here + let numericDefault = parseFloat( + input.getAttribute("wt-cmsfilter-default"), + ); + if (!Number.isFinite(numericDefault) && ranges) { + numericDefault = rangeType === "from" ? ranges.min : ranges.max; + } + + if (Number.isFinite(numericDefault)) { + if (rangeType === "from" && value !== numericDefault) { + rangeFilters[category].from = value; + } else if (rangeType === "to" && value !== numericDefault) { + rangeFilters[category].to = value; } - }); + } + } else { + rangeFilters[category][rangeType] = null; + } + } else if (input.value.trim() !== "") { + filters[category].push(input.value.trim()); + } else { + filters[category] = []; + } + } else if (input.checked) { + filters[category].push(input.nextElementSibling.textContent.trim()); + if (this.activeFilterClass) { + element.classList.add(this.activeFilterClass); + } + } else { + if (this.activeFilterClass) { + element.classList.remove(this.activeFilterClass); + } + } + } + }); + + Object.keys(rangeFilters).forEach((category) => { + const range = rangeFilters[category]; + if (range.from !== null && range.to !== null) { + filters[category].push({ from: range.from, to: range.to }); + } else if (range.from !== null && range.to == null) { + filters[category].push({ from: range.from, to: null }); + } else if (range.from == null && range.to !== null) { + filters[category].push({ from: null, to: range.to }); + } + }); + + return filters; + } + + GetDataSet(str) { + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) { + return index === 0 ? word.toLowerCase() : word.toUpperCase(); + }) + .replace(/\s+/g, "") + .replace("-", ""); + } + + /** + * Captures original display styles for filter elements + * Called once during initialization to preserve original CSS + */ + captureOriginalDisplayStyles() { + this.filterElements.forEach((element) => { + const istoggle = element.querySelector( + 'input[type="checkbox"], input[type="radio"]', + ); + if (istoggle) { + // Get computed style to capture the actual display value (flex, block, etc.) + const computedStyle = window.getComputedStyle(element); + const originalDisplay = computedStyle.display; + this.originalDisplayStyles.set(element, originalDisplay); + } + }); + } + + UpdateAvailableFilters() { + if (this.filterForm.getAttribute("wt-cmsfilter-filtering") !== "advanced") + return; + this.availableFilters = {}; + + this.filterElements.forEach((element) => { + const category = this.GetDataSet( + element.getAttribute("wt-cmsfilter-category"), + ); + + // Safari-compatible dataset access + const availableValues = new Set( + this.filteredItems + .map((item) => + item.dataset && item.dataset[category] + ? item.dataset[category] + : "", + ) + .filter((value) => value !== ""), + ); + this.availableFilters[category] = availableValues; + + const istoggle = element.querySelector( + 'input[type="checkbox"], input[type="radio"]', + ); + if (istoggle) { + // Safari-compatible text extraction and comparison + let elementText = ""; + if (element.textContent) { + elementText = element.textContent.trim(); + } else if (element.innerText) { + elementText = element.innerText.trim(); } - } - async FetchHTML(url) { - const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); - const text = await response.text(); - const parser = new DOMParser(); - return parser.parseFromString(text, 'text/html'); - } + // Normalize whitespace for Safari compatibility + elementText = elementText.replace(/\s+/g, " "); - FiltersApplied() { - return Object.values(this.activeFilters).some(arr => Array.isArray(arr) && arr.length > 0); - } + // Safari-compatible Set.has() check + let isAvailable = false; + availableValues.forEach((value) => { + const normalizedValue = value.toString().replace(/\s+/g, " ").trim(); + if (normalizedValue === elementText) { + isAvailable = true; + } + }); - RenderItems() { - this.listElement.innerHTML = ''; - if(this.filteredItems.length === 0) { - if(!this.FiltersApplied()) { - this.filteredItems = this.allItems; - } - } - if(this.paginationWrapper) { - if(this.loadMode === 'load-all') { - this.filteredItems.forEach(item => { - this.listElement.appendChild(item); - }); - if(this.paginationWrapper){ - this.paginationWrapper.remove(); - } - } else if (this.loadMode === 'paginate') { - this.totalPages = Math.ceil(this.filteredItems.length / this.itemsPerPage); - const currentSlice = (this.currentPage * this.itemsPerPage) - this.itemsPerPage; - const currentPage = this.filteredItems.slice(currentSlice, currentSlice + this.itemsPerPage); - currentPage.forEach(item => { - this.listElement.appendChild(item); - if(this.resetIx2) this.ResetInteraction(item); - }); - } + // Restore original display style or hide + if (isAvailable) { + // Restore original display style + const originalDisplay = this.originalDisplayStyles.get(element); + if (originalDisplay && originalDisplay !== "none") { + element.style.display = originalDisplay; + } else { + // Fallback: remove display override to use CSS default + element.style.display = ""; + } + element.style.visibility = "visible"; } else { - this.filteredItems.forEach(item => { - this.listElement.appendChild(item); - if(this.resetIx2) this.ResetInteraction(item); - }); + element.style.display = "none"; + element.style.visibility = "hidden"; } - - this.ToggleEmptyState(); - this.UpdatePaginationDisplay(); + } + }); + } + + ToggleEmptyState() { + if (this.emptyElement) { + if (this.filteredItems.length <= this.emptyMaxCount) { + this.emptyElement.style.display = "block"; + } else { + this.emptyElement.style.display = "none"; + } } + } + + InitializeTagTemplate() { + if (!this.tagTemplate) return; + this.tagTemplateContainer.innerHTML = ""; + } + + SetActiveTags() { + if (!this.tagTemplateContainer) return; + this.InitializeTagTemplate(); + + const filterTags = Object.keys(this.activeFilters); + filterTags.forEach((tag) => { + if (this.activeFilters[tag].length !== 0) { + this.activeFilters[tag].forEach((filterValue) => { + const newTag = this.tagTemplate.cloneNode(true); + const tagText = newTag.querySelector( + '[wt-cmsfilter-element="tag-text"]', + ); + const showTagCategory = + newTag.getAttribute("wt-cmsfilter-tag-category") || "true"; + const tagRemove = newTag.querySelector( + '[wt-cmsfilter-element="tag-remove"]', + ); + + if ( + typeof filterValue === "object" && + filterValue.from !== null && + filterValue.to !== null + ) { + tagText.innerText = `${showTagCategory === "true" ? `${tag}:` : ""} ${filterValue?.from} - ${filterValue?.to}`; + } else if ( + typeof filterValue === "object" && + filterValue.from !== null && + filterValue.to === null + ) { + tagText.innerText = `${showTagCategory === "true" ? `${tag}:` : ""} ${filterValue?.from}`; + } else if ( + typeof filterValue === "object" && + filterValue.from === null && + filterValue.to !== null + ) { + tagText.innerText = `${showTagCategory === "true" ? `${tag}:` : ""} ${filterValue?.to}`; + } else { + tagText.innerText = `${showTagCategory === "true" ? `${tag}:` : ""} ${filterValue}`; + } + this.tagTemplateContainer.append(newTag); + + // Bind the remove event listener + tagRemove.addEventListener("click", (event) => { + event.preventDefault(); + this.RemoveActiveTag(newTag, tag, filterValue); + }); + }); + } + }); + } + + RemoveActiveTag(_tag, filterTag, value) { + const categoryElements = this.filterForm.querySelectorAll( + `[wt-cmsfilter-category="${filterTag}"]`, + ); + const advancedFiltering = this.filterForm.getAttribute( + "wt-cmsfilter-filtering", + ); + categoryElements.forEach((categoryElement) => { + const input = + categoryElement.tagName === "INPUT" + ? categoryElement + : categoryElement.querySelector( + 'input[type="checkbox"], input[type="radio"], input[type="text"]', + ); - SortItems() { - if (!this.sortOptions) return; - - let [key, order] = this.sortOptions.value.split('-'); - this.filteredItems = this.filteredItems.filter(item => !item.hasAttribute('wt-renderstatic-element')); - this.filteredItems.sort((a, b) => { - let aValue = a.dataset[key]; - let bValue = b.dataset[key]; - - // Handle null or undefined values - if (aValue === undefined || aValue === null) aValue = ''; - if (bValue === undefined || bValue === null) bValue = ''; - - // Handle numeric values - if (!isNaN(aValue) && !isNaN(bValue)) { - aValue = parseFloat(aValue); - bValue = parseFloat(bValue); + if (input) { + if (input.type === "text") { + if (input.hasAttribute("wt-cmsfilter-default")) { + input.value = input.getAttribute("wt-cmsfilter-default"); + } else { + input.value = ""; + } + } else if (input.type === "checkbox") { + if (advancedFiltering === "advanced") { + input.checked = false; + } else { + if (categoryElement.innerText === value) { + input.checked = false; } - // Handle date values - else if (!isNaN(Date.parse(aValue)) && !isNaN(Date.parse(bValue))) { - aValue = new Date(aValue); - bValue = new Date(bValue); - } - // Handle text values - else { - aValue = aValue.toString().toLowerCase(); - bValue = bValue.toString().toLowerCase(); - } - - if (order === 'asc') { - return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; - } else { - return aValue < bValue ? 1 : aValue > bValue ? -1 : 0; - } - }); - } - - ApplyFilters() { - const filters = this.GetFilters(); - this.currentPage = 1; // Reset pagination to first page - this.filteredItems = this.allItems.filter(item => { - return Object.keys(filters).every(category => { - // Fix 1: Safari-compatible array handling - const categoryFilters = filters[category] || []; - const values = Array.isArray(categoryFilters) ? categoryFilters.slice() : []; - if (values.length === 0) return true; - - // Use cached search data instead of live DOM queries - const searchCache = item._wtSearchCache; - if (!searchCache) { - console.warn('Search cache missing for item, falling back to live query'); - // Fallback to original method if cache is missing - const categoryElement = item.querySelector(`[wt-cmsfilter-category="${category}"]`); - let matchingText = ''; - if (categoryElement && categoryElement.innerText) { - matchingText = categoryElement.innerText.toLowerCase(); - } - matchingText = matchingText.replace(/(?: |\s)+/gi, ' '); - } + } + } + } + }); - if (category === '*') { - // Global search using cached text - const globalText = searchCache ? searchCache.globalSearchText : ''; - return values.some(value => globalText.includes(value.toLowerCase())) || - Object.values(item.dataset || {}).some(dataValue => - values.some(value => { - if (dataValue && typeof dataValue.toLowerCase === 'function') { - return dataValue.toLowerCase().includes(value.toLowerCase()); - } - return false; - }) - ); - } else { - return values.some(value => { - if (typeof value === 'object' && value !== null) { - // Range filtering - use normalized dataset key - const datasetCategory = this.GetDataSet(category); - const datasetValue = (item.dataset && item.dataset[datasetCategory]) ? item.dataset[datasetCategory] : ''; - const itemValue = parseFloat(datasetValue); - if (isNaN(itemValue)) return false; - if (value.from !== null && value.to !== null) { - return itemValue >= value.from && itemValue <= value.to; - } else if (value.from !== null && value.to == null) { - return itemValue >= value.from; - } else if (value.from == null && value.to !== null) { - return itemValue <= value.to; - } - return false; - } else { - // Text filtering using cached data - const datasetCategory = this.GetDataSet(category); - const cachedDatasetValue = searchCache ? searchCache.datasetValues.get(datasetCategory) || '' : ''; - const cachedCategoryText = searchCache ? searchCache.categoryTexts.get(category) || '' : ''; - const valueStr = value ? value.toString().toLowerCase() : ''; - - return cachedDatasetValue.includes(valueStr) || cachedCategoryText.includes(valueStr); - } - }); - } - }); - }); + this.activeFilters[filterTag] = this.activeFilters[filterTag].filter( + (filter) => filter !== value, + ); - this.activeFilters = filters; - this.SortItems(); - this.RenderItems(); - this.UpdateAvailableFilters(); - this.ShowResultCount(); - this.SetActiveTags(); - } + _tag.remove(); - ShowResultCount() { - if(!this.resultCount) return; - this.resultCount.innerText = this.GetResults(); - } + this.ApplyFilters(); + } - GetFilters() { - const filters = {}; - const rangeFilters = {}; - - this.filterElements.forEach(element => { - const category = element.getAttribute('wt-cmsfilter-category'); - - if (!filters[category]) { - filters[category] = []; - } - - const input = (element.tagName === "INPUT") ? element : element.querySelector('input[type="checkbox"], input[type="radio"], input[type="text"]'); - - if (input) { - if (input.type === 'text') { - const rangeType = element.getAttribute('wt-cmsfilter-range'); - if (rangeType === 'from' || rangeType === 'to') { - if (!rangeFilters[category]) { - rangeFilters[category] = { from: null, to: null }; - } - - const value = parseFloat(input.value.trim()); - if (Number.isFinite(value)) { - const datasetCategory = this.GetDataSet(category); - const ranges = this.dataRanges ? this.dataRanges[datasetCategory] : null; - // Determine default for comparison without mutating attributes here - let numericDefault = parseFloat(input.getAttribute('wt-cmsfilter-default')); - if (!Number.isFinite(numericDefault) && ranges) { - numericDefault = rangeType === 'from' ? ranges.min : ranges.max; - } - - if (Number.isFinite(numericDefault)) { - if (rangeType === 'from' && value !== numericDefault) { - rangeFilters[category].from = value; - } else if (rangeType === 'to' && value !== numericDefault) { - rangeFilters[category].to = value; - } - } - } else { - rangeFilters[category][rangeType] = null; - } - } else if (input.value.trim() !== '') { - filters[category].push(input.value.trim()); - } else { - filters[category] = []; - } - } else if (input.checked) { - filters[category].push(input.nextElementSibling.textContent.trim()); - if (this.activeFilterClass) { - element.classList.add(this.activeFilterClass); - } - } else { - if (this.activeFilterClass) { - element.classList.remove(this.activeFilterClass); - } - } - } - }); - - Object.keys(rangeFilters).forEach(category => { - const range = rangeFilters[category]; - if (range.from !== null && range.to !== null) { - filters[category].push({ from: range.from, to: range.to }); - } - else if (range.from !== null && range.to == null) { - filters[category].push({ from: range.from, to: null }); - } - else if (range.from == null && range.to !== null) { - filters[category].push({ from: null, to: range.to }); - } - }); - - return filters; - } - - GetDataSet(str) { - return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(word, index) { - return index === 0 ? word.toLowerCase() : word.toUpperCase(); - }).replace(/\s+/g, '').replace('-', ''); + NextPage() { + if (this.currentPage <= this.totalPages) { + this.currentPage = this.currentPage + 1; + this.RenderItems(); } + } - /** - * Captures original display styles for filter elements - * Called once during initialization to preserve original CSS - */ - captureOriginalDisplayStyles() { - this.filterElements.forEach(element => { - const istoggle = element.querySelector('input[type="checkbox"], input[type="radio"]'); - if (istoggle) { - // Get computed style to capture the actual display value (flex, block, etc.) - const computedStyle = window.getComputedStyle(element); - const originalDisplay = computedStyle.display; - this.originalDisplayStyles.set(element, originalDisplay); - } - }); + PrevPage() { + if (this.currentPage > 1) { + this.currentPage = this.currentPage - 1; + this.RenderItems(); } + } - UpdateAvailableFilters() { - if (this.filterForm.getAttribute('wt-cmsfilter-filtering') !== 'advanced') return; - this.availableFilters = {}; - - this.filterElements.forEach(element => { - const category = this.GetDataSet(element.getAttribute('wt-cmsfilter-category')); - - // Safari-compatible dataset access - const availableValues = new Set( - this.filteredItems - .map(item => (item.dataset && item.dataset[category]) ? item.dataset[category] : '') - .filter(value => value !== "") - ); - this.availableFilters[category] = availableValues; - - const istoggle = element.querySelector('input[type="checkbox"], input[type="radio"]'); - if (istoggle) { - // Safari-compatible text extraction and comparison - let elementText = ''; - if (element.textContent) { - elementText = element.textContent.trim(); - } else if (element.innerText) { - elementText = element.innerText.trim(); - } - - // Normalize whitespace for Safari compatibility - elementText = elementText.replace(/\s+/g, ' '); - - // Safari-compatible Set.has() check - let isAvailable = false; - availableValues.forEach(value => { - const normalizedValue = value.toString().replace(/\s+/g, ' ').trim(); - if (normalizedValue === elementText) { - isAvailable = true; - } - }); - - // Restore original display style or hide - if (isAvailable) { - // Restore original display style - const originalDisplay = this.originalDisplayStyles.get(element); - if (originalDisplay && originalDisplay !== 'none') { - element.style.display = originalDisplay; - } else { - // Fallback: remove display override to use CSS default - element.style.display = ''; - } - element.style.visibility = 'visible'; - } else { - element.style.display = 'none'; - element.style.visibility = 'hidden'; - } - } - }); - } + UpdatePaginationDisplay() { + if (!this.paginationWrapper) return; - ToggleEmptyState() { - if (this.emptyElement) { - if (this.filteredItems.length === 0) { - this.emptyElement.style.display = 'block'; - } else { - this.emptyElement.style.display = 'none'; - } - } + if (this.paginationcounter) { + this.paginationcounter.innerText = `${this.currentPage} / ${this.totalPages}`; } - - InitializeTagTemplate() { - if(!this.tagTemplate) return; - this.tagTemplateContainer.innerHTML = ""; + if (this.currentPage === 1) { + if (this.previousButton) this.previousButton.hidden = true; + if (this.customPrevButton) this.customPrevButton.hidden = true; + } else { + if (this.previousButton) this.previousButton.hidden = false; + if (this.customPrevButton) this.customPrevButton.hidden = false; } - - SetActiveTags() { - if(!this.tagTemplateContainer) return; - this.InitializeTagTemplate(); - - const filterTags = Object.keys(this.activeFilters); - filterTags.forEach(tag => { - if (this.activeFilters[tag].length !== 0) { - this.activeFilters[tag].forEach(filterValue => { - const newTag = this.tagTemplate.cloneNode(true); - const tagText = newTag.querySelector('[wt-cmsfilter-element="tag-text"]'); - const showTagCategory = newTag.getAttribute('wt-cmsfilter-tag-category') || 'true'; - const tagRemove = newTag.querySelector('[wt-cmsfilter-element="tag-remove"]'); - - if (typeof filterValue === 'object' && filterValue.from !== null && filterValue.to !== null) { - tagText.innerText = `${showTagCategory === 'true' ? `${tag}:` : ''} ${filterValue?.from} - ${filterValue?.to}`; - } - else if (typeof filterValue === 'object' && filterValue.from !== null && filterValue.to === null ) { - tagText.innerText = `${showTagCategory === 'true' ? `${tag}:` : ''} ${filterValue?.from}`; - } - else if (typeof filterValue === 'object' && filterValue.from === null && filterValue.to !== null ) { - tagText.innerText = `${showTagCategory === 'true' ? `${tag}:` : ''} ${filterValue?.to}`; - } - else{ - tagText.innerText = `${showTagCategory === 'true' ? `${tag}:` : ''} ${filterValue}`; - } - this.tagTemplateContainer.append(newTag); - - // Bind the remove event listener - tagRemove.addEventListener('click', (event) => { - event.preventDefault(); - this.RemoveActiveTag(newTag, tag, filterValue); - }); - }); - } - }); + if (this.currentPage === this.totalPages) { + if (this.nextButton) this.nextButton.hidden = true; + if (this.customNextButton) this.customNextButton.hidden = true; + } else { + if (this.nextButton) this.nextButton.hidden = false; + if (this.customNextButton) this.customNextButton.hidden = false; } - - RemoveActiveTag(_tag, filterTag, value) { - const categoryElements = this.filterForm.querySelectorAll(`[wt-cmsfilter-category="${filterTag}"]`); - const advancedFiltering = this.filterForm.getAttribute('wt-cmsfilter-filtering'); - categoryElements.forEach(categoryElement => { - const input = (categoryElement.tagName === "INPUT") - ? categoryElement - : categoryElement.querySelector('input[type="checkbox"], input[type="radio"], input[type="text"]'); - - if (input) { - if (input.type === 'text') { - if(input.hasAttribute('wt-cmsfilter-default')) { - input.value = input.getAttribute('wt-cmsfilter-default'); - } - else { - input.value = ''; - } - } else if (input.type === 'checkbox') { - if(advancedFiltering === 'advanced') { - input.checked = false; - } - else { - if(categoryElement.innerText === value) { - input.checked = false; - } - } - } - } - }); - - this.activeFilters[filterTag] = this.activeFilters[filterTag].filter(filter => filter !== value); - - _tag.remove(); - - this.ApplyFilters(); + } + + GetResults() { + if (this.activeFilters) { + let currActive = Object.values(this.activeFilters).filter( + (filter) => filter.length > 0, + ); + if (currActive.length > 0) { + return this.filteredItems.length; + } } - - NextPage() { - if (this.currentPage <= this.totalPages) { - this.currentPage = this.currentPage + 1; - this.RenderItems(); - } + if (this.allItems) { + //trim out static elements from RenderStatic + let elements = this.allItems.filter( + (item) => !item.hasAttribute("wt-renderstatic-element"), + ); + if (elements.length > 0) { + return elements.length; + } + return 0; } + return 0; + } + + ClearAllFilters() { + this.filterElements.forEach((element) => { + const input = + element.tagName === "INPUT" + ? element + : element.querySelector( + 'input[type="checkbox"], input[type="radio"], input[type="text"]', + ); - PrevPage() { - if (this.currentPage > 1) { - this.currentPage = this.currentPage - 1; - this.RenderItems(); + if (input) { + if (input.type === "text") { + if (input.hasAttribute("wt-cmsfilter-default")) { + input.value = input.getAttribute("wt-cmsfilter-default"); + } else { + input.value = ""; + } + } else if (input.type === "checkbox") { + input.checked = false; } - } + } - UpdatePaginationDisplay() { - if(!this.paginationWrapper) return; + if (this.activeFilterClass) { + element.classList.remove(this.activeFilterClass); + } + }); - if (this.paginationcounter) { - this.paginationcounter.innerText = `${this.currentPage} / ${this.totalPages}`; - } - if(this.currentPage === 1){ - if(this.previousButton) this.previousButton.hidden = true; - if(this.customPrevButton) this.customPrevButton.hidden = true; - } else { - if(this.previousButton) this.previousButton.hidden = false; - if(this.customPrevButton) this.customPrevButton.hidden = false; - } - if(this.currentPage === this.totalPages){ - if(this.nextButton) this.nextButton.hidden = true; - if(this.customNextButton) this.customNextButton.hidden = true; - } else { - if(this.nextButton) this.nextButton.hidden = false; - if(this.customNextButton) this.customNextButton.hidden = false; - } - } + this.activeFilters = {}; - GetResults() { - if(this.activeFilters){ - let currActive = Object.values(this.activeFilters).filter(filter => filter.length > 0); - if(currActive.length > 0){ - return this.filteredItems.length; - } - } - if(this.allItems){ - //trim out static elements from RenderStatic - let elements = this.allItems.filter(item => !item.hasAttribute('wt-renderstatic-element')); - if(elements.length > 0) { - return elements.length; - } - return 0; - } - return 0; + if (this.tagTemplateContainer) { + this.tagTemplateContainer.innerHTML = ""; } - ClearAllFilters() { - this.filterElements.forEach(element => { - const input = (element.tagName === "INPUT") - ? element - : element.querySelector('input[type="checkbox"], input[type="radio"], input[type="text"]'); - - if (input) { - if (input.type === 'text') { - if(input.hasAttribute('wt-cmsfilter-default')) { - input.value = input.getAttribute('wt-cmsfilter-default'); - } - else { - input.value = ''; - } - } else if (input.type === 'checkbox') { - input.checked = false; - } - } - - if (this.activeFilterClass) { - element.classList.remove(this.activeFilterClass); - } - }); - - this.activeFilters = {}; - - if (this.tagTemplateContainer) { - this.tagTemplateContainer.innerHTML = ""; - } - - this.ApplyFilters(); - } + this.ApplyFilters(); + } - ResetInteraction(element) { - if (!element) { - console.error('Element not found'); - return; - } - - const WebflowIX2 = window.Webflow && Webflow.require('ix2'); - if (!WebflowIX2) { - console.error('Webflow IX2 engine not found.'); - return; - } + ResetInteraction(element) { + if (!element) { + console.error("Element not found"); + return; + } - const targetElement = element.hasAttribute('data-w-id') - ? element - : element.querySelector('[data-w-id]'); - - if (!targetElement) { - console.warn('No IX2 interaction found on the element or its children.'); - return; - } + const WebflowIX2 = window.Webflow && Webflow.require("ix2"); + if (!WebflowIX2) { + console.error("Webflow IX2 engine not found."); + return; + } - const dataWId = targetElement.getAttribute('data-w-id'); - if (dataWId) { - targetElement.removeAttribute('data-w-id'); - targetElement.setAttribute('data-w-id', dataWId); + const targetElement = element.hasAttribute("data-w-id") + ? element + : element.querySelector("[data-w-id]"); - WebflowIX2.init(); - } else { - console.warn('No valid data-w-id attribute found.'); - } + if (!targetElement) { + console.warn("No IX2 interaction found on the element or its children."); + return; } - GetFilterData() { - let filterData = { - 'filters': this.filterElements, - 'active': this.activeFilters, - 'available': this.availableFilters, - 'results': this.GetResults(), - 'per-page-items': this.itemsPerPage, - 'total-pages': this.totalPages, - 'current-page': this.currentPage, - 'all-items': this.allItems, - 'filtered-items': this.filteredItems, - 'load-mode': this.loadMode, - 'range-sliders': this.dataRanges - } - return filterData; - } + const dataWId = targetElement.getAttribute("data-w-id"); + if (dataWId) { + targetElement.removeAttribute("data-w-id"); + targetElement.setAttribute("data-w-id", dataWId); - // Utility method for debouncing function calls - debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func.apply(this, args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; + WebflowIX2.init(); + } else { + console.warn("No valid data-w-id attribute found."); } + } + + GetFilterData() { + let filterData = { + filters: this.filterElements, + active: this.activeFilters, + available: this.availableFilters, + results: this.GetResults(), + "per-page-items": this.itemsPerPage, + "total-pages": this.totalPages, + "current-page": this.currentPage, + "all-items": this.allItems, + "filtered-items": this.filteredItems, + "load-mode": this.loadMode, + "range-sliders": this.dataRanges, + }; + return filterData; + } + + // Utility method for debouncing function calls + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func.apply(this, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } } const InitializeCMSFilter = () => { - window.webtricks = window.webtricks || []; - let instance = new CMSFilter(); - window.webtricks.push({'CMSFilter': instance}); -} + window.webtricks = window.webtricks || []; + let instance = new CMSFilter(); + window.webtricks.push({ CMSFilter: instance }); +}; if (/complete|interactive|loaded/.test(document.readyState)) { - InitializeCMSFilter(); -} else { - window.addEventListener('DOMContentLoaded', InitializeCMSFilter) + InitializeCMSFilter(); +} else { + window.addEventListener("DOMContentLoaded", InitializeCMSFilter); } // Allow requiring this module in test environments without affecting browser usage try { - if (typeof module !== 'undefined' && module.exports) { - module.exports = { CMSFilter, InitializeCMSFilter }; - } -} catch {} \ No newline at end of file + if (typeof module !== "undefined" && module.exports) { + module.exports = { CMSFilter, InitializeCMSFilter }; + } +} catch {} diff --git a/__tests__/CMSFilter.test.js b/__tests__/CMSFilter.test.js index 1d617d4..52ac3c0 100644 --- a/__tests__/CMSFilter.test.js +++ b/__tests__/CMSFilter.test.js @@ -1,30 +1,45 @@ /** @jest-environment jsdom */ // Prevent auto init before we control DOM -Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); +Object.defineProperty(document, "readyState", { + value: "loading", + configurable: true, +}); // requestAnimationFrame polyfill for consistency if (!global.requestAnimationFrame) global.requestAnimationFrame = (cb) => cb(); -describe('CMSFilter', () => { +describe("CMSFilter", () => { let CMSFilter, InitializeCMSFilter; beforeEach(() => { - document.body.innerHTML = ''; + document.body.innerHTML = ""; window.webtricks = []; jest.resetModules(); - ({ CMSFilter, InitializeCMSFilter } = require('../Dist/WebflowOnly/CMSFilter.js')); + ({ + CMSFilter, + InitializeCMSFilter, + } = require("../Dist/WebflowOnly/CMSFilter.js")); }); - function buildBasicDOM({ withPagination=false, loadMode='load-all', advanced=false }={}) { - const paginationMarkup = withPagination ? ` + function buildBasicDOM({ + withPagination = false, + loadMode = "load-all", + advanced = false, + emptyMax, + } = {}) { + const paginationMarkup = withPagination + ? `
Page1
1 / 1
Prev Next -
` : ''; - const advancedAttr = advanced ? 'wt-cmsfilter-filtering="advanced" wt-cmsfilter-class="is-active"' : ''; + ` + : ""; + const advancedAttr = advanced + ? 'wt-cmsfilter-filtering="advanced" wt-cmsfilter-class="is-active"' + : ""; document.body.innerHTML = `
@@ -53,73 +68,90 @@ describe('CMSFilter', () => {
Beta Item
Gamma Item
-
No results
+
No results
${paginationMarkup} `; } - test('initializes and caches items, pushes instance', () => { + test("initializes and caches items, pushes instance", () => { buildBasicDOM(); InitializeCMSFilter(); - const instance = window.webtricks.find(e => e.CMSFilter).CMSFilter; + const instance = window.webtricks.find((e) => e.CMSFilter).CMSFilter; // resultCount may be updated after init sequence; ensure fallback to computing directly - const countText = instance.resultCount.textContent || String(instance.filteredItems.length); + const countText = + instance.resultCount.textContent || String(instance.filteredItems.length); expect(instance.allItems.length).toBe(3); expect(instance.filteredItems.length).toBe(3); - expect(countText).toBe('3'); + expect(countText).toBe("3"); }); - test('category checkbox filter reduces items and shows result count', () => { + test("category checkbox filter reduces items and shows result count", () => { buildBasicDOM(); InitializeCMSFilter(); const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); // Check Beta only - const betaLabel = Array.from(form.querySelectorAll('label')).find(l => l.textContent.includes('Beta')); - betaLabel.querySelector('input').checked = true; - betaLabel.querySelector('input').dispatchEvent(new Event('change', { bubbles: true })); + const betaLabel = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Beta"), + ); + betaLabel.querySelector("input").checked = true; + betaLabel + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); const instance = window.webtricks[0].CMSFilter; // Manually apply filters to bypass debounce timing instance.ApplyFilters(); expect(instance.filteredItems.length).toBe(1); - expect(instance.filteredItems[0].dataset.title).toBe('beta'); - expect(instance.resultCount.textContent).toBe('1'); + expect(instance.filteredItems[0].dataset.title).toBe("beta"); + expect(instance.resultCount.textContent).toBe("1"); }); - test('global search via * category filters list items', () => { + test("global search via * category filters list items", () => { buildBasicDOM(); InitializeCMSFilter(); - const searchInput = document.querySelector('[wt-cmsfilter-category="*"] input'); - searchInput.value = 'gamma'; - searchInput.dispatchEvent(new Event('input', { bubbles: true })); + const searchInput = document.querySelector( + '[wt-cmsfilter-category="*"] input', + ); + searchInput.value = "gamma"; + searchInput.dispatchEvent(new Event("input", { bubbles: true })); const instance = window.webtricks[0].CMSFilter; instance.ApplyFilters(); expect(instance.filteredItems.length).toBe(1); - expect(instance.filteredItems[0].textContent.toLowerCase()).toContain('gamma'); + expect(instance.filteredItems[0].textContent.toLowerCase()).toContain( + "gamma", + ); }); - test('range filtering narrows items between from/to values', () => { + test("range filtering narrows items between from/to values", () => { buildBasicDOM(); InitializeCMSFilter(); - const priceFrom = document.querySelector('[wt-cmsfilter-category="Price"][wt-cmsfilter-range="from"] input'); - const priceTo = document.querySelector('[wt-cmsfilter-category="Price"][wt-cmsfilter-range="to"] input'); + const priceFrom = document.querySelector( + '[wt-cmsfilter-category="Price"][wt-cmsfilter-range="from"] input', + ); + const priceTo = document.querySelector( + '[wt-cmsfilter-category="Price"][wt-cmsfilter-range="to"] input', + ); // After init these should have defaults set (min=10 max=50). Narrow to 20 - 30 - priceFrom.value = '20'; - priceTo.value = '30'; - priceFrom.dispatchEvent(new Event('input', { bubbles: true })); - priceTo.dispatchEvent(new Event('input', { bubbles: true })); + priceFrom.value = "20"; + priceTo.value = "30"; + priceFrom.dispatchEvent(new Event("input", { bubbles: true })); + priceTo.dispatchEvent(new Event("input", { bubbles: true })); const instance = window.webtricks[0].CMSFilter; instance.ApplyFilters(); expect(instance.filteredItems.length).toBe(1); - expect(instance.filteredItems[0].dataset.price).toBe('25'); + expect(instance.filteredItems[0].dataset.price).toBe("25"); }); - test('clear all resets filters and shows all items again', () => { + test("clear all resets filters and shows all items again", () => { buildBasicDOM(); InitializeCMSFilter(); const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); - const alpha = Array.from(form.querySelectorAll('label')).find(l => l.textContent.includes('Alpha')); - alpha.querySelector('input').checked = true; - alpha.querySelector('input').dispatchEvent(new Event('change', { bubbles: true })); + const alpha = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Alpha"), + ); + alpha.querySelector("input").checked = true; + alpha + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); const instance = window.webtricks[0].CMSFilter; instance.ApplyFilters(); expect(instance.filteredItems.length).toBe(1); @@ -127,51 +159,122 @@ describe('CMSFilter', () => { expect(instance.filteredItems.length).toBe(3); }); - test('sort options reorder items (title-desc)', () => { + test("sort options reorder items (title-desc)", () => { buildBasicDOM(); InitializeCMSFilter(); - const select = document.querySelector('[wt-cmsfilter-element="sort-options"]'); - select.value = 'title-desc'; - select.dispatchEvent(new Event('change', { bubbles: true })); + const select = document.querySelector( + '[wt-cmsfilter-element="sort-options"]', + ); + select.value = "title-desc"; + select.dispatchEvent(new Event("change", { bubbles: true })); const instance = window.webtricks[0].CMSFilter; - const ordered = instance.filteredItems.map(i => i.dataset.title); - expect(ordered).toEqual(['gamma','beta','alpha']); + const ordered = instance.filteredItems.map((i) => i.dataset.title); + expect(ordered).toEqual(["gamma", "beta", "alpha"]); }); - test('advanced filtering hides unavailable checkboxes then restores after clearing', () => { - buildBasicDOM({ advanced:true }); + test("advanced filtering hides unavailable checkboxes then restores after clearing", () => { + buildBasicDOM({ advanced: true }); InitializeCMSFilter(); const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); // Apply search that matches only Gamma const searchInput = form.querySelector('[wt-cmsfilter-category="*"] input'); - searchInput.value = 'gamma'; - searchInput.dispatchEvent(new Event('input', { bubbles:true })); + searchInput.value = "gamma"; + searchInput.dispatchEvent(new Event("input", { bubbles: true })); const instance = window.webtricks[0].CMSFilter; instance.ApplyFilters(); // Only Gamma toggle should be visible - const labels = Array.from(form.querySelectorAll('label[wt-cmsfilter-category="Category"]')); - const visible = labels.filter(l => l.style.display !== 'none').map(l => l.textContent.trim()); - expect(visible).toEqual(expect.arrayContaining(['Gamma'])); + const labels = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="Category"]'), + ); + const visible = labels + .filter((l) => l.style.display !== "none") + .map((l) => l.textContent.trim()); + expect(visible).toEqual(expect.arrayContaining(["Gamma"])); expect(visible).toHaveLength(1); // Clear all restores document.querySelector('[wt-cmsfilter-element="clear-all"]').click(); - const restoredVisible = labels.filter(l => l.style.display !== 'none'); + const restoredVisible = labels.filter((l) => l.style.display !== "none"); expect(restoredVisible.length).toBe(3); }); - test('tag template displays active filters and can remove a tag', () => { - buildBasicDOM({ advanced:true }); + test("tag template displays active filters and can remove a tag", () => { + buildBasicDOM({ advanced: true }); InitializeCMSFilter(); const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); - const beta = Array.from(form.querySelectorAll('label')).find(l => l.textContent.includes('Beta')); - beta.querySelector('input').checked = true; - beta.querySelector('input').dispatchEvent(new Event('change', { bubbles:true })); + const beta = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Beta"), + ); + beta.querySelector("input").checked = true; + beta + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); const instance = window.webtricks[0].CMSFilter; instance.ApplyFilters(); const tagsContainer = instance.tagTemplateContainer; expect(tagsContainer.children.length).toBeGreaterThan(0); - const remove = tagsContainer.querySelector('[wt-cmsfilter-element="tag-remove"]'); + const remove = tagsContainer.querySelector( + '[wt-cmsfilter-element="tag-remove"]', + ); remove.click(); expect(instance.filteredItems.length).toBe(3); // back to all }); + + test("empty element shows when filtered result count is less than or equal to wt-cmsfilter-empty-max", () => { + buildBasicDOM({ emptyMax: 1 }); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const betaLabel = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Beta"), + ); + betaLabel.querySelector("input").checked = true; + betaLabel + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + + expect(instance.filteredItems.length).toBe(1); + expect(instance.emptyElement.style.display).toBe("block"); + }); + + test('wt-cmsfilter-element="empty" works by itself', () => { + buildBasicDOM(); + InitializeCMSFilter(); + const instance = window.webtricks[0].CMSFilter; + + expect(instance.emptyElement).toBeTruthy(); + expect(instance.emptyElement.style.display).toBe("none"); + + const searchInput = document.querySelector( + '[wt-cmsfilter-category="*"] input', + ); + searchInput.value = "no-match-value"; + searchInput.dispatchEvent(new Event("input", { bubbles: true })); + instance.ApplyFilters(); + + expect(instance.filteredItems.length).toBe(0); + expect(instance.emptyElement.style.display).toBe("block"); + }); + + test("missing wt-cmsfilter-empty-max defaults to 0", () => { + buildBasicDOM(); + InitializeCMSFilter(); + const instance = window.webtricks[0].CMSFilter; + + expect(instance.emptyMaxCount).toBe(0); + + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const betaLabel = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Beta"), + ); + betaLabel.querySelector("input").checked = true; + betaLabel + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + instance.ApplyFilters(); + + expect(instance.filteredItems.length).toBe(1); + expect(instance.emptyElement.style.display).toBe("none"); + }); }); diff --git a/docs/WebflowOnly/CMSFilter.md b/docs/WebflowOnly/CMSFilter.md index e8d0f92..4fc8b96 100644 --- a/docs/WebflowOnly/CMSFilter.md +++ b/docs/WebflowOnly/CMSFilter.md @@ -54,6 +54,7 @@ Add the script to your Webflow project and include the required attributes on yo #### Additional Elements - `wt-cmsfilter-element="results-count"` - Shows the number of filtered results - `wt-cmsfilter-element="empty"` - Element shown when no results are found +- `wt-cmsfilter-empty-max="n"` - Optional on the empty element; shows empty block when filtered results are ≤ n (default: 0, so only when zero results; e.g. `wt-cmsfilter-empty-max="3"` shows at 3 or fewer) - `wt-cmsfilter-element="clear-all"` - Button to clear all active filters - `wt-cmsfilter-element="sort-options"` - Select element for sorting options - `wt-cmsfilter-element="tag-template"` - Template for active filter tags @@ -111,6 +112,11 @@ Add the script to your Webflow project and include the required attributes on yo
No results found.
+ + +
+ Only a few results found. +
``` From f95a2158eacac63c08b44bdd3cef27cae2f4b7c9 Mon Sep 17 00:00:00 2001 From: Matthew Simpson Date: Wed, 25 Feb 2026 16:36:49 -0800 Subject: [PATCH 36/52] address PR feedback --- Dist/WebflowOnly/CMSFilter.js | 11 +++--- __tests__/CMSFilter.test.js | 63 +++++++++++++++++++++++++++++++++++ docs/WebflowOnly/CMSFilter.md | 2 +- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/Dist/WebflowOnly/CMSFilter.js b/Dist/WebflowOnly/CMSFilter.js index 66bae7c..03f0f1f 100644 --- a/Dist/WebflowOnly/CMSFilter.js +++ b/Dist/WebflowOnly/CMSFilter.js @@ -63,12 +63,11 @@ class CMSFilter { ); this.emptyMaxCount = 0; if (this.emptyElement) { - const emptyMaxValue = parseInt( - this.emptyElement.getAttribute("wt-cmsfilter-empty-max"), - 10, + const emptyMaxAttr = this.emptyElement.getAttribute( + "wt-cmsfilter-empty-max", ); - if (Number.isInteger(emptyMaxValue) && emptyMaxValue >= 0) { - this.emptyMaxCount = emptyMaxValue; + if (emptyMaxAttr !== null && /^[1-9]\d*$/.test(emptyMaxAttr)) { + this.emptyMaxCount = Number(emptyMaxAttr); } } this.resetIx2 = @@ -446,7 +445,7 @@ class CMSFilter { } } } else { - console.error("Failed to fetch HTML from the URL:", link.href); + console.error("Failed to fetch HTML from the URL:", link); } } catch (error) { console.error("Error fetching HTML:", error); diff --git a/__tests__/CMSFilter.test.js b/__tests__/CMSFilter.test.js index 52ac3c0..516ab00 100644 --- a/__tests__/CMSFilter.test.js +++ b/__tests__/CMSFilter.test.js @@ -277,4 +277,67 @@ describe("CMSFilter", () => { expect(instance.filteredItems.length).toBe(1); expect(instance.emptyElement.style.display).toBe("none"); }); + + test("invalid wt-cmsfilter-empty-max defaults to 0", () => { + buildBasicDOM({ emptyMax: "3px" }); + InitializeCMSFilter(); + const instance = window.webtricks[0].CMSFilter; + + expect(instance.emptyMaxCount).toBe(0); + + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const betaLabel = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Beta"), + ); + betaLabel.querySelector("input").checked = true; + betaLabel + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + instance.ApplyFilters(); + + expect(instance.filteredItems.length).toBe(1); + expect(instance.emptyElement.style.display).toBe("none"); + }); + + test("negative wt-cmsfilter-empty-max defaults to 0", () => { + buildBasicDOM({ emptyMax: "-1" }); + InitializeCMSFilter(); + const instance = window.webtricks[0].CMSFilter; + + expect(instance.emptyMaxCount).toBe(0); + + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const betaLabel = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Beta"), + ); + betaLabel.querySelector("input").checked = true; + betaLabel + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + instance.ApplyFilters(); + + expect(instance.filteredItems.length).toBe(1); + expect(instance.emptyElement.style.display).toBe("none"); + }); + + test("non-numeric wt-cmsfilter-empty-max defaults to 0", () => { + buildBasicDOM({ emptyMax: "abc" }); + InitializeCMSFilter(); + const instance = window.webtricks[0].CMSFilter; + + expect(instance.emptyMaxCount).toBe(0); + + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const betaLabel = Array.from(form.querySelectorAll("label")).find((l) => + l.textContent.includes("Beta"), + ); + betaLabel.querySelector("input").checked = true; + betaLabel + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + instance.ApplyFilters(); + + expect(instance.filteredItems.length).toBe(1); + expect(instance.emptyElement.style.display).toBe("none"); + }); }); diff --git a/docs/WebflowOnly/CMSFilter.md b/docs/WebflowOnly/CMSFilter.md index 4fc8b96..071a04a 100644 --- a/docs/WebflowOnly/CMSFilter.md +++ b/docs/WebflowOnly/CMSFilter.md @@ -54,7 +54,7 @@ Add the script to your Webflow project and include the required attributes on yo #### Additional Elements - `wt-cmsfilter-element="results-count"` - Shows the number of filtered results - `wt-cmsfilter-element="empty"` - Element shown when no results are found -- `wt-cmsfilter-empty-max="n"` - Optional on the empty element; shows empty block when filtered results are ≤ n (default: 0, so only when zero results; e.g. `wt-cmsfilter-empty-max="3"` shows at 3 or fewer) +- `wt-cmsfilter-empty-max="n"` - Optional on the empty element; accepts positive whole integers only (`1+`) and shows empty block when filtered results are ≤ n. Missing, `0`, or invalid values default to `0` (show only when there are zero results; e.g. `wt-cmsfilter-empty-max="3"` shows at 3 or fewer). Valid: `1`, `2`, `3`. Invalid: `0`, `3.5`, `3px`, `-1`, `abc`. - `wt-cmsfilter-element="clear-all"` - Button to clear all active filters - `wt-cmsfilter-element="sort-options"` - Select element for sorting options - `wt-cmsfilter-element="tag-template"` - Template for active filter tags From 5d573a9a37cde388430b1e9b3fa12ec5b47ad6a2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 05:47:34 +0000 Subject: [PATCH 37/52] docs(readme): update contributors [skip ci] --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 75f27f8..582f885 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,13 @@ After a pull request has been open for over 30 days with no activity or response JorchCortez +
+ + matthewcsimpson +
+ matthewcsimpson +
+
Likhithakathireddy From 2313a4e8c9d0096803a2bebd9a62179010ab4b2d Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:23:59 -0700 Subject: [PATCH 38/52] feat: rangeslidersimple --- Dist/Functional/RangeSlider.js | 903 ++++++++++++--------------- Dist/Functional/RangeSliderSimple.js | 485 ++++++++++++++ README.md | 15 + __tests__/RangeSlider.test.js | 50 ++ __tests__/RangeSliderSimple.test.js | 58 ++ docs/Functional/CookieConsent.md | 14 +- docs/Functional/CopyToClipboard.md | 15 +- docs/Functional/CountUp.md | 14 +- docs/Functional/DateCountDown.md | 15 +- docs/Functional/FormCheck.md | 14 +- docs/Functional/FormatNumbers.md | 15 +- docs/Functional/Marquee.md | 15 +- docs/Functional/RangeSlider.md | 20 +- docs/Functional/RangeSliderSimple.md | 77 +++ docs/Functional/ReadTime.md | 15 +- docs/Functional/ShareLink.md | 12 +- docs/WebflowOnly/CMSFilter.md | 24 +- docs/WebflowOnly/CMSSelect.md | 19 +- docs/WebflowOnly/HideContainer.md | 13 +- docs/WebflowOnly/MirrorClick.md | 14 +- docs/WebflowOnly/RenderStatic.md | 12 +- docs/WebflowOnly/TabsSlider.md | 12 +- package-lock.json | 4 +- 23 files changed, 1310 insertions(+), 525 deletions(-) create mode 100644 Dist/Functional/RangeSliderSimple.js create mode 100644 __tests__/RangeSlider.test.js create mode 100644 __tests__/RangeSliderSimple.test.js create mode 100644 docs/Functional/RangeSliderSimple.md diff --git a/Dist/Functional/RangeSlider.js b/Dist/Functional/RangeSlider.js index 6d8dd77..664e8e6 100644 --- a/Dist/Functional/RangeSlider.js +++ b/Dist/Functional/RangeSlider.js @@ -5,150 +5,183 @@ * (c) 2023 Jorge Cortez * MIT License * https://github.com/JorchCortez/Weblfow-Trickery + * + * Self-contained: no separate shared script required (backward compatible with single-tag embeds). */ +'use strict'; + +/** @private Core helpers (IIFE keeps globals clean if RangeSliderSimple.js is also on the page) */ +var __WT_RANGE_SLIDER_CORE = (function () { + function validateNumber(value) { + const num = parseFloat(value); + if (isNaN(num)) { + throw new Error(`Invalid number value: ${value}`); + } + return num; + } + + function formatNumber(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + function readSliderConfig(sliderEl, prefix) { + const minAttr = sliderEl.getAttribute(`${prefix}-min`); + const sliderMin = minAttr !== null ? validateNumber(minAttr) : 0; + + const maxAttr = sliderEl.getAttribute(`${prefix}-max`); + const sliderMax = maxAttr !== null ? validateNumber(maxAttr) : 100; + + const stepsAttr = sliderEl.getAttribute(`${prefix}-steps`); + const sliderSteps = stepsAttr !== null ? validateNumber(stepsAttr) : 1; + + const minDiffAttr = sliderEl.getAttribute(`${prefix}-mindifference`); + const minDifference = minDiffAttr !== null ? validateNumber(minDiffAttr) : sliderSteps; + + return { + sliderMin, + sliderMax, + sliderSteps, + minDifference, + rightSuffix: sliderEl.getAttribute(`${prefix}-rightsuffix`) || null, + defaultSuffix: sliderEl.getAttribute(`${prefix}-defaultsuffix`) || null, + shouldFormatNumber: sliderEl.getAttribute(`${prefix}-formatnumber`) || null, + }; + } + + function constrainLeftValue(rawValue, rightValueStr, minDifference) { + return Math.min( + parseInt(rawValue, 10), + parseInt(rightValueStr, 10) - minDifference, + ); + } + + function constrainRightValue(rawValue, leftValueStr, minDifference) { + return Math.max( + parseInt(rawValue, 10), + parseInt(leftValueStr, 10) + minDifference, + ); + } + + function setNativeTextInputValue(input, constrainedValue, suspendBegin, suspendEnd) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + suspendBegin(); + valueProp.set.call(input, constrainedValue); + suspendEnd(); + } + + function formatStartDisplayContent(constrainedValue, shouldFormatNumberAttr) { + const displayLeft = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + return String(displayLeft); + } + + function formatEndDisplayContent( + constrainedValue, + rawInputValue, + sliderMax, + shouldFormatNumberAttr, + rightSuffix, + defaultSuffix, + ) { + let finalDisplay = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + const rawNum = parseFloat(rawInputValue); + if (rightSuffix && !isNaN(rawNum) && rawNum >= sliderMax) { + return `${finalDisplay}${rightSuffix}`; + } + if (defaultSuffix) { + return `${finalDisplay}${defaultSuffix}`; + } + return String(finalDisplay); + } + + function hookInputValueSync(input, handler, isSuspended) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + try { + Object.defineProperty(input, 'value', { + get() { + return valueProp.get.call(this); + }, + set(v) { + valueProp.set.call(this, v); + if (!isSuspended()) { + handler(v); + } + }, + configurable: true, + enumerable: true, + }); + } catch (err) { + // Fallback: rely on 'input'/'change' listeners if defineProperty fails + } + } + + function dispatchInputEvent(element) { + if (element) { + element.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + return { + readSliderConfig, + constrainLeftValue, + constrainRightValue, + setNativeTextInputValue, + formatStartDisplayContent, + formatEndDisplayContent, + hookInputValueSync, + dispatchInputEvent, + }; +}()); + /** * @file RangeSlider.js * @description A customizable dual-handle range slider for selecting value ranges. - * - * Key Features: - * - Dual handles for selecting a range of values - * - Real-time visual updates - * - Form integration with input fields - * - Display elements for current values - * - Customizable thumbs with SVG/image support - * - Mutation observer support for programmatic value changes - * - Configurable min/max values and step size - * - * Required Attributes: - * - wt-rangeslider-element="slider-wrapper": Container element - * - wt-rangeslider-element="slider": Main slider element - * - wt-rangeslider-element="input-left": Left range input - * - wt-rangeslider-element="input-right": Right range input - * - wt-rangeslider-element="thumb-left": Left thumb element - * - wt-rangeslider-element="thumb-right": Right thumb element - * - wt-rangeslider-element="range": Range indicator element - * - * Optional Attributes: - * - wt-rangeslider-min: Minimum value (default: 0) - * - wt-rangeslider-max: Maximum value (default: 100) - * - wt-rangeslider-steps: Step size (default: 1) - * - * Optional Elements: - * - [wt-rangeslider-range="from"]: Form input for start value - * - [wt-rangeslider-range="to"]: Form input for end value - * - [wt-rangeslider-display="from"]: Display element for start value - * - [wt-rangeslider-display="to"]: Display element for end value - * - * @example - * - *
- * - *
0
- *
100
- * - * - * - * - * - * - *
- *
- *
- *
- * - * - *
- *
- * - * @example - * - *
- *
- *
- *
- * - * - * - *
- *
- * - * - * - *
- * - * - *
- *
- * - * Behavior: - * 1. Slider updates in real-time during interaction - * 2. Form inputs update simultaneously with slider movement - * 3. Display elements update in real-time - * 4. Programmatic changes to form inputs trigger slider updates - * 5. Values are constrained within min/max range - * 6. Left value cannot exceed right value minus step size - * 7. Right value cannot be less than left value plus step size + * + * For native-only handles (no custom thumbs), see RangeSliderSimple.js. */ /** * @class RangeSlider - * @classdesc Creates a range slider component with two handles for selecting a range of values. - * The component supports both visual slider interaction and direct input of values. - * - * @param {HTMLElement} wrapper - The container element for the range slider - * @throws {Error} If required elements or attributes are missing + * @param {HTMLElement} wrapper */ class RangeSlider { -/** - * @constructor - * @param {HTMLElement} wrapper - The wrapper element containing all slider components - */ -constructor(wrapper) { - try { - this.wrapper = wrapper; - this.slider = wrapper.querySelector('[wt-rangeslider-element="slider"]'); - // Guard flag to avoid recursive updates when syncing external inputs - this.__suspendExternalSync = false; + constructor(wrapper) { + try { + this.rs = __WT_RANGE_SLIDER_CORE; - if (!this.slider) { - throw new Error('Slider element not found within wrapper'); - } - - // Add required styles - this.addStyles(); - - // Initialize configuration - this.initConfig(); + this.wrapper = wrapper; + this.slider = wrapper.querySelector('[wt-rangeslider-element="slider"]'); + this.__suspendExternalSync = false; - // Initialize elements - this.initElements(); - - // Setup initial state - this.initState(); + if (!this.slider) { + throw new Error('Slider element not found within wrapper'); + } - // Setup event listeners - this.setupEventListeners(); - } catch (err) { - console.error(`RangeSlider initialization failed: ${err.message}`); + this.addStyles(); + this.initConfig(); + this.initElements(); + this.initState(); + this.setupEventListeners(); + } catch (err) { + console.error(`RangeSlider initialization failed: ${err.message}`); + } } -} -/** - * Adds required styles for proper thumb alignment - * @private - */ -addStyles() { - // Inject styles once per document - const existing = document.getElementById('wt-rangeslider-styles'); - if (existing) return; - - const style = document.createElement('style'); - style.id = 'wt-rangeslider-styles'; - style.textContent = ` + addStyles() { + const existing = document.getElementById('wt-rangeslider-styles'); + if (existing) return; + + const style = document.createElement('style'); + style.id = 'wt-rangeslider-styles'; + style.textContent = ` [wt-rangeslider-element="slider"] { position: relative; } @@ -195,430 +228,292 @@ addStyles() { will-change: transform; } `; - document.head.appendChild(style); -} + document.head.appendChild(style); + } -/** - * Initialize slider configuration from attributes - * @private - */ -initConfig() { - const minAttr = this.slider.getAttribute('wt-rangeslider-min'); - this.sliderMin = minAttr !== null ? this.validateNumber(minAttr) : 0; - - const maxAttr = this.slider.getAttribute('wt-rangeslider-max'); - this.sliderMax = maxAttr !== null ? this.validateNumber(maxAttr) : 100; - - const stepsAttr = this.slider.getAttribute('wt-rangeslider-steps'); - this.sliderSteps = stepsAttr !== null ? this.validateNumber(stepsAttr) : 1; - - // Minimum difference between left and right values - const minDiffAttr = this.slider.getAttribute('wt-rangeslider-mindifference'); - this.minDifference = minDiffAttr !== null ? this.validateNumber(minDiffAttr) : this.sliderSteps; - - // Show preffix - this.rightSuffix = - this.slider.getAttribute('wt-rangeslider-rightsuffix') || null; - // Show preffix - this.defaultSuffix = - this.slider.getAttribute('wt-rangeslider-defaultsuffix') || null; - // Show preffix - this.shouldFormatNumber = - this.slider.getAttribute('wt-rangeslider-formatnumber') || null; -} + initConfig() { + const cfg = this.rs.readSliderConfig(this.slider, 'wt-rangeslider'); + this.sliderMin = cfg.sliderMin; + this.sliderMax = cfg.sliderMax; + this.sliderSteps = cfg.sliderSteps; + this.minDifference = cfg.minDifference; + this.rightSuffix = cfg.rightSuffix; + this.defaultSuffix = cfg.defaultSuffix; + this.shouldFormatNumber = cfg.shouldFormatNumber; + } -/** - * Initialize DOM elements - * @private - */ -initElements() { - // Range inputs (form elements) - this.rangeStart = this.wrapper.querySelector( - '[wt-rangeslider-range="from"]', - ); - this.rangeEnd = this.wrapper.querySelector('[wt-rangeslider-range="to"]'); - - // Display elements - this.displayStart = this.wrapper.querySelector( - '[wt-rangeslider-display="from"]', - ); - this.displayEnd = this.wrapper.querySelector( - '[wt-rangeslider-display="to"]', - ); - - // Slider elements - this.inputLeft = this.slider.querySelector( - '[wt-rangeslider-element="input-left"]', - ); - this.inputRight = this.slider.querySelector( - '[wt-rangeslider-element="input-right"]', - ); - this.thumbLeft = this.slider.querySelector( - '[wt-rangeslider-element="thumb-left"]', - ); - this.thumbRight = this.slider.querySelector( - '[wt-rangeslider-element="thumb-right"]', - ); - this.range = this.slider.querySelector('[wt-rangeslider-element="range"]'); - - this.validateRequiredElements(); - this.setupThumbStyles(); -} + initElements() { + this.rangeStart = this.wrapper.querySelector( + '[wt-rangeslider-range="from"]', + ); + this.rangeEnd = this.wrapper.querySelector('[wt-rangeslider-range="to"]'); + + this.displayStart = this.wrapper.querySelector( + '[wt-rangeslider-display="from"]', + ); + this.displayEnd = this.wrapper.querySelector( + '[wt-rangeslider-display="to"]', + ); + + this.inputLeft = this.slider.querySelector( + '[wt-rangeslider-element="input-left"]', + ); + this.inputRight = this.slider.querySelector( + '[wt-rangeslider-element="input-right"]', + ); + this.thumbLeft = this.slider.querySelector( + '[wt-rangeslider-element="thumb-left"]', + ); + this.thumbRight = this.slider.querySelector( + '[wt-rangeslider-element="thumb-right"]', + ); + this.range = this.slider.querySelector('[wt-rangeslider-element="range"]'); + + this.validateRequiredElements(); + this.setupThumbStyles(); + } -/** - * Sets up proper thumb styling to ensure clickable areas align with visuals - * @private - */ -setupThumbStyles() { - const setupThumb = (thumb, input) => { - // Ensure the input's thumb aligns with our custom thumb - const thumbWidth = thumb.offsetWidth || parseInt(getComputedStyle(thumb).width) || 20; - input.style.setProperty('--thumb-width', `${thumbWidth}px`); - - // Apply styles to ensure proper positioning and hit areas - thumb.style.position = 'absolute'; - thumb.style.pointerEvents = 'none'; - - // Create a custom property for the thumb offset - this.slider.style.setProperty('--thumb-offset', `${thumbWidth / 2}px`); - }; + setupThumbStyles() { + const setupThumb = (thumb, input) => { + const thumbWidth = + thumb.offsetWidth || + parseInt(getComputedStyle(thumb).width, 10) || + 20; + input.style.setProperty('--thumb-width', `${thumbWidth}px`); - setupThumb(this.thumbLeft, this.inputLeft); - setupThumb(this.thumbRight, this.inputRight); -} + thumb.style.position = 'absolute'; + thumb.style.pointerEvents = 'none'; -/** - * Initialize slider state - * @private - */ -initState() { - // Configure range inputs - [this.inputLeft, this.inputRight].forEach((input) => { - input.setAttribute('min', this.sliderMin); - input.setAttribute('max', this.sliderMax); - input.setAttribute('step', this.sliderSteps); - input.setAttribute('formnovalidate', ''); - input.setAttribute('data-form-ignore', ''); - }); - - // Set initial values from range inputs if they exist - if (this.rangeStart && this.rangeStart.value) { - this.updateLeftValues(this.rangeStart.value); - } else { - this.updateLeftValues(this.sliderMin); - } + this.slider.style.setProperty('--thumb-offset', `${thumbWidth / 2}px`); + }; - if (this.rangeEnd && this.rangeEnd.value) { - this.updateRightValues(this.rangeEnd.value); - } else { - this.updateRightValues(this.sliderMax); + setupThumb(this.thumbLeft, this.inputLeft); + setupThumb(this.thumbRight, this.inputRight); } -} -/** - * Formats a number with commas as thousand separators - * @param {number} number - The number to format - * @returns {string} The formatted number string - * @private - */ -formatNumber(number) { - return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -} + initState() { + [this.inputLeft, this.inputRight].forEach((input) => { + input.setAttribute('min', this.sliderMin); + input.setAttribute('max', this.sliderMax); + input.setAttribute('step', this.sliderSteps); + input.setAttribute('formnovalidate', ''); + input.setAttribute('data-form-ignore', ''); + }); -/** - * Updates all visual elements and values for the left handle - * @private - */ -updateLeftValues(value) { - const constrainedValue = Math.min( - parseInt(value), - parseInt(this.inputRight.value) - this.minDifference, - ); - - // Update slider input - this.inputLeft.value = constrainedValue; - - // Update form input - if (this.rangeStart) { - // Avoid recursive reaction to our own programmatic updates - const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); - this.__suspendExternalSync = true; - valueProp.set.call(this.rangeStart, constrainedValue); - this.__suspendExternalSync = false; + if (this.rangeStart && this.rangeStart.value) { + this.updateLeftValues(this.rangeStart.value); + } else { + this.updateLeftValues(this.sliderMin); + } + + if (this.rangeEnd && this.rangeEnd.value) { + this.updateRightValues(this.rangeEnd.value); + } else { + this.updateRightValues(this.sliderMax); + } } - // Update display - if (this.displayStart) { - const displayLeft = this.shouldFormatNumber === 'true' ? this.formatNumber(constrainedValue) : constrainedValue; - this.displayStart.textContent = String(displayLeft); + updateLeftValues(value) { + const constrainedValue = this.rs.constrainLeftValue( + value, + this.inputRight.value, + this.minDifference, + ); + + this.inputLeft.value = constrainedValue; + + if (this.rangeStart) { + this.rs.setNativeTextInputValue( + this.rangeStart, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayStart) { + this.displayStart.textContent = this.rs.formatStartDisplayContent( + constrainedValue, + this.shouldFormatNumber, + ); + } + + this.updateThumbPosition( + this.inputLeft, + this.thumbLeft, + this.range, + 'left', + ); } - // Update visual position - this.updateThumbPosition( - this.inputLeft, - this.thumbLeft, - this.range, - 'left', - ); -} - -/** - * Updates all visual elements and values for the right handle - * @private - */ -updateRightValues(value) { - const constrainedValue = Math.max( - parseInt(value), - parseInt(this.inputLeft.value) + this.minDifference, - ); - - // Update slider input - this.inputRight.value = constrainedValue; - - // Update form input - if (this.rangeEnd) { - // Avoid recursive reaction to our own programmatic updates - const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); - this.__suspendExternalSync = true; - valueProp.set.call(this.rangeEnd, constrainedValue); - this.__suspendExternalSync = false; + updateRightValues(value) { + const constrainedValue = this.rs.constrainRightValue( + value, + this.inputLeft.value, + this.minDifference, + ); + + this.inputRight.value = constrainedValue; + + if (this.rangeEnd) { + this.rs.setNativeTextInputValue( + this.rangeEnd, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayEnd) { + this.displayEnd.textContent = this.rs.formatEndDisplayContent( + constrainedValue, + value, + this.sliderMax, + this.shouldFormatNumber, + this.rightSuffix, + this.defaultSuffix, + ); + } + + this.updateThumbPosition( + this.inputRight, + this.thumbRight, + this.range, + 'right', + ); } - // Update display - if (this.displayEnd) { - let finalDisplay = this.shouldFormatNumber === 'true' ? this.formatNumber(constrainedValue) : constrainedValue; - - if (this.rightSuffix && value >= this.sliderMax) { - this.displayEnd.textContent = `${finalDisplay}${this.rightSuffix}`; - } else if (this.defaultSuffix) { - this.displayEnd.textContent = `${finalDisplay}${this.defaultSuffix}`; - } else { - this.displayEnd.textContent = String(finalDisplay); - } - } + setupEventListeners() { + this.inputLeft.addEventListener('input', () => { + this.updateLeftValues(this.inputLeft.value); + if (this.rangeStart) { + this.rs.dispatchInputEvent(this.rangeStart); + } + }); - // Update visual position - this.updateThumbPosition( - this.inputRight, - this.thumbRight, - this.range, - 'right', - ); -} + this.inputRight.addEventListener('input', () => { + this.updateRightValues(this.inputRight.value); + if (this.rangeEnd) { + this.rs.dispatchInputEvent(this.rangeEnd); + } + }); -/** - * Sets up event listeners for the range slider - * @private - */ -setupEventListeners() { - // Input events for the slider inputs - this.inputLeft.addEventListener('input', () => { - this.updateLeftValues(this.inputLeft.value); - if (this.rangeStart) { - this.triggerEvent(this.rangeStart); + if (this.rangeStart) { + this.rangeStart.addEventListener('input', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rangeStart.addEventListener('change', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeStart, + (val) => { + this.updateLeftValues(val); + }, + () => this.__suspendExternalSync, + ); + } + + if (this.rangeEnd) { + this.rangeEnd.addEventListener('input', (e) => { + this.updateRightValues(e.target.value); + }); + this.rangeEnd.addEventListener('change', (e) => { + this.updateRightValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeEnd, + (val) => { + this.updateRightValues(val); + }, + () => this.__suspendExternalSync, + ); + } } - }); - this.inputRight.addEventListener('input', () => { - this.updateRightValues(this.inputRight.value); - if (this.rangeEnd) { - this.triggerEvent(this.rangeEnd); - } - }); - - // Watch for changes to the form inputs - if (this.rangeStart) { - // React to user typing immediately - this.rangeStart.addEventListener('input', (e) => { - this.updateLeftValues(e.target.value); - }); - this.rangeStart.addEventListener('change', (e) => { - this.updateLeftValues(e.target.value); - }); - // Hook into programmatic value assignments - this.hookInputValueSync(this.rangeStart, (val) => { - this.updateLeftValues(val); - }); + updateThumbPosition(input, thumb, range, side) { + const min = parseInt(input.min, 10); + const max = parseInt(input.max, 10); + const current = parseInt(input.value, 10); + const percent = ((current - min) / (max - min)) * 100; + + if (side === 'left') { + thumb.style.left = `${percent}%`; + thumb.style.transform = 'translateX(-50%)'; + range.style.left = `${percent}%`; + } else { + thumb.style.right = `${100 - percent}%`; + thumb.style.transform = 'translateX(50%)'; + range.style.right = `${100 - percent}%`; + } } - if (this.rangeEnd) { - // React to user typing immediately - this.rangeEnd.addEventListener('input', (e) => { - this.updateRightValues(e.target.value); - }); - this.rangeEnd.addEventListener('change', (e) => { - this.updateRightValues(e.target.value); - }); - // Hook into programmatic value assignments - this.hookInputValueSync(this.rangeEnd, (val) => { - this.updateRightValues(val); - }); + setFrom(value) { + this.updateLeftValues(value); } -} -/** - * Updates the position of a thumb element - * @param {HTMLInputElement} input - The input element - * @param {HTMLElement} thumb - The thumb element - * @param {HTMLElement} range - The range element - * @param {string} side - The side of the slider ('left' or 'right') - * @private - */ -updateThumbPosition(input, thumb, range, side) { - const min = parseInt(input.min); - const max = parseInt(input.max); - const current = parseInt(input.value); - const percent = ((current - min) / (max - min)) * 100; - - // Get the thumb's width to account for its dimensions - const thumbWidth = thumb.offsetWidth || parseInt(getComputedStyle(thumb).width) || 20; - const sliderWidth = this.slider.offsetWidth || parseInt(getComputedStyle(this.slider).width) || 1; - - // Calculate the percentage that represents half the thumb width - const thumbHalfPercent = (thumbWidth / sliderWidth) * 100; - - if (side === 'left') { - thumb.style.left = `${percent}%`; - thumb.style.transform = 'translateX(-50%)'; - range.style.left = `${percent}%`; - } else { - thumb.style.right = `${100 - percent}%`; - thumb.style.transform = 'translateX(50%)'; - range.style.right = `${100 - percent}%`; + setTo(value) { + this.updateRightValues(value); } -} -/** - * Validates that a value is a valid number - * @param {string} value - The value to validate - * @returns {number} The parsed number - * @throws {Error} If the value is not a valid number - */ -validateNumber(value) { - const num = parseFloat(value); - if (isNaN(num)) { - throw new Error(`Invalid number value: ${value}`); + setRange(from, to) { + this.updateLeftValues(from); + this.updateRightValues(to); } - return num; -} -/** - * Triggers an input event on an element - * @param {HTMLElement} element - The element to trigger the event on - * @private - */ -triggerEvent(element) { - if (element) { - element.dispatchEvent(new Event('input', { bubbles: true })); + reset() { + this.setRange(this.sliderMin, this.sliderMax); } -} -/** - * Hook into a text input's value property to react to programmatic assignments - * @param {HTMLInputElement} input - The input to hook - * @param {(val: string|number) => void} handler - Handler to run on assignment - * @private - */ -hookInputValueSync(input, handler) { - if (!input) return; - const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); - const self = this; - try { - Object.defineProperty(input, 'value', { - get() { - return valueProp.get.call(this); - }, - set(v) { - valueProp.set.call(this, v); - if (!self.__suspendExternalSync) { - handler(v); + validateRequiredElements() { + const requiredElements = { + inputLeft: this.inputLeft, + inputRight: this.inputRight, + thumbLeft: this.thumbLeft, + thumbRight: this.thumbRight, + range: this.range, + }; + + Object.entries(requiredElements).forEach(([name, element]) => { + if (!element) { + throw new Error(`Required element ${name} is missing`); } - }, - configurable: true, - enumerable: true, }); - } catch (err) { - // Fallback: rely on 'input'/'change' listeners if defineProperty fails } } -/** - * Public API: set left range value - * @param {number|string} value - */ -setFrom(value) { - this.updateLeftValues(value); -} - -/** - * Public API: set right range value - * @param {number|string} value - */ -setTo(value) { - this.updateRightValues(value); -} - -/** - * Public API: set both range values atomically and in correct order - * @param {number|string} from - * @param {number|string} to - */ -setRange(from, to) { - this.updateLeftValues(from); - this.updateRightValues(to); -} - -/** - * Public API: reset slider to configured bounds - */ -reset() { - this.setRange(this.sliderMin, this.sliderMax); -} - -/** - * Validates that all required elements are present - * @private - * @throws {Error} If any required element is missing - */ -validateRequiredElements() { - const requiredElements = { - inputLeft: this.inputLeft, - inputRight: this.inputRight, - thumbLeft: this.thumbLeft, - thumbRight: this.thumbRight, - range: this.range, - }; - - Object.entries(requiredElements).forEach(([name, element]) => { - if (!element) { - throw new Error(`Required element ${name} is missing`); - } - }); -} -} - -/** - * Initialize all range sliders on the page - */ const initializeRangeSlider = () => { try { window.webtricks = window.webtricks || []; - const wrappers = document.querySelectorAll('[wt-rangeslider-element="slider-wrapper"]'); + const wrappers = document.querySelectorAll( + '[wt-rangeslider-element="slider-wrapper"]', + ); if (!wrappers || wrappers.length === 0) return; - wrappers.forEach(wrapper => { + wrappers.forEach((wrapper) => { const instance = new RangeSlider(wrapper); - window.webtricks.push({ 'RangeSlider': instance }); + window.webtricks.push({ RangeSlider: instance }); }); } catch (err) { console.error(`RangeSlider initialization error: ${err.message}`); } }; -// Initialize on DOM ready if (/complete|interactive|loaded/.test(document.readyState)) { initializeRangeSlider(); } else { window.addEventListener('DOMContentLoaded', initializeRangeSlider); -} \ No newline at end of file +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { RangeSlider, InitializeRangeSlider: initializeRangeSlider }; +} diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js new file mode 100644 index 0000000..5a1d554 --- /dev/null +++ b/Dist/Functional/RangeSliderSimple.js @@ -0,0 +1,485 @@ +/*! + * WebTricks — RangeSliderSimple + * Dual native range inputs (no custom thumb DOM). Self-contained (single script tag). + * MIT License + */ + +'use strict'; + +/** @private Duplicated core logic (same behavior as RangeSlider) so this file has no shared dependency. */ +var __WT_RANGE_SLIDER_SIMPLE_CORE = (function () { + function validateNumber(value) { + const num = parseFloat(value); + if (isNaN(num)) { + throw new Error(`Invalid number value: ${value}`); + } + return num; + } + + function formatNumber(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + function readSliderConfig(sliderEl, prefix) { + const minAttr = sliderEl.getAttribute(`${prefix}-min`); + const sliderMin = minAttr !== null ? validateNumber(minAttr) : 0; + + const maxAttr = sliderEl.getAttribute(`${prefix}-max`); + const sliderMax = maxAttr !== null ? validateNumber(maxAttr) : 100; + + const stepsAttr = sliderEl.getAttribute(`${prefix}-steps`); + const sliderSteps = stepsAttr !== null ? validateNumber(stepsAttr) : 1; + + const minDiffAttr = sliderEl.getAttribute(`${prefix}-mindifference`); + const minDifference = minDiffAttr !== null ? validateNumber(minDiffAttr) : sliderSteps; + + return { + sliderMin, + sliderMax, + sliderSteps, + minDifference, + rightSuffix: sliderEl.getAttribute(`${prefix}-rightsuffix`) || null, + defaultSuffix: sliderEl.getAttribute(`${prefix}-defaultsuffix`) || null, + shouldFormatNumber: sliderEl.getAttribute(`${prefix}-formatnumber`) || null, + }; + } + + function constrainLeftValue(rawValue, rightValueStr, minDifference) { + return Math.min( + parseInt(rawValue, 10), + parseInt(rightValueStr, 10) - minDifference, + ); + } + + function constrainRightValue(rawValue, leftValueStr, minDifference) { + return Math.max( + parseInt(rawValue, 10), + parseInt(leftValueStr, 10) + minDifference, + ); + } + + function setNativeTextInputValue(input, constrainedValue, suspendBegin, suspendEnd) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + suspendBegin(); + valueProp.set.call(input, constrainedValue); + suspendEnd(); + } + + function formatStartDisplayContent(constrainedValue, shouldFormatNumberAttr) { + const displayLeft = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + return String(displayLeft); + } + + function formatEndDisplayContent( + constrainedValue, + rawInputValue, + sliderMax, + shouldFormatNumberAttr, + rightSuffix, + defaultSuffix, + ) { + let finalDisplay = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + const rawNum = parseFloat(rawInputValue); + if (rightSuffix && !isNaN(rawNum) && rawNum >= sliderMax) { + return `${finalDisplay}${rightSuffix}`; + } + if (defaultSuffix) { + return `${finalDisplay}${defaultSuffix}`; + } + return String(finalDisplay); + } + + function hookInputValueSync(input, handler, isSuspended) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + try { + Object.defineProperty(input, 'value', { + get() { + return valueProp.get.call(this); + }, + set(v) { + valueProp.set.call(this, v); + if (!isSuspended()) { + handler(v); + } + }, + configurable: true, + enumerable: true, + }); + } catch (err) { + // Fallback: rely on 'input'/'change' listeners if defineProperty fails + } + } + + function dispatchInputEvent(element) { + if (element) { + element.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + return { + readSliderConfig, + constrainLeftValue, + constrainRightValue, + setNativeTextInputValue, + formatStartDisplayContent, + formatEndDisplayContent, + hookInputValueSync, + dispatchInputEvent, + }; +}()); + +const ATTR_PREFIX = 'wt-rangeslidersimple'; + +/** + * Native dual-handle range slider; visible thumbs match browser hit targets. + * @param {HTMLElement} wrapper + */ +class RangeSliderSimple { + constructor(wrapper) { + try { + this.rs = __WT_RANGE_SLIDER_SIMPLE_CORE; + + this.wrapper = wrapper; + this.slider = wrapper.querySelector( + `[${ATTR_PREFIX}-element="slider"]`, + ); + this.__suspendExternalSync = false; + + if (!this.slider) { + throw new Error('Slider element not found within wrapper'); + } + + this.addStyles(); + this.initConfig(); + this.initElements(); + this.initState(); + this.setupEventListeners(); + } catch (err) { + console.error(`RangeSliderSimple initialization failed: ${err.message}`); + } + } + + addStyles() { + const existing = document.getElementById('wt-rangeslidersimple-styles'); + if (existing) return; + + const style = document.createElement('style'); + style.id = 'wt-rangeslidersimple-styles'; + style.textContent = ` + [${ATTR_PREFIX}-element="slider"] { + position: relative; + min-height: 32px; + } + + [${ATTR_PREFIX}-element="input-left"], + [${ATTR_PREFIX}-element="input-right"] { + position: absolute; + left: 0; + width: 100%; + top: 0; + bottom: 0; + margin: auto; + height: 24px; + -webkit-appearance: none; + appearance: none; + background: transparent; + pointer-events: auto; + z-index: 2; + outline: none; + } + + [${ATTR_PREFIX}-element="input-right"] { + z-index: 1; + } + + [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-thumb, + [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: #222; + cursor: pointer; + pointer-events: auto; + } + + [${ATTR_PREFIX}-element="input-left"]::-moz-range-thumb, + [${ATTR_PREFIX}-element="input-right"]::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: #222; + cursor: pointer; + border: none; + } + + [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-runnable-track, + [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-runnable-track { + height: 6px; + border-radius: 3px; + background: rgba(0, 0, 0, 0.12); + } + + [${ATTR_PREFIX}-element="input-left"]::-moz-range-track, + [${ATTR_PREFIX}-element="input-right"]::-moz-range-track { + height: 6px; + border-radius: 3px; + background: rgba(0, 0, 0, 0.12); + } + `; + document.head.appendChild(style); + } + + initConfig() { + const cfg = this.rs.readSliderConfig(this.slider, ATTR_PREFIX); + this.sliderMin = cfg.sliderMin; + this.sliderMax = cfg.sliderMax; + this.sliderSteps = cfg.sliderSteps; + this.minDifference = cfg.minDifference; + this.rightSuffix = cfg.rightSuffix; + this.defaultSuffix = cfg.defaultSuffix; + this.shouldFormatNumber = cfg.shouldFormatNumber; + } + + initElements() { + this.rangeStart = this.wrapper.querySelector( + `[${ATTR_PREFIX}-range="from"]`, + ); + this.rangeEnd = this.wrapper.querySelector(`[${ATTR_PREFIX}-range="to"]`); + + this.displayStart = this.wrapper.querySelector( + `[${ATTR_PREFIX}-display="from"]`, + ); + this.displayEnd = this.wrapper.querySelector( + `[${ATTR_PREFIX}-display="to"]`, + ); + + this.inputLeft = this.slider.querySelector( + `[${ATTR_PREFIX}-element="input-left"]`, + ); + this.inputRight = this.slider.querySelector( + `[${ATTR_PREFIX}-element="input-right"]`, + ); + + this.validateRequiredElements(); + } + + initState() { + [this.inputLeft, this.inputRight].forEach((input) => { + input.setAttribute('min', this.sliderMin); + input.setAttribute('max', this.sliderMax); + input.setAttribute('step', this.sliderSteps); + input.setAttribute('formnovalidate', ''); + input.setAttribute('data-form-ignore', ''); + }); + + if (this.rangeStart && this.rangeStart.value) { + this.updateLeftValues(this.rangeStart.value); + } else { + this.updateLeftValues(this.sliderMin); + } + + if (this.rangeEnd && this.rangeEnd.value) { + this.updateRightValues(this.rangeEnd.value); + } else { + this.updateRightValues(this.sliderMax); + } + } + + bringInputToFront(which) { + if (which === 'left') { + this.inputLeft.style.zIndex = '3'; + this.inputRight.style.zIndex = '1'; + } else { + this.inputRight.style.zIndex = '3'; + this.inputLeft.style.zIndex = '2'; + } + } + + updateLeftValues(value) { + const constrainedValue = this.rs.constrainLeftValue( + value, + this.inputRight.value, + this.minDifference, + ); + + this.inputLeft.value = constrainedValue; + + if (this.rangeStart) { + this.rs.setNativeTextInputValue( + this.rangeStart, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayStart) { + this.displayStart.textContent = this.rs.formatStartDisplayContent( + constrainedValue, + this.shouldFormatNumber, + ); + } + } + + updateRightValues(value) { + const constrainedValue = this.rs.constrainRightValue( + value, + this.inputLeft.value, + this.minDifference, + ); + + this.inputRight.value = constrainedValue; + + if (this.rangeEnd) { + this.rs.setNativeTextInputValue( + this.rangeEnd, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayEnd) { + this.displayEnd.textContent = this.rs.formatEndDisplayContent( + constrainedValue, + value, + this.sliderMax, + this.shouldFormatNumber, + this.rightSuffix, + this.defaultSuffix, + ); + } + } + + setupEventListeners() { + const onLeftPointer = () => this.bringInputToFront('left'); + const onRightPointer = () => this.bringInputToFront('right'); + + this.inputLeft.addEventListener('pointerdown', onLeftPointer); + this.inputRight.addEventListener('pointerdown', onRightPointer); + + this.inputLeft.addEventListener('input', () => { + this.updateLeftValues(this.inputLeft.value); + if (this.rangeStart) { + this.rs.dispatchInputEvent(this.rangeStart); + } + }); + + this.inputRight.addEventListener('input', () => { + this.updateRightValues(this.inputRight.value); + if (this.rangeEnd) { + this.rs.dispatchInputEvent(this.rangeEnd); + } + }); + + if (this.rangeStart) { + this.rangeStart.addEventListener('input', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rangeStart.addEventListener('change', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeStart, + (val) => { + this.updateLeftValues(val); + }, + () => this.__suspendExternalSync, + ); + } + + if (this.rangeEnd) { + this.rangeEnd.addEventListener('input', (e) => { + this.updateRightValues(e.target.value); + }); + this.rangeEnd.addEventListener('change', (e) => { + this.updateRightValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeEnd, + (val) => { + this.updateRightValues(val); + }, + () => this.__suspendExternalSync, + ); + } + } + + setFrom(value) { + this.updateLeftValues(value); + } + + setTo(value) { + this.updateRightValues(value); + } + + setRange(from, to) { + this.updateLeftValues(from); + this.updateRightValues(to); + } + + reset() { + this.setRange(this.sliderMin, this.sliderMax); + } + + validateRequiredElements() { + const requiredElements = { + inputLeft: this.inputLeft, + inputRight: this.inputRight, + }; + + Object.entries(requiredElements).forEach(([name, element]) => { + if (!element) { + throw new Error(`Required element ${name} is missing`); + } + }); + } +} + +const initializeRangeSliderSimple = () => { + try { + window.webtricks = window.webtricks || []; + const wrappers = document.querySelectorAll( + `[${ATTR_PREFIX}-element="slider-wrapper"]`, + ); + + if (!wrappers || wrappers.length === 0) return; + + wrappers.forEach((wrapper) => { + const instance = new RangeSliderSimple(wrapper); + window.webtricks.push({ RangeSliderSimple: instance }); + }); + } catch (err) { + console.error(`RangeSliderSimple initialization error: ${err.message}`); + } +}; + +if (/complete|interactive|loaded/.test(document.readyState)) { + initializeRangeSliderSimple(); +} else { + window.addEventListener('DOMContentLoaded', initializeRangeSliderSimple); +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + RangeSliderSimple, + InitializeRangeSliderSimple: initializeRangeSliderSimple, + }; +} diff --git a/README.md b/README.md index 582f885..956ee63 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,21 @@ Multiple Scripts: Add as many scripts as you need to your project by referencing ``` + +**Range sliders:** each script is **self-contained** (one tag). Use **`RangeSlider.js`** for custom thumbs or **`RangeSliderSimple.js`** for native thumbs only. Use jsDelivr (not raw `githubusercontent.com`, which often serves `text/plain` and blocks execution). + +``` + +``` + +Native-thumb variant: + +``` + +``` + +If a page uses **both** slider types, you may include **both** scripts; they use separate attribute namespaces (`wt-rangeslider-*` vs `wt-rangeslidersimple-*`). + Ready to Use: Once imported, the scripts initialize automatically, provided the correct HTML attributes are in place. diff --git a/__tests__/RangeSlider.test.js b/__tests__/RangeSlider.test.js new file mode 100644 index 0000000..9f0f9f4 --- /dev/null +++ b/__tests__/RangeSlider.test.js @@ -0,0 +1,50 @@ +/** @jest-environment jsdom */ + +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('RangeSlider', () => { + let RangeSlider; + let InitializeRangeSlider; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ RangeSlider, InitializeRangeSlider } = require('../Dist/Functional/RangeSlider.js')); + }); + + function mountRangeSlider() { + document.body.innerHTML = ` +
+
+
+
+
+ + +
+
+ `; + } + + test('constructor sets initial values from min/max', () => { + mountRangeSlider(); + const wrapper = document.querySelector( + '[wt-rangeslider-element="slider-wrapper"]', + ); + new RangeSlider(wrapper); + const left = wrapper.querySelector('[wt-rangeslider-element="input-left"]'); + const right = wrapper.querySelector('[wt-rangeslider-element="input-right"]'); + expect(left.value).toBe('0'); + expect(right.value).toBe('100'); + }); + + test('InitializeRangeSlider pushes instance to webtricks', () => { + mountRangeSlider(); + InitializeRangeSlider(); + expect(window.webtricks.some((e) => e.RangeSlider)).toBe(true); + }); +}); diff --git a/__tests__/RangeSliderSimple.test.js b/__tests__/RangeSliderSimple.test.js new file mode 100644 index 0000000..94d4e28 --- /dev/null +++ b/__tests__/RangeSliderSimple.test.js @@ -0,0 +1,58 @@ +/** @jest-environment jsdom */ + +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('RangeSliderSimple', () => { + let RangeSliderSimple; + let InitializeRangeSliderSimple; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ + RangeSliderSimple, + InitializeRangeSliderSimple, + } = require('../Dist/Functional/RangeSliderSimple.js')); + }); + + function mountSlider() { + document.body.innerHTML = ` +
+
0
+
100
+
+ + +
+
+ `; + } + + test('constructor wires inputs and displays', () => { + mountSlider(); + const wrapper = document.querySelector( + '[wt-rangeslidersimple-element="slider-wrapper"]', + ); + const instance = new RangeSliderSimple(wrapper); + const left = wrapper.querySelector( + '[wt-rangeslidersimple-element="input-left"]', + ); + const right = wrapper.querySelector( + '[wt-rangeslidersimple-element="input-right"]', + ); + expect(left.value).toBe('0'); + expect(right.value).toBe('100'); + expect(instance.sliderMin).toBe(0); + expect(instance.sliderMax).toBe(100); + }); + + test('InitializeRangeSliderSimple pushes instance to webtricks', () => { + mountSlider(); + InitializeRangeSliderSimple(); + expect(window.webtricks.some((e) => e.RangeSliderSimple)).toBe(true); + }); +}); diff --git a/docs/Functional/CookieConsent.md b/docs/Functional/CookieConsent.md index 81aa7d1..b7c57df 100644 --- a/docs/Functional/CookieConsent.md +++ b/docs/Functional/CookieConsent.md @@ -1,12 +1,15 @@ # CookieConsent ## Version + Current Version: 1.0.0 ## Description + CookieConsent is a GDPR-compliant cookie consent management system that provides granular control over cookie preferences and script loading. It supports multiple consent categories, manages consent persistence, and controls script loading based on user preferences. ## Functionality + - Cookie consent banner management - Multiple consent categories - Granular script loading control @@ -17,18 +20,22 @@ CookieConsent is a GDPR-compliant cookie consent management system that provides - Category-based script loading ## Usage + Add the script to your project and include the required attributes on your banner and script elements. ### Installation + ```html ``` ### Required Attributes + - `wt-cookieconsent-element="banner"` - Applied to the main cookie banner container - `wt-cookieconsent-script="category"` - Applied to scripts that require consent ### Optional Elements and Attributes + - `wt-cookieconsent-element="accept-all"` - Accept all cookies button - `wt-cookieconsent-element="accept-necessary"` - Accept necessary cookies only button - `wt-cookieconsent-element="manage-cookies"` - Manage cookies settings button @@ -36,6 +43,7 @@ Add the script to your project and include the required attributes on your banne - `wt-cookieconsent-category="category-name"` - Category checkbox inputs ## Considerations + 1. **GDPR Compliance**: Supports granular consent management 2. **Script Loading**: Automatically handles script loading based on consent 3. **Persistence**: Stores consent in cookies with configurable expiry @@ -45,6 +53,7 @@ Add the script to your project and include the required attributes on your banne ## Examples ### Basic Implementation + ```html
@@ -59,6 +68,7 @@ Add the script to your project and include the required attributes on your banne ``` ### Advanced Implementation with Categories + ```html

Choose your cookie preferences

@@ -102,6 +112,7 @@ Add the script to your project and include the required attributes on your banne ``` ### Inline Script Implementation + ```html ``` ### Required Attributes + - `wt-copycb-element="container"` - Applied to the container element - `wt-copycb-element="trigger"` - Applied to the click trigger element - `wt-copycb-element="target"` - Applied to the element containing text to copy ### Optional Attributes + - `wt-copycb-message="Copied!"` - Custom success message - `wt-copycb-active="is-copy"` - CSS class for active state - `wt-copycb-timeout="2000"` - Duration to show success state (ms) - `wt-copycb-element="texttarget"` - Element within trigger to update with success message ## Considerations + 1. **Clipboard API**: Uses modern navigator.clipboard API 2. **State Management**: Automatically resets to original state 3. **Visual Feedback**: Supports both text and class-based feedback @@ -44,6 +52,7 @@ Add the script to your project and include the required attributes on your copy ## Examples ### Basic Implementation + ```html
@@ -52,6 +61,7 @@ Add the script to your project and include the required attributes on your copy ``` ### Advanced Implementation + ```html
@@ -50,6 +60,7 @@ For multiple pairs: ``` ### Multiple Pairs Implementation + ```html @@ -60,8 +71,9 @@ For multiple pairs: ``` ### Common Use Cases + 1. Synchronized tab switching 2. Multiple button controls 3. Hidden element triggering 4. Form submission from multiple locations -5. Modal/popup controls \ No newline at end of file +5. Modal/popup controls diff --git a/docs/WebflowOnly/RenderStatic.md b/docs/WebflowOnly/RenderStatic.md index 8c71284..d2e9e2d 100644 --- a/docs/WebflowOnly/RenderStatic.md +++ b/docs/WebflowOnly/RenderStatic.md @@ -1,12 +1,15 @@ # RenderStatic ## Version + Current Version: 1.0.0 ## Description + RenderStatic is a Webflow-specific script that allows you to automatically insert cloned elements at specified intervals within a container. It's particularly useful for creating dynamic layouts where you need to repeat certain elements at regular intervals among existing content. ## Functionality + - Automatically clones and inserts elements at specified intervals - Maintains proper spacing between original and cloned elements - Supports multiple different cloneable elements @@ -15,21 +18,26 @@ RenderStatic is a Webflow-specific script that allows you to automatically inser - Smart reinitialization on content changes ## Usage + Add the script to your Webflow project and include the required attributes on your container and cloneable elements. ### Installation + ```html ``` ### Required Attributes + - `wt-renderstatic-element="container"` - Applied to the main container where elements will be inserted - `wt-renderstatic-element="cloneable"` - Applied to elements that will be cloned and inserted ### Optional Attributes + - `wt-renderstatic-gap="1"` - Number of elements to skip before inserting a clone (default: 1) ## Considerations + 1. **DOM Updates**: The script automatically observes container changes and reinitializes when needed 2. **Element Order**: Cloned elements are inserted at calculated positions based on the gap value 3. **Multiple Cloneables**: If multiple cloneable elements are defined, they will be used in sequence @@ -39,6 +47,7 @@ Add the script to your Webflow project and include the required attributes on yo ## Examples ### Basic Implementation + ```html
Original Item 1
@@ -49,6 +58,7 @@ Add the script to your Webflow project and include the required attributes on yo ``` ### With Custom Gap and Multiple Cloneables + ```html
Original Item 1
@@ -58,4 +68,4 @@ Add the script to your Webflow project and include the required attributes on yo
Cloneable 1
Cloneable 2
-``` \ No newline at end of file +``` diff --git a/docs/WebflowOnly/TabsSlider.md b/docs/WebflowOnly/TabsSlider.md index 28426fe..06f4d8a 100644 --- a/docs/WebflowOnly/TabsSlider.md +++ b/docs/WebflowOnly/TabsSlider.md @@ -1,12 +1,15 @@ # TabsSlider ## Version + Current Version: 1.0.0 ## Description + TabsSlider is a Webflow-specific script that adds automatic sliding functionality to Webflow's native tab components. It enables automatic rotation between tabs with configurable timing and pause-on-hover functionality. ## Functionality + - Automatic rotation between tabs at specified intervals - Configurable slide timing - Pause-on-hover capability @@ -15,22 +18,27 @@ TabsSlider is a Webflow-specific script that adds automatic sliding functionalit - Error handling and graceful degradation ## Usage + Add the script to your Webflow project and include the required attributes on your tabs component. ### Installation + ```html ``` ### Required Attributes + - `wt-tabslider-element="tabs"` - Applied to the main tabs container - `wt-tabslider-element="menu"` - Applied to the tabs menu/navigation container ### Optional Attributes + - `wt-tabslider-speed="5000"` - Set the sliding speed in milliseconds (default: 5000ms) - `wt-tabslider-pauseonhover="true"` - Enable/disable pause on hover functionality (default: false) ## Considerations + 1. **Webflow Dependency**: This script requires Webflow's native tabs component to function 2. **Browser Compatibility**: Includes special handling for Safari browsers 3. **Performance**: Uses debouncing for hover events to optimize performance @@ -39,6 +47,7 @@ Add the script to your Webflow project and include the required attributes on yo ## Examples ### Basic Implementation + ```html
@@ -51,6 +60,7 @@ Add the script to your Webflow project and include the required attributes on yo ``` ### With Custom Speed and Hover Pause + ```html
-``` \ No newline at end of file +``` diff --git a/package-lock.json b/package-lock.json index 71519f4..f75581b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webtricks", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webtricks", - "version": "0.1.0", + "version": "1.0.0", "devDependencies": { "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" From 4926cf11c9960393e934d4f2a9d620650363eed3 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:31:51 -0700 Subject: [PATCH 39/52] css revision --- Dist/Functional/RangeSliderSimple.js | 53 +++++----------------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js index 5a1d554..3eeecc0 100644 --- a/Dist/Functional/RangeSliderSimple.js +++ b/Dist/Functional/RangeSliderSimple.js @@ -173,10 +173,12 @@ class RangeSliderSimple { const style = document.createElement('style'); style.id = 'wt-rangeslidersimple-styles'; + /* Layout only: no appearance:none or ::-webkit-slider-* / ::-moz-range-* so thumbs/tracks stay browser-default. */ style.textContent = ` [${ATTR_PREFIX}-element="slider"] { position: relative; - min-height: 32px; + min-height: 2.75rem; + box-sizing: border-box; } [${ATTR_PREFIX}-element="input-left"], @@ -184,56 +186,19 @@ class RangeSliderSimple { position: absolute; left: 0; width: 100%; - top: 0; - bottom: 0; - margin: auto; - height: 24px; - -webkit-appearance: none; - appearance: none; - background: transparent; + max-width: 100%; + top: 50%; + transform: translateY(-50%); + margin: 0; + padding: 0; + box-sizing: border-box; pointer-events: auto; z-index: 2; - outline: none; } [${ATTR_PREFIX}-element="input-right"] { z-index: 1; } - - [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-thumb, - [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 18px; - height: 18px; - border-radius: 50%; - background: #222; - cursor: pointer; - pointer-events: auto; - } - - [${ATTR_PREFIX}-element="input-left"]::-moz-range-thumb, - [${ATTR_PREFIX}-element="input-right"]::-moz-range-thumb { - width: 18px; - height: 18px; - border-radius: 50%; - background: #222; - cursor: pointer; - border: none; - } - - [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-runnable-track, - [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-runnable-track { - height: 6px; - border-radius: 3px; - background: rgba(0, 0, 0, 0.12); - } - - [${ATTR_PREFIX}-element="input-left"]::-moz-range-track, - [${ATTR_PREFIX}-element="input-right"]::-moz-range-track { - height: 6px; - border-radius: 3px; - background: rgba(0, 0, 0, 0.12); - } `; document.head.appendChild(style); } From 302d497e4927123dd8efa01d330ba32408baef20 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:35:08 -0700 Subject: [PATCH 40/52] vnumber --- Dist/Functional/RangeSliderSimple.js | 1 + docs/Functional/RangeSliderSimple.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js index 3eeecc0..9d123d7 100644 --- a/Dist/Functional/RangeSliderSimple.js +++ b/Dist/Functional/RangeSliderSimple.js @@ -1,5 +1,6 @@ /*! * WebTricks — RangeSliderSimple + * @version 0.0.2 — pre-release; bump patch (and docs/Functional/RangeSliderSimple.md) on every change to this file. * Dual native range inputs (no custom thumb DOM). Self-contained (single script tag). * MIT License */ diff --git a/docs/Functional/RangeSliderSimple.md b/docs/Functional/RangeSliderSimple.md index 6d9ebd4..136a493 100644 --- a/docs/Functional/RangeSliderSimple.md +++ b/docs/Functional/RangeSliderSimple.md @@ -1,5 +1,9 @@ # RangeSliderSimple +## Version + +Current version: **0.0.2** (pre-release — see banner in `Dist/Functional/RangeSliderSimple.js`; bump patch and this line when the script changes). + ## Description `RangeSliderSimple` is a dual-handle range control built from two native `` elements. The draggable thumbs you see are the browser’s own controls, so hit targets stay aligned with the visuals. The file is **self-contained** (same core behavior as `RangeSlider`, inlined—keep edits in sync manually if you change constraint/display logic). From aa98ae5ad0d10b5feb87c6908775997df8c63862 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:46:22 -0700 Subject: [PATCH 41/52] simplify --- Dist/Functional/RangeSliderSimple.js | 129 +++++++++++++++++++++++++-- docs/Functional/RangeSlider.md | 2 + docs/Functional/RangeSliderSimple.md | 19 +++- 3 files changed, 139 insertions(+), 11 deletions(-) diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js index 9d123d7..1367091 100644 --- a/Dist/Functional/RangeSliderSimple.js +++ b/Dist/Functional/RangeSliderSimple.js @@ -1,6 +1,6 @@ /*! * WebTricks — RangeSliderSimple - * @version 0.0.2 — pre-release; bump patch (and docs/Functional/RangeSliderSimple.md) on every change to this file. + * @version 0.0.5 — pre-release; bump patch (and docs/Functional/RangeSliderSimple.md) on every change to this file. * Dual native range inputs (no custom thumb DOM). Self-contained (single script tag). * MIT License */ @@ -174,36 +174,125 @@ class RangeSliderSimple { const style = document.createElement('style'); style.id = 'wt-rangeslidersimple-styles'; - /* Layout only: no appearance:none or ::-webkit-slider-* / ::-moz-range-* so thumbs/tracks stay browser-default. */ + /* MDN-like range UI (Chrome docs): blue filled track, grey remainder, white pill thumb. + --wt-rs-track-fill defaults to #3b82f6 (typical RangeSlider [range] bar in docs). */ style.textContent = ` [${ATTR_PREFIX}-element="slider"] { + --wt-rs-track-fill: #3b82f6; + --wt-rs-track-bg: #e5e7eb; + --wt-rs-thumb-bg: #ffffff; + --wt-rs-thumb-border: #cbd5e1; + --wt-rs-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); position: relative; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + align-items: center; + justify-items: stretch; min-height: 2.75rem; box-sizing: border-box; } - [${ATTR_PREFIX}-element="input-left"], - [${ATTR_PREFIX}-element="input-right"] { - position: absolute; - left: 0; + input[type="range"][${ATTR_PREFIX}-element="input-left"], + input[type="range"][${ATTR_PREFIX}-element="input-right"] { + grid-column: 1; + grid-row: 1; width: 100%; max-width: 100%; - top: 50%; - transform: translateY(-50%); margin: 0; padding: 0; box-sizing: border-box; pointer-events: auto; z-index: 2; + height: 1.5rem; + min-height: 1.5rem; + background: transparent; + -webkit-appearance: none !important; + appearance: none !important; + -moz-appearance: none !important; } - [${ATTR_PREFIX}-element="input-right"] { + input[type="range"][${ATTR_PREFIX}-element="input-right"] { z-index: 1; } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-webkit-slider-runnable-track, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-webkit-slider-runnable-track { + height: 6px; + border-radius: 3px; + background: linear-gradient( + to right, + var(--wt-rs-track-fill) 0%, + var(--wt-rs-track-fill) var(--wt-rs-pct, 0%), + var(--wt-rs-track-bg) var(--wt-rs-pct, 0%), + var(--wt-rs-track-bg) 100% + ); + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-webkit-slider-thumb, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-webkit-slider-thumb { + -webkit-appearance: none !important; + width: 12px; + height: 16px; + margin-top: -5px; + border-radius: 8px; + background: var(--wt-rs-thumb-bg); + border: 1px solid var(--wt-rs-thumb-border); + box-shadow: var(--wt-rs-thumb-shadow); + cursor: pointer; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-moz-range-track, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-moz-range-track { + height: 6px; + border-radius: 3px; + background: var(--wt-rs-track-bg); + border: none; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-moz-range-progress, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-moz-range-progress { + height: 6px; + border-radius: 3px; + background: var(--wt-rs-track-fill); + border: none; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-moz-range-thumb, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-moz-range-thumb { + width: 12px; + height: 16px; + border-radius: 8px; + background: var(--wt-rs-thumb-bg); + border: 1px solid var(--wt-rs-thumb-border); + box-shadow: var(--wt-rs-thumb-shadow); + cursor: pointer; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]:focus-visible, + input[type="range"][${ATTR_PREFIX}-element="input-right"]:focus-visible { + outline: 2px solid var(--wt-rs-track-fill); + outline-offset: 2px; + } `; document.head.appendChild(style); } + syncTrackFillPercents() { + if (!this.inputLeft || !this.inputRight) return; + [this.inputLeft, this.inputRight].forEach((input) => { + const min = parseInt(input.min, 10); + const max = parseInt(input.max, 10); + const val = parseInt(input.value, 10); + const safeMin = Number.isFinite(min) ? min : 0; + const safeMax = Number.isFinite(max) ? max : 100; + const safeVal = Number.isFinite(val) ? val : safeMin; + const pct = + safeMax <= safeMin ? 0 : ((safeVal - safeMin) / (safeMax - safeMin)) * 100; + input.style.setProperty('--wt-rs-pct', `${pct}%`); + }); + } + initConfig() { const cfg = this.rs.readSliderConfig(this.slider, ATTR_PREFIX); this.sliderMin = cfg.sliderMin; @@ -213,6 +302,15 @@ class RangeSliderSimple { this.rightSuffix = cfg.rightSuffix; this.defaultSuffix = cfg.defaultSuffix; this.shouldFormatNumber = cfg.shouldFormatNumber; + + const trackFill = this.slider.getAttribute(`${ATTR_PREFIX}-trackfill`); + if (trackFill) { + this.slider.style.setProperty('--wt-rs-track-fill', trackFill); + } + const trackBg = this.slider.getAttribute(`${ATTR_PREFIX}-trackbg`); + if (trackBg) { + this.slider.style.setProperty('--wt-rs-track-bg', trackBg); + } } initElements() { @@ -298,6 +396,8 @@ class RangeSliderSimple { this.shouldFormatNumber, ); } + + this.syncTrackFillPercents(); } updateRightValues(value) { @@ -332,6 +432,8 @@ class RangeSliderSimple { this.defaultSuffix, ); } + + this.syncTrackFillPercents(); } setupEventListeners() { @@ -432,6 +534,15 @@ const initializeRangeSliderSimple = () => { const instance = new RangeSliderSimple(wrapper); window.webtricks.push({ RangeSliderSimple: instance }); }); + + const bumpStyleOrder = () => { + const injectedStyle = document.getElementById('wt-rangeslidersimple-styles'); + if (injectedStyle && document.head) { + document.head.appendChild(injectedStyle); + } + }; + bumpStyleOrder(); + setTimeout(bumpStyleOrder, 0); } catch (err) { console.error(`RangeSliderSimple initialization error: ${err.message}`); } diff --git a/docs/Functional/RangeSlider.md b/docs/Functional/RangeSlider.md index 4ecfab1..f1ebb7a 100644 --- a/docs/Functional/RangeSlider.md +++ b/docs/Functional/RangeSlider.md @@ -50,6 +50,8 @@ Self-contained: **one** script tag. Use [jsDelivr](https://www.jsdelivr.com/) (n - `wt-rangeslider-max="100"` - Maximum value (default: 100) - `wt-rangeslider-steps="1"` - Step size (default: 1) +Style the `[wt-rangeslider-element="range"]` bar in your CSS; examples often use **#3b82f6** for the fill. [RangeSliderSimple](./RangeSliderSimple.md) defaults its track fill to the same for visual parity. + ### Optional Elements - `wt-rangeslider-range="from"` - Form input for start value diff --git a/docs/Functional/RangeSliderSimple.md b/docs/Functional/RangeSliderSimple.md index 136a493..7cdac52 100644 --- a/docs/Functional/RangeSliderSimple.md +++ b/docs/Functional/RangeSliderSimple.md @@ -2,11 +2,11 @@ ## Version -Current version: **0.0.2** (pre-release — see banner in `Dist/Functional/RangeSliderSimple.js`; bump patch and this line when the script changes). +Current version: **0.0.5** (pre-release — see banner in `Dist/Functional/RangeSliderSimple.js`; bump patch and this line when the script changes). ## Description -`RangeSliderSimple` is a dual-handle range control built from two native `` elements. The draggable thumbs you see are the browser’s own controls, so hit targets stay aligned with the visuals. The file is **self-contained** (same core behavior as `RangeSlider`, inlined—keep edits in sync manually if you change constraint/display logic). +`RangeSliderSimple` is a dual-handle range control built from two `` elements. Styling follows the **default MDN / Chrome** pattern (blue filled track, grey remainder, white pill thumb); default fill **#3b82f6** matches typical `RangeSlider` `[range]` bar examples. Hit targets match the painted thumbs. The file is **self-contained** (same core behavior as `RangeSlider`, inlined—keep edits in sync manually if you change constraint/display logic). Use **`RangeSlider`** when you need custom thumb graphics or a separate range bar element. Use **`RangeSliderSimple`** when native appearance (plus your own CSS overrides) is enough. @@ -40,6 +40,17 @@ Same semantics as `RangeSlider`, with the `wt-rangeslidersimple-` prefix: - `wt-rangeslidersimple-min`, `wt-rangeslidersimple-max`, `wt-rangeslidersimple-steps` - `wt-rangeslidersimple-mindifference` - `wt-rangeslidersimple-formatnumber`, `wt-rangeslidersimple-rightsuffix`, `wt-rangeslidersimple-defaultsuffix` +- `wt-rangeslidersimple-trackfill` — optional CSS color for the filled portion of the track (default **#3b82f6**, aligned with common `RangeSlider` `[range]` bar examples) +- `wt-rangeslidersimple-trackbg` — optional unfilled track color (default **#e5e7eb**) + +## Theming (CSS variables) + +On `[wt-rangeslidersimple-element="slider"]` you can override: + +- `--wt-rs-track-fill`, `--wt-rs-track-bg` +- `--wt-rs-thumb-bg`, `--wt-rs-thumb-border`, `--wt-rs-thumb-shadow` + +Injected styling approximates the default **MDN / Chrome** range look (white vertical pill thumb, blue progress, grey track). ## Optional elements (inside wrapper) @@ -79,3 +90,7 @@ Filter-driven min/max for collection ranges applies to **`wt-rangeslider-*`** on After init, instances are available on `window.webtricks` as `{ RangeSliderSimple: instance }`. - `setFrom(value)`, `setTo(value)`, `setRange(from, to)`, `reset()` + +## CodePen / global CSS + +The script injects complete **WebKit / Firefox** range pseudo-element styling so host resets (e.g. CodePen `input { appearance: none }`) do not leave unstyled thumbs. It also moves the injected `