diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..0aef9e6 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,48 @@ +name: Playwright Tests + +on: + pull_request: + branches: + - trunk + +# The GITHUB_TOKEN used by Dependabot has read-only permissions by default. We +# provide write permissions to this workflow so that comments can be left on +# the pull request. +# +# @see https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#changing-github_token-permissions +permissions: + contents: read + pull-requests: write + +jobs: + tests_e2e: + name: Run end-to-end tests + runs-on: ubuntu-latest + + steps: + - name: Checkout project + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Verify Node dependency cache + uses: actions/cache@v4 + with: + path: ./node_modules + key: node-dependencies-${{ hashFiles('package-lock.json') }} + restore-keys: node-dependencies- + + - name: Install dependencies + run: npm ci + + - name: Install playwright browsers + run: npx playwright install --with-deps + + - name: Run tests + run: npm run test:e2e diff --git a/.gitignore b/.gitignore index 475f062..b28e474 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules /coverage *.zip +/test-results diff --git a/build/index.js b/build/index.js index c597cff..35360b1 100644 --- a/build/index.js +++ b/build/index.js @@ -1 +1 @@ -class ObserveTriggers{constructor(config={}){this.config={baseTriggerClass:"observe-trigger",baseTriggeredClass:"observe-triggered",baseScrollClass:"observe-scroll",baseScrollingClass:"observe-scrolling",scrollOffsetProperty:"--observe-triggers-scroll-offset",offsetTop:0,...config};this.observers=new Map;this.elementStates=new WeakMap;this.scrollElements=new Set;this.boundScrollHandler=this.handleScroll.bind(this);this.documentMutationObserver=null;this.classMutationObserver=null;this.observerId=0;this.init()}init(){this.observeElements();window.addEventListener("DOMContentLoaded",(()=>this.observeElements()));window.addEventListener("load",(()=>this.observeElements()));this.setupScrolls=this.setupScrolls.bind(this);window.addEventListener("observerTriggered",this.setupScrolls);this.documentMutationObserver=new MutationObserver((mutations=>{Array.from(mutations).filter((mutation=>mutation.type==="childList")).forEach((mutation=>{Array.from(mutation.addedNodes).filter((node=>node.nodeType===Node.ELEMENT_NODE)).forEach((node=>{if(node.matches(`[class*="${this.config.baseTriggerClass}"]`)){Array.from(node.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(node,className)))}node.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{Array.from(element.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(element,className)))}))}))}))}));this.documentMutationObserver.observe(document.body,{childList:true,subtree:true})}parseObserverClass(className){const parts=className.replace(this.config.baseTriggerClass+"-","").split("-");const config={rootMargin:0,threshold:0,edge:"top",action:"toggle",class:this.config.baseTriggeredClass,root:null};let currentPart=0;if(!isNaN(parts[currentPart])){config.rootMargin=parseInt(parts[currentPart]);currentPart++}if(!isNaN(parts[currentPart])){config.threshold=parseInt(parts[currentPart])/100;currentPart++}if(["top","bottom","left","right"].includes(parts[currentPart])){config.edge=parts[currentPart];currentPart++}if(["toggle","add","remove","replace"].includes(parts[currentPart])){config.action=parts[currentPart];currentPart++}const remainingParts=parts.slice(currentPart);const rootIndex=remainingParts.findIndex((part=>part.startsWith("#")||part.startsWith(".")));if(rootIndex!==-1){config.class=remainingParts.slice(0,rootIndex).join("-");config.root=remainingParts.slice(rootIndex).join("-")}else{config.class=remainingParts.join("-")||config.class}return config}observeElements(){document.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)&&!element.getAttribute("data-observer-id")){this.setupObserver(element,className)}}))}))}setupObserver(element,className){element.setAttribute("data-observer-id",`obs-${++this.observerId}`);const config=this.parseObserverClass(className);const rootMargin=100-parseInt(config.rootMargin);const options={root:null,threshold:config.threshold};if("top"===config.edge){const offsetPercentage=this.config.offsetTop/window.innerHeight*100;const adjustedRootMargin=rootMargin-offsetPercentage;options.rootMargin="50% 0px -"+adjustedRootMargin+"% 0px"}else if("bottom"===config.edge){options.rootMargin="-"+rootMargin+"% 0px 50% 0px"}else if("left"===config.edge){options.rootMargin="0px -"+rootMargin+"% 0px 50%"}else if("right"===config.edge){options.rootMargin="0px 50% 0px -"+rootMargin+"%"}const observer=new IntersectionObserver((entries=>{entries.forEach((entry=>{this.handleIntersection(element,entry,config,className)}))}),options);observer.observe(element);if(!this.observers.has(element)){this.observers.set(element,new Map)}this.observers.get(element).set(className,observer,false)}handleIntersection(element,entry,config,className){const elementStates=this.elementStates.get(element)||new Map;const hasTriggered=elementStates.get(className);const isTriggered=entry.intersectionRatio>config.threshold;if(entry.isIntersecting||hasTriggered){switch(config.action){case"add":if(entry.isIntersecting&&isTriggered){element.classList.add(config.class)}break;case"remove":if(entry.isIntersecting&&isTriggered){element.classList.remove(config.class)}break;case"replace":element.classList.forEach((otherClass=>{if(otherClass.startsWith(this.config.baseTriggerClass)){const otherConfig=this.parseObserverClass(otherClass);if(otherConfig.class!==config.class){element.classList.remove(otherConfig.class)}}}));element.classList.add(config.class);break;case"toggle":default:element.classList.toggle(config.class,isTriggered);break}}if(["add","remove"].includes(config.action)&&isTriggered){this.disconnectObserver(element,className);this.observeClassChanges(element)}if(!hasTriggered&&isTriggered){elementStates.set(className,true)}else{elementStates.set(className,false)}this.elementStates.set(element,elementStates);this.dispatchEvent(element,isTriggered,config,className)}disconnectObserver(element,className){const observers=this.observers.get(element);if(observers&&observers.has(className)){observers.get(className).disconnect();observers.delete(className);if(observers.size===0){this.observers.delete(element)}}}dispatchEvent(element,isIntersecting,config,className){const event=new CustomEvent("observerTriggered",{detail:{element:element,isIntersecting:isIntersecting,config:config,className:className}});window.dispatchEvent(event)}destroy(){this.observers.forEach(((observers,element)=>{observers.forEach((observer=>observer.disconnect()))}));this.observers.clear();this.elementStates=new WeakMap;if(this.classMutationObserver){this.classMutationObserver.disconnect();this.classMutationObserver=null}if(this.scrollElements.size>0){window.removeEventListener("scroll",this.boundScrollHandler);this.scrollElements.clear()}}observeClassChanges(element){if(!this.classMutationObserver){this.classMutationObserver=new MutationObserver((mutations=>{mutations.forEach((mutation=>{if(mutation.type==="attributes"&&mutation.attributeName==="class"){const element=mutation.target;const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)){const config=this.parseObserverClass(className);if("add"===config.action&&!element.classList.contains(config.class)){this.setupObserver(element,className)}else if("remove"===config.action&&element.classList.contains(config.class)){this.setupObserver(element,className)}}}))}}))}))}this.classMutationObserver.observe(element,{attributes:true,attributeFilter:["class"]})}setupScrolls(event){const element=event.detail.element;if(event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){if(!element._initialTriggerPosition){element._initialTriggerPosition=element.getBoundingClientRect().top}const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset)}this.scrollElements.add(element);element.classList.add(this.config.baseScrollingClass);if(this.scrollElements.size===1){window.addEventListener("scroll",this.boundScrollHandler,{passive:true})}}else if(!event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset)}element.classList.remove(this.config.baseScrollingClass);this.scrollElements.delete(element);if(this.scrollElements.size===0){window.removeEventListener("scroll",this.boundScrollHandler)}}}handleScroll(){requestAnimationFrame((()=>{for(const element of this.scrollElements){if(element&&element.isConnected){const currentPosition=Math.round(element.getBoundingClientRect().top);const scrollOffset=Math.round(element._initialTriggerPosition-currentPosition);if(element._lastKnownOffset!==undefined){const offsetDifference=Math.round(scrollOffset-element._lastKnownOffset);element.style.setProperty(this.config.scrollOffsetProperty,element._lastKnownOffset+offsetDifference)}else{element.style.setProperty(this.config.scrollOffsetProperty,scrollOffset)}}else{this.scrollElements.delete(element)}}}))}}export default ObserveTriggers; \ No newline at end of file +class ObserveTriggers{constructor(config={}){this.config={baseTriggerClass:"observe-trigger",baseTriggeredClass:"observe-triggered",baseScrollClass:"observe-scroll",baseScrollingClass:"observe-scrolling",scrollOffsetProperty:"--observe-triggers-scroll-offset",offsetTop:0,...config};this.observers=new Map;this.elementStates=new WeakMap;this.scrollElements=new Set;this.boundScrollHandler=this.handleScroll.bind(this);this.documentMutationObserver=null;this.classMutationObserver=null;this.observerId=0;this.init()}init(){this.observeElements();window.addEventListener("DOMContentLoaded",(()=>this.observeElements()));window.addEventListener("load",(()=>this.observeElements()));this.setupScrolls=this.setupScrolls.bind(this);window.addEventListener("observerTriggered",this.setupScrolls);this.documentMutationObserver=new MutationObserver((mutations=>{Array.from(mutations).filter((mutation=>mutation.type==="childList")).forEach((mutation=>{Array.from(mutation.addedNodes).filter((node=>node.nodeType===Node.ELEMENT_NODE)).forEach((node=>{if(node.matches(`[class*="${this.config.baseTriggerClass}"]`)){Array.from(node.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(node,className)))}node.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{Array.from(element.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(element,className)))}))}))}))}));this.documentMutationObserver.observe(document.body,{childList:true,subtree:true})}parseObserverClass(className){const parts=className.replace(this.config.baseTriggerClass+"-","").split("-");const config={rootMargin:0,threshold:0,edge:"top",action:"toggle",class:this.config.baseTriggeredClass,root:null};let currentPart=0;if(!isNaN(parts[currentPart])){config.rootMargin=parseInt(parts[currentPart]);currentPart++}if(!isNaN(parts[currentPart])){config.threshold=parseInt(parts[currentPart])/100;currentPart++}if(["top","bottom","left","right"].includes(parts[currentPart])){config.edge=parts[currentPart];currentPart++}if(["toggle","add","remove","replace"].includes(parts[currentPart])){config.action=parts[currentPart];currentPart++}const remainingParts=parts.slice(currentPart);const rootIndex=remainingParts.findIndex((part=>part.startsWith("#")||part.startsWith(".")));if(rootIndex!==-1){config.class=remainingParts.slice(0,rootIndex).join("-");config.root=remainingParts.slice(rootIndex).join("-")}else{config.class=remainingParts.join("-")||config.class}return config}observeElements(){document.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)&&!element.getAttribute("data-observer-id")){this.setupObserver(element,className)}}))}))}setupObserver(element,className){element.setAttribute("data-observer-id",`obs-${++this.observerId}`);const config=this.parseObserverClass(className);const rootMargin=100-parseInt(config.rootMargin);const options={root:null,threshold:config.threshold};if("top"===config.edge){const offsetPercentage=this.config.offsetTop/window.innerHeight*100;const adjustedRootMargin=rootMargin-offsetPercentage;options.rootMargin="50% 0px -"+adjustedRootMargin+"% 0px"}else if("bottom"===config.edge){options.rootMargin="50% 0px -"+parseInt(config.rootMargin)+"% 0px"}else if("left"===config.edge){options.rootMargin="0px -"+rootMargin+"% 0px 50%"}else if("right"===config.edge){options.rootMargin="0px 50% 0px -"+rootMargin+"%"}const observer=new IntersectionObserver((entries=>{entries.forEach((entry=>{this.handleIntersection(element,entry,config,className)}))}),options);observer.observe(element);if(!this.observers.has(element)){this.observers.set(element,new Map)}this.observers.get(element).set(className,observer,false)}handleIntersection(element,entry,config,className){const elementStates=this.elementStates.get(element)||new Map;const hasTriggered=elementStates.get(className);const isTriggered=entry.intersectionRatio>config.threshold;if(entry.isIntersecting||hasTriggered){switch(config.action){case"add":if(entry.isIntersecting&&isTriggered){element.classList.add(config.class)}break;case"remove":if(entry.isIntersecting&&isTriggered){element.classList.remove(config.class)}break;case"replace":element.classList.forEach((otherClass=>{if(otherClass.startsWith(this.config.baseTriggerClass)){const otherConfig=this.parseObserverClass(otherClass);if(otherConfig.class!==config.class){element.classList.remove(otherConfig.class)}}}));element.classList.add(config.class);break;case"toggle":default:element.classList.toggle(config.class,isTriggered);break}}if(["add","remove"].includes(config.action)&&isTriggered){this.disconnectObserver(element,className);this.observeClassChanges(element)}if(!hasTriggered&&isTriggered){elementStates.set(className,true)}else{elementStates.set(className,false)}this.elementStates.set(element,elementStates);this.dispatchEvent(element,isTriggered,config,className)}disconnectObserver(element,className){const observers=this.observers.get(element);if(observers&&observers.has(className)){observers.get(className).disconnect();observers.delete(className);if(observers.size===0){this.observers.delete(element)}}}dispatchEvent(element,isIntersecting,config,className){const event=new CustomEvent("observerTriggered",{detail:{element:element,isIntersecting:isIntersecting,config:config,className:className}});window.dispatchEvent(event)}destroy(){this.observers.forEach(((observers,element)=>{observers.forEach((observer=>observer.disconnect()))}));this.observers.clear();this.elementStates=new WeakMap;if(this.classMutationObserver){this.classMutationObserver.disconnect();this.classMutationObserver=null}if(this.scrollElements.size>0){window.removeEventListener("scroll",this.boundScrollHandler);this.scrollElements.clear()}}observeClassChanges(element){if(!this.classMutationObserver){this.classMutationObserver=new MutationObserver((mutations=>{mutations.forEach((mutation=>{if(mutation.type==="attributes"&&mutation.attributeName==="class"){const element=mutation.target;const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)){const config=this.parseObserverClass(className);if("add"===config.action&&!element.classList.contains(config.class)){this.setupObserver(element,className)}else if("remove"===config.action&&element.classList.contains(config.class)){this.setupObserver(element,className)}}}))}}))}))}this.classMutationObserver.observe(element,{attributes:true,attributeFilter:["class"]})}setupScrolls(event){const element=event.detail.element;if(event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){if(!element._initialTriggerPosition){element._initialTriggerPosition=element.getBoundingClientRect().top}const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset)}this.scrollElements.add(element);element.classList.add(this.config.baseScrollingClass);if(this.scrollElements.size===1){window.addEventListener("scroll",this.boundScrollHandler,{passive:true})}}else if(!event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset)}element.classList.remove(this.config.baseScrollingClass);this.scrollElements.delete(element);if(this.scrollElements.size===0){window.removeEventListener("scroll",this.boundScrollHandler)}}}handleScroll(){requestAnimationFrame((()=>{for(const element of this.scrollElements){if(element&&element.isConnected){const currentPosition=Math.round(element.getBoundingClientRect().top);const scrollOffset=Math.round(element._initialTriggerPosition-currentPosition);if(element._lastKnownOffset!==undefined){const offsetDifference=Math.round(scrollOffset-element._lastKnownOffset);element.style.setProperty(this.config.scrollOffsetProperty,element._lastKnownOffset+offsetDifference)}else{element.style.setProperty(this.config.scrollOffsetProperty,scrollOffset)}}else{this.scrollElements.delete(element)}}}))}}export default ObserveTriggers; \ No newline at end of file diff --git a/integrations/wordpress/js/build/observe-triggers.js b/integrations/wordpress/js/build/observe-triggers.js index a54d223..8bbe144 100644 --- a/integrations/wordpress/js/build/observe-triggers.js +++ b/integrations/wordpress/js/build/observe-triggers.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - class ObserveTriggers{constructor(config={}){this.config={baseTriggerClass:"observe-trigger",baseTriggeredClass:"observe-triggered",baseScrollClass:"observe-scroll",baseScrollingClass:"observe-scrolling",scrollOffsetProperty:"--observe-triggers-scroll-offset",offsetTop:0,...config};this.observers=new Map;this.elementStates=new WeakMap;this.scrollElements=new Set;this.boundScrollHandler=this.handleScroll.bind(this);this.documentMutationObserver=null;this.classMutationObserver=null;this.observerId=0;this.init();}init(){this.observeElements();window.addEventListener("DOMContentLoaded",(()=>this.observeElements()));window.addEventListener("load",(()=>this.observeElements()));this.setupScrolls=this.setupScrolls.bind(this);window.addEventListener("observerTriggered",this.setupScrolls);this.documentMutationObserver=new MutationObserver((mutations=>{Array.from(mutations).filter((mutation=>mutation.type==="childList")).forEach((mutation=>{Array.from(mutation.addedNodes).filter((node=>node.nodeType===Node.ELEMENT_NODE)).forEach((node=>{if(node.matches(`[class*="${this.config.baseTriggerClass}"]`)){Array.from(node.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(node,className)));}node.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{Array.from(element.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(element,className)));}));}));}));}));this.documentMutationObserver.observe(document.body,{childList:true,subtree:true});}parseObserverClass(className){const parts=className.replace(this.config.baseTriggerClass+"-","").split("-");const config={rootMargin:0,threshold:0,edge:"top",action:"toggle",class:this.config.baseTriggeredClass,root:null};let currentPart=0;if(!isNaN(parts[currentPart])){config.rootMargin=parseInt(parts[currentPart]);currentPart++;}if(!isNaN(parts[currentPart])){config.threshold=parseInt(parts[currentPart])/100;currentPart++;}if(["top","bottom","left","right"].includes(parts[currentPart])){config.edge=parts[currentPart];currentPart++;}if(["toggle","add","remove","replace"].includes(parts[currentPart])){config.action=parts[currentPart];currentPart++;}const remainingParts=parts.slice(currentPart);const rootIndex=remainingParts.findIndex((part=>part.startsWith("#")||part.startsWith(".")));if(rootIndex!==-1){config.class=remainingParts.slice(0,rootIndex).join("-");config.root=remainingParts.slice(rootIndex).join("-");}else {config.class=remainingParts.join("-")||config.class;}return config}observeElements(){document.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)&&!element.getAttribute("data-observer-id")){this.setupObserver(element,className);}}));}));}setupObserver(element,className){element.setAttribute("data-observer-id",`obs-${++this.observerId}`);const config=this.parseObserverClass(className);const rootMargin=100-parseInt(config.rootMargin);const options={root:null,threshold:config.threshold};if("top"===config.edge){const offsetPercentage=this.config.offsetTop/window.innerHeight*100;const adjustedRootMargin=rootMargin-offsetPercentage;options.rootMargin="50% 0px -"+adjustedRootMargin+"% 0px";}else if("bottom"===config.edge){options.rootMargin="-"+rootMargin+"% 0px 50% 0px";}else if("left"===config.edge){options.rootMargin="0px -"+rootMargin+"% 0px 50%";}else if("right"===config.edge){options.rootMargin="0px 50% 0px -"+rootMargin+"%";}const observer=new IntersectionObserver((entries=>{entries.forEach((entry=>{this.handleIntersection(element,entry,config,className);}));}),options);observer.observe(element);if(!this.observers.has(element)){this.observers.set(element,new Map);}this.observers.get(element).set(className,observer,false);}handleIntersection(element,entry,config,className){const elementStates=this.elementStates.get(element)||new Map;const hasTriggered=elementStates.get(className);const isTriggered=entry.intersectionRatio>config.threshold;if(entry.isIntersecting||hasTriggered){switch(config.action){case "add":if(entry.isIntersecting&&isTriggered){element.classList.add(config.class);}break;case "remove":if(entry.isIntersecting&&isTriggered){element.classList.remove(config.class);}break;case "replace":element.classList.forEach((otherClass=>{if(otherClass.startsWith(this.config.baseTriggerClass)){const otherConfig=this.parseObserverClass(otherClass);if(otherConfig.class!==config.class){element.classList.remove(otherConfig.class);}}}));element.classList.add(config.class);break;case "toggle":default:element.classList.toggle(config.class,isTriggered);break}}if(["add","remove"].includes(config.action)&&isTriggered){this.disconnectObserver(element,className);this.observeClassChanges(element);}if(!hasTriggered&&isTriggered){elementStates.set(className,true);}else {elementStates.set(className,false);}this.elementStates.set(element,elementStates);this.dispatchEvent(element,isTriggered,config,className);}disconnectObserver(element,className){const observers=this.observers.get(element);if(observers&&observers.has(className)){observers.get(className).disconnect();observers.delete(className);if(observers.size===0){this.observers.delete(element);}}}dispatchEvent(element,isIntersecting,config,className){const event=new CustomEvent("observerTriggered",{detail:{element:element,isIntersecting:isIntersecting,config:config,className:className}});window.dispatchEvent(event);}destroy(){this.observers.forEach(((observers,element)=>{observers.forEach((observer=>observer.disconnect()));}));this.observers.clear();this.elementStates=new WeakMap;if(this.classMutationObserver){this.classMutationObserver.disconnect();this.classMutationObserver=null;}if(this.scrollElements.size>0){window.removeEventListener("scroll",this.boundScrollHandler);this.scrollElements.clear();}}observeClassChanges(element){if(!this.classMutationObserver){this.classMutationObserver=new MutationObserver((mutations=>{mutations.forEach((mutation=>{if(mutation.type==="attributes"&&mutation.attributeName==="class"){const element=mutation.target;const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)){const config=this.parseObserverClass(className);if("add"===config.action&&!element.classList.contains(config.class)){this.setupObserver(element,className);}else if("remove"===config.action&&element.classList.contains(config.class)){this.setupObserver(element,className);}}}));}}));}));}this.classMutationObserver.observe(element,{attributes:true,attributeFilter:["class"]});}setupScrolls(event){const element=event.detail.element;if(event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){if(!element._initialTriggerPosition){element._initialTriggerPosition=element.getBoundingClientRect().top;}const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset);}this.scrollElements.add(element);element.classList.add(this.config.baseScrollingClass);if(this.scrollElements.size===1){window.addEventListener("scroll",this.boundScrollHandler,{passive:true});}}else if(!event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset);}element.classList.remove(this.config.baseScrollingClass);this.scrollElements.delete(element);if(this.scrollElements.size===0){window.removeEventListener("scroll",this.boundScrollHandler);}}}handleScroll(){requestAnimationFrame((()=>{for(const element of this.scrollElements){if(element&&element.isConnected){const currentPosition=Math.round(element.getBoundingClientRect().top);const scrollOffset=Math.round(element._initialTriggerPosition-currentPosition);if(element._lastKnownOffset!==undefined){const offsetDifference=Math.round(scrollOffset-element._lastKnownOffset);element.style.setProperty(this.config.scrollOffsetProperty,element._lastKnownOffset+offsetDifference);}else {element.style.setProperty(this.config.scrollOffsetProperty,scrollOffset);}}else {this.scrollElements.delete(element);}}}));}} + class ObserveTriggers{constructor(config={}){this.config={baseTriggerClass:"observe-trigger",baseTriggeredClass:"observe-triggered",baseScrollClass:"observe-scroll",baseScrollingClass:"observe-scrolling",scrollOffsetProperty:"--observe-triggers-scroll-offset",offsetTop:0,...config};this.observers=new Map;this.elementStates=new WeakMap;this.scrollElements=new Set;this.boundScrollHandler=this.handleScroll.bind(this);this.documentMutationObserver=null;this.classMutationObserver=null;this.observerId=0;this.init();}init(){this.observeElements();window.addEventListener("DOMContentLoaded",(()=>this.observeElements()));window.addEventListener("load",(()=>this.observeElements()));this.setupScrolls=this.setupScrolls.bind(this);window.addEventListener("observerTriggered",this.setupScrolls);this.documentMutationObserver=new MutationObserver((mutations=>{Array.from(mutations).filter((mutation=>mutation.type==="childList")).forEach((mutation=>{Array.from(mutation.addedNodes).filter((node=>node.nodeType===Node.ELEMENT_NODE)).forEach((node=>{if(node.matches(`[class*="${this.config.baseTriggerClass}"]`)){Array.from(node.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(node,className)));}node.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{Array.from(element.classList).filter((className=>className.startsWith(this.config.baseTriggerClass))).forEach((className=>this.setupObserver(element,className)));}));}));}));}));this.documentMutationObserver.observe(document.body,{childList:true,subtree:true});}parseObserverClass(className){const parts=className.replace(this.config.baseTriggerClass+"-","").split("-");const config={rootMargin:0,threshold:0,edge:"top",action:"toggle",class:this.config.baseTriggeredClass,root:null};let currentPart=0;if(!isNaN(parts[currentPart])){config.rootMargin=parseInt(parts[currentPart]);currentPart++;}if(!isNaN(parts[currentPart])){config.threshold=parseInt(parts[currentPart])/100;currentPart++;}if(["top","bottom","left","right"].includes(parts[currentPart])){config.edge=parts[currentPart];currentPart++;}if(["toggle","add","remove","replace"].includes(parts[currentPart])){config.action=parts[currentPart];currentPart++;}const remainingParts=parts.slice(currentPart);const rootIndex=remainingParts.findIndex((part=>part.startsWith("#")||part.startsWith(".")));if(rootIndex!==-1){config.class=remainingParts.slice(0,rootIndex).join("-");config.root=remainingParts.slice(rootIndex).join("-");}else {config.class=remainingParts.join("-")||config.class;}return config}observeElements(){document.querySelectorAll(`[class*="${this.config.baseTriggerClass}"]`).forEach((element=>{const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)&&!element.getAttribute("data-observer-id")){this.setupObserver(element,className);}}));}));}setupObserver(element,className){element.setAttribute("data-observer-id",`obs-${++this.observerId}`);const config=this.parseObserverClass(className);const rootMargin=100-parseInt(config.rootMargin);const options={root:null,threshold:config.threshold};if("top"===config.edge){const offsetPercentage=this.config.offsetTop/window.innerHeight*100;const adjustedRootMargin=rootMargin-offsetPercentage;options.rootMargin="50% 0px -"+adjustedRootMargin+"% 0px";}else if("bottom"===config.edge){options.rootMargin="50% 0px -"+parseInt(config.rootMargin)+"% 0px";}else if("left"===config.edge){options.rootMargin="0px -"+rootMargin+"% 0px 50%";}else if("right"===config.edge){options.rootMargin="0px 50% 0px -"+rootMargin+"%";}const observer=new IntersectionObserver((entries=>{entries.forEach((entry=>{this.handleIntersection(element,entry,config,className);}));}),options);observer.observe(element);if(!this.observers.has(element)){this.observers.set(element,new Map);}this.observers.get(element).set(className,observer,false);}handleIntersection(element,entry,config,className){const elementStates=this.elementStates.get(element)||new Map;const hasTriggered=elementStates.get(className);const isTriggered=entry.intersectionRatio>config.threshold;if(entry.isIntersecting||hasTriggered){switch(config.action){case "add":if(entry.isIntersecting&&isTriggered){element.classList.add(config.class);}break;case "remove":if(entry.isIntersecting&&isTriggered){element.classList.remove(config.class);}break;case "replace":element.classList.forEach((otherClass=>{if(otherClass.startsWith(this.config.baseTriggerClass)){const otherConfig=this.parseObserverClass(otherClass);if(otherConfig.class!==config.class){element.classList.remove(otherConfig.class);}}}));element.classList.add(config.class);break;case "toggle":default:element.classList.toggle(config.class,isTriggered);break}}if(["add","remove"].includes(config.action)&&isTriggered){this.disconnectObserver(element,className);this.observeClassChanges(element);}if(!hasTriggered&&isTriggered){elementStates.set(className,true);}else {elementStates.set(className,false);}this.elementStates.set(element,elementStates);this.dispatchEvent(element,isTriggered,config,className);}disconnectObserver(element,className){const observers=this.observers.get(element);if(observers&&observers.has(className)){observers.get(className).disconnect();observers.delete(className);if(observers.size===0){this.observers.delete(element);}}}dispatchEvent(element,isIntersecting,config,className){const event=new CustomEvent("observerTriggered",{detail:{element:element,isIntersecting:isIntersecting,config:config,className:className}});window.dispatchEvent(event);}destroy(){this.observers.forEach(((observers,element)=>{observers.forEach((observer=>observer.disconnect()));}));this.observers.clear();this.elementStates=new WeakMap;if(this.classMutationObserver){this.classMutationObserver.disconnect();this.classMutationObserver=null;}if(this.scrollElements.size>0){window.removeEventListener("scroll",this.boundScrollHandler);this.scrollElements.clear();}}observeClassChanges(element){if(!this.classMutationObserver){this.classMutationObserver=new MutationObserver((mutations=>{mutations.forEach((mutation=>{if(mutation.type==="attributes"&&mutation.attributeName==="class"){const element=mutation.target;const classes=Array.from(element.classList);classes.forEach((className=>{if(className.startsWith(this.config.baseTriggerClass)){const config=this.parseObserverClass(className);if("add"===config.action&&!element.classList.contains(config.class)){this.setupObserver(element,className);}else if("remove"===config.action&&element.classList.contains(config.class)){this.setupObserver(element,className);}}}));}}));}));}this.classMutationObserver.observe(element,{attributes:true,attributeFilter:["class"]});}setupScrolls(event){const element=event.detail.element;if(event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){if(!element._initialTriggerPosition){element._initialTriggerPosition=element.getBoundingClientRect().top;}const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset);}this.scrollElements.add(element);element.classList.add(this.config.baseScrollingClass);if(this.scrollElements.size===1){window.addEventListener("scroll",this.boundScrollHandler,{passive:true});}}else if(!event.detail.isIntersecting&&element.classList.contains(this.config.baseScrollClass)){const currentOffset=element.style.getPropertyValue("--observe-scroll-offset");if(currentOffset){element._lastKnownOffset=parseFloat(currentOffset);}element.classList.remove(this.config.baseScrollingClass);this.scrollElements.delete(element);if(this.scrollElements.size===0){window.removeEventListener("scroll",this.boundScrollHandler);}}}handleScroll(){requestAnimationFrame((()=>{for(const element of this.scrollElements){if(element&&element.isConnected){const currentPosition=Math.round(element.getBoundingClientRect().top);const scrollOffset=Math.round(element._initialTriggerPosition-currentPosition);if(element._lastKnownOffset!==undefined){const offsetDifference=Math.round(scrollOffset-element._lastKnownOffset);element.style.setProperty(this.config.scrollOffsetProperty,element._lastKnownOffset+offsetDifference);}else {element.style.setProperty(this.config.scrollOffsetProperty,scrollOffset);}}else {this.scrollElements.delete(element);}}}));}} /** * Determine the height of the admin bar if it is output. diff --git a/package-lock.json b/package-lock.json index f6bc838..b4cb7f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@babel/core": "^7.26.10", "@babel/preset-env": "^7.26.9", "@happyprime/eslint-config": "^0.0.14", + "@playwright/test": "^1.52.0", "@rollup/plugin-node-resolve": "^16.0.1", "archiver": "^7.0.1", "babel-jest": "^29.7.0", @@ -21,6 +22,7 @@ "npm-package-json-lint": "^8.0.0", "npm-package-json-lint-config-default": "^7.0.1", "rollup": "^4.39.0", + "serve": "^14.2.4", "terser": "^5.39.0" } }, @@ -2541,6 +2543,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", @@ -3106,6 +3124,13 @@ "license": "ISC", "peer": true }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -3127,6 +3152,20 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -3215,6 +3254,16 @@ "ajv": ">=5.0.0" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3271,6 +3320,27 @@ "node": ">= 8" } }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -3367,6 +3437,13 @@ "node": ">=14" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -3843,6 +3920,153 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3952,6 +4176,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4081,6 +4315,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -4152,6 +4402,37 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4252,6 +4533,62 @@ "node": ">= 14" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4259,6 +4596,16 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4582,6 +4929,16 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6591,6 +6948,13 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6779,6 +7143,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6932,6 +7312,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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", @@ -7119,6 +7512,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -8442,7 +8848,6 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8496,6 +8901,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8838,6 +9253,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9044,6 +9469,13 @@ "node": ">=0.10.0" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -9085,6 +9517,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9138,6 +9577,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plur": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", @@ -9373,6 +9859,42 @@ "node": ">=8" } }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -9642,6 +10164,30 @@ "node": ">=4" } }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", @@ -9672,6 +10218,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "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", @@ -9932,6 +10488,108 @@ "semver": "bin/semver.js" } }, + "node_modules/serve": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", + "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.12.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.7.4", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.6", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/serve/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/serve/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10982,6 +11640,17 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -11057,6 +11726,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -11236,6 +11915,76 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 16b46e7..5f4918a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@babel/core": "^7.26.10", "@babel/preset-env": "^7.26.9", "@happyprime/eslint-config": "^0.0.14", + "@playwright/test": "^1.52.0", "@rollup/plugin-node-resolve": "^16.0.1", "archiver": "^7.0.1", "babel-jest": "^29.7.0", @@ -26,6 +27,7 @@ "npm-package-json-lint": "^8.0.0", "npm-package-json-lint-config-default": "^7.0.1", "rollup": "^4.39.0", + "serve": "^14.2.4", "terser": "^5.39.0" }, "scripts": { @@ -36,6 +38,9 @@ "fix": "eslint . --fix", "test": "jest", "lint:package": "node ./node_modules/npm-package-json-lint/dist/cli.js ./", - "watch": "nodemon --watch src --ext js --exec 'npm run build'" + "watch": "nodemon --watch src --ext js --exec 'npm run build'", + "test:e2e": "playwright test tests/e2e", + "test:e2e:headed": "playwright test tests/e2e --headed", + "test:e2e:debug": "playwright test tests/e2e --headed --debug" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..993dd89 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from '@playwright/test'; + + export default defineConfig({ + webServer: { + command: 'npx serve ./', + port: 3000, + reuseExistingServer: true, + }, + use: { + baseURL: 'http://localhost:3000', + }, + expect: { timeout: 1_000 } + }); diff --git a/src/index.js b/src/index.js index a29644a..acb2b83 100644 --- a/src/index.js +++ b/src/index.js @@ -229,7 +229,8 @@ class ObserveTriggers { options.rootMargin = '50% 0px -' + adjustedRootMargin + '% 0px'; } else if ('bottom' === config.edge) { - options.rootMargin = '-' + rootMargin + '% 0px 50% 0px'; + options.rootMargin = + '50% 0px -' + parseInt(config.rootMargin) + '% 0px'; } else if ('left' === config.edge) { options.rootMargin = '0px -' + rootMargin + '% 0px 50%'; } else if ('right' === config.edge) { diff --git a/tests/e2e/observe-trigger.spec.js b/tests/e2e/observe-trigger.spec.js new file mode 100644 index 0000000..800959b --- /dev/null +++ b/tests/e2e/observe-trigger.spec.js @@ -0,0 +1,181 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Observe Trigger', () => { + test('toggles class on intersection 20% from top of viewport', async ({ + page, + }) => { + await page.goto( + '/tests/html/observe-trigger-20-top-toggle-example.html' + ); + + const box = page.locator('#test-box'); + + // Initially, the class should not be present + await expect(box).not.toContainClass('example'); + + await page.evaluate(() => { + // Calculate the height of the viewport and the position of the box and + // then scroll so that the box is 20% from the top of the viewport. + const viewportHeight = window.innerHeight; + const boxPosition = document.getElementById('test-box').offsetTop; + const scrollPosition = boxPosition - viewportHeight * 0.2 + 2; + + window.scrollTo(0, scrollPosition); + }); + + // Wait for the intersection observer to trigger + await expect(box).toContainClass('example'); + + // Scroll away (simulate leaving intersection) + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(box).not.toContainClass('example'); + }); + + test('adds class on intersection 50% from top of viewport', async ({ + page, + }) => { + await page.goto('/tests/html/observe-trigger-50-top-add-example.html'); + + const box = page.locator('#test-box'); + + // Initially, the class should not be present + await expect(box).not.toContainClass('example'); + + await page.evaluate(() => { + // Calculate the height of the viewport and the position of the box and + // then scroll so that the box is 50% from the top of the viewport. + const viewportHeight = window.innerHeight; + const boxPosition = document.getElementById('test-box').offsetTop; + const scrollPosition = boxPosition - viewportHeight * 0.5 + 2; + + window.scrollTo(0, scrollPosition); + }); + + // Wait for the intersection observer to trigger + await expect(box).toContainClass('example'); + + // Scroll away (simulate leaving intersection) + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(box).toContainClass('example'); + }); + + test('removes class on intersection 30% from top of viewport', async ({ + page, + }) => { + await page.goto( + '/tests/html/observe-trigger-30-top-remove-example.html' + ); + + const box = page.locator('#test-box'); + + // Initially, the class should be present + await expect(box).toContainClass('example'); + + await page.evaluate(() => { + // Calculate the height of the viewport and the position of the box and + // then scroll so that the box is 30% from the top of the viewport. + const viewportHeight = window.innerHeight; + const boxPosition = document.getElementById('test-box').offsetTop; + const scrollPosition = boxPosition - viewportHeight * 0.3 + 2; + + window.scrollTo(0, scrollPosition); + }); + + // Wait for the intersection observer to trigger + await expect(box).not.toContainClass('example'); + + // Scroll away (simulate leaving intersection) + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(box).not.toContainClass('example'); + }); + + test('toggles class on intersection 20% from bottom of viewport', async ({ + page, + }) => { + await page.goto( + '/tests/html/observe-trigger-20-bottom-toggle-example.html' + ); + + const box = page.locator('#test-box'); + + // Initially, the class should not be present + await expect(box).not.toContainClass('example'); + + await page.evaluate(() => { + // Calculate the height of the viewport and the position of the box and + // then scroll so that the box is 30% from the top of the viewport. + const viewportHeight = window.innerHeight; + const boxPosition = document.getElementById('test-box').offsetTop; + const scrollPosition = boxPosition - viewportHeight * 0.8 + 2; + + window.scrollTo(0, scrollPosition); + }); + + // Wait for the intersection observer to trigger + await expect(box).toContainClass('example'); + + // Scroll away (simulate leaving intersection) + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(box).not.toContainClass('example'); + }); + + test('adds class on intersection 50% from bottom of viewport', async ({ + page, + }) => { + await page.goto( + '/tests/html/observe-trigger-50-bottom-add-example.html' + ); + + const box = page.locator('#test-box'); + + // Initially, the class should not be present + await expect(box).not.toContainClass('example'); + + await page.evaluate(() => { + // Calculate the height of the viewport and the position of the box and + // then scroll so that the box is 30% from the top of the viewport. + const viewportHeight = window.innerHeight; + const boxPosition = document.getElementById('test-box').offsetTop; + const scrollPosition = boxPosition - viewportHeight * 0.5 + 2; + + window.scrollTo(0, scrollPosition); + }); + + // Wait for the intersection observer to trigger + await expect(box).toContainClass('example'); + + // Scroll away (simulate leaving intersection) + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(box).toContainClass('example'); + }); + + test('removes class on intersection 30% from bottom of viewport', async ({ + page, + }) => { + await page.goto( + '/tests/html/observe-trigger-30-bottom-remove-example.html' + ); + + const box = page.locator('#test-box'); + + // Initially, the class should be present + await expect(box).toContainClass('example'); + + await page.evaluate(() => { + // Calculate the height of the viewport and the position of the box and + // then scroll so that the box is 30% from the top of the viewport. + const viewportHeight = window.innerHeight; + const boxPosition = document.getElementById('test-box').offsetTop; + const scrollPosition = boxPosition - viewportHeight * 0.7 + 2; + + window.scrollTo(0, scrollPosition); + }); + + // Wait for the intersection observer to trigger + await expect(box).not.toContainClass('example'); + + // Scroll away (simulate leaving intersection) + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(box).not.toContainClass('example'); + }); +}); diff --git a/tests/html/observe-trigger-20-bottom-toggle-example.html b/tests/html/observe-trigger-20-bottom-toggle-example.html new file mode 100644 index 0000000..54a1653 --- /dev/null +++ b/tests/html/observe-trigger-20-bottom-toggle-example.html @@ -0,0 +1,40 @@ + + + + + + Observe Trigger 20 Bottom Toggle Example + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+

