diff --git a/build/index.js b/build/index.js index 5029133..9df6773 100644 --- a/build/index.js +++ b/build/index.js @@ -1 +1 @@ -class ObserveTriggers{constructor(config={}){this.config={baseTriggerClass:"observe-trigger",baseTriggeredClass:"observe-triggered",...config};this.observers=new Map;this.elementStates=new WeakMap;this.init()}init(){this.observeElements();window.addEventListener("DOMContentLoaded",(()=>this.observeElements()));window.addEventListener("load",(()=>this.observeElements()))}parseObserverClass(className){const parts=className.replace(this.config.baseTriggerClass+"-","").split("-");const config={rootMargin:0,threshold:0,edge:"top",action:"toggle",class:"observe-triggered",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)){this.setupObserver(element,className)}}))}))}setupObserver(element,className){const config=this.parseObserverClass(className);const rootMargin=100-parseInt(config.rootMargin);const options={root:null,threshold:config.threshold};if("top"===config.edge){options.rootMargin="50% 0px -"+rootMargin+"% 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(config.action.includes(["add","remove"])&&isTriggered){this.disconnectObserver(element,className)}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.clear()}}export default ObserveTriggers; \ No newline at end of file +class ObserveTriggers{constructor(config={}){this.config={baseTriggerClass:"observe-trigger",baseTriggeredClass:"observe-triggered",...config};this.observers=new Map;this.elementStates=new WeakMap;this.scrollers=new Map;this.scrollersActive=false;this.handleScrollers=this.handleScrollers.bind(this);this.viewportHeight=false;this.init()}init(){this.observeElements();window.addEventListener("DOMContentLoaded",(()=>this.observeElements()));window.addEventListener("load",(()=>this.observeElements()))}parseObserverClass(className){const parts=className.replace(this.config.baseTriggerClass+"-","").split("-");const config={rootMargin:0,threshold:0,edge:"top",action:"toggle",class:"observe-triggered",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","scroll"].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)){this.setupObserver(element,className)}}))}))}setupObserver(element,className){const config=this.parseObserverClass(className);const rootMargin=100-parseInt(config.rootMargin);const options={root:null,threshold:config.threshold};if("top"===config.edge){options.rootMargin="50% 0px -"+rootMargin+"% 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)}handleScrollers(scrollEvent){if(!this.scrollers){return}this.scrollers.forEach((({classes:classes,config:config},element)=>{if(false===this.viewportHeight){this.viewportHeight=window.innerHeight}if(classes.includes("observe-scroll")){let hasScrolledAmount=this.viewportHeight*(config.rootMargin/100)-element.getBoundingClientRect().top;if(hasScrolledAmount<0){hasScrolledAmount=0}element.style.setProperty("--distance-from-trigger",hasScrolledAmount)}}))}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"scroll":if(entry.isIntersecting&&!this.scrollers.has(element)&&isTriggered){const scrollerClasses=[];element.classList.forEach((className=>{if(className.startsWith("observe-scroll")){scrollerClasses.push(className)}}));this.scrollers.set(element,{classes:scrollerClasses,config:config})}else if(!entry.isIntersecting&&this.scrollers.has(element)){this.scrollers.delete(element)}if(this.scrollers.size>0&&!this.scrollersActive){window.addEventListener("scroll",this.handleScrollers,{passive:true});this.scrollersActive=true}else if(this.scrollers.size===0){window.removeEventListener("scroll",this.handleScrollers,{passive:true});this.scrollersActive=false}break;case"toggle":default:element.classList.toggle(config.class,isTriggered);break}}if(config.action.includes(["add","remove"])&&isTriggered){this.disconnectObserver(element,className)}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.clear()}}export default ObserveTriggers; \ No newline at end of file diff --git a/demo/css/style.css b/demo/css/style.css index d86f7a6..6a37f49 100644 --- a/demo/css/style.css +++ b/demo/css/style.css @@ -6,7 +6,11 @@ body { .container { position: relative; - padding: 100vh 0; /* Add padding to allow scrolling */ + padding: 100vh 0 200vh 0; /* Add padding to allow scrolling */ +} + +.scrollers .container { + padding-top: 20vh; } .horizontal-line { @@ -36,8 +40,13 @@ body { position: relative; } +.double-box { + height: 200px; +} + +.demo-box::before, .demo-box::after { - content: attr(class); + content: attr(style); position: absolute; bottom: -20px; left: 0; @@ -50,9 +59,26 @@ body { background-color: rgba(255, 255, 255, 0.8); padding: 2px; border-radius: 3px; + width: fit-content; +} + +.demo-box::after { + content: attr(class); + top: -20px; + bottom: initial; } .observe-triggered { transform: scale(1.1); box-shadow: 0 0 10px rgba(0,0,0,0.5); } + +.animated-left { + transform: translateX( calc( var( --distance-from-trigger ) * -1px ) ); + transition: transform 50ms linear; +} + +.animated-right { + transform: translateX( calc( var( --distance-from-trigger ) * 1px ) ); + transition: transform 50ms linear; +} diff --git a/demo/scrollers.html b/demo/scrollers.html new file mode 100644 index 0000000..ef4735f --- /dev/null +++ b/demo/scrollers.html @@ -0,0 +1,46 @@ + + +
+ + +