This box should have the `example` class when the bottom of the box crosses 20% from the bottom of the viewport.

+
+
+
+ + + + diff --git a/tests/html/observe-trigger-20-top-toggle-example.html b/tests/html/observe-trigger-20-top-toggle-example.html new file mode 100644 index 0000000..6dadb28 --- /dev/null +++ b/tests/html/observe-trigger-20-top-toggle-example.html @@ -0,0 +1,40 @@ + + + + + + Observe Trigger 20 Top Toggle Example + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+

This box should have the `example` class when the top of the box crosses 20% from the top of the viewport.

+
+
+
+ + + + diff --git a/tests/html/observe-trigger-30-bottom-remove-example.html b/tests/html/observe-trigger-30-bottom-remove-example.html new file mode 100644 index 0000000..2286820 --- /dev/null +++ b/tests/html/observe-trigger-30-bottom-remove-example.html @@ -0,0 +1,40 @@ + + + + + + Observe Trigger 30 Bottom Remove Example + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+

This box should have its `example` class removed when the bottom of the box crosses 30% from the bottom of the viewport. Once the class is removed, it should remain removed.

+
+
+
+ + + + diff --git a/tests/html/observe-trigger-30-top-remove-example.html b/tests/html/observe-trigger-30-top-remove-example.html new file mode 100644 index 0000000..d88522d --- /dev/null +++ b/tests/html/observe-trigger-30-top-remove-example.html @@ -0,0 +1,40 @@ + + + + + + Observe Trigger 30 Top Remove Example + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+

This box should have its `example` class removed when the top of the box crosses 30% from the top of the viewport. Once the class is removed, it should remain removed.

+
+
+
+ + + + diff --git a/tests/html/observe-trigger-50-bottom-add-example.html b/tests/html/observe-trigger-50-bottom-add-example.html new file mode 100644 index 0000000..dfb0670 --- /dev/null +++ b/tests/html/observe-trigger-50-bottom-add-example.html @@ -0,0 +1,40 @@ + + + + + + Observe Trigger 50 Bottom Add Example + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+

This box should have the `example` class when the bottom of the box crosses 50% from the bottom of the viewport. Once it has the class, it should remain.

+
+
+
+ + + + diff --git a/tests/html/observe-trigger-50-top-add-example.html b/tests/html/observe-trigger-50-top-add-example.html new file mode 100644 index 0000000..099d256 --- /dev/null +++ b/tests/html/observe-trigger-50-top-add-example.html @@ -0,0 +1,40 @@ + + + + + + Observe Trigger 50 Top Add Example + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+

This box should have the `example` class when the top of the box crosses 50% from the top of the viewport. Once it has the class, it should remain.

+
+
+
+ + + +