@magic-spells/scrolling-content
Version:
Scrolling content web component with smooth infinite animation.
2 lines (1 loc) • 6.52 kB
JavaScript
!function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";class ScrollingTrack extends HTMLElement{connectedCallback(){this.style.display="flex",this.style.flexWrap="nowrap",this.style.alignItems="center",this.style.gap="var(--scrolling-content-gap, 1rem)",this.style.cursor="pointer",this.style.touchAction="pan-y",this.dataset.gap&&(this.style.gap=`${parseFloat(this.dataset.gap)}px`)}}customElements.define("scrolling-track",ScrollingTrack);class ScrollingItem extends HTMLElement{connectedCallback(){this.style.display="flex",this.style.alignItems="center",this.style.gap="var(--scrolling-content-gap, 1rem)",this.dataset.pad&&(this.style.padding=`${parseFloat(this.dataset.pad)}px`)}}customElements.define("scrolling-item",ScrollingItem);class ScrollingContent extends HTMLElement{static get observedAttributes(){return["data-mobile-speed","data-desktop-speed","data-breakpoint"]}constructor(){super();const t=this;t.track=null,t.items=[],t.mobileSpeed=40,t.desktopSpeed=60,t.breakpoint=767,t.isRunning=!1,t.isHoverPaused=!1,t.isDragging=!1,t.prevTime=0,t.offsetX=0,t.dragStartX=0,t.dragStartY=0,t.startOffset=0,t.containerWidth=0,t.loopDistance=0,t.rafId=null,t.recalculateLayout=t.throttle((()=>t.doRecalculateLayout()),5),t.resizeHandler=()=>t.handleResize(),t.focusHandler=()=>t.handleFocus(),t.touchDirection=0,t.directionThreshold=3}throttle(t,e){let n;return(...s)=>{clearTimeout(n),n=setTimeout((()=>t.apply(this,s)),e)}}connectedCallback(){const t=this;t.initElements(),t.readAttributes(),setTimeout((()=>{t.checkTrackWidth(),t.attachEvents(),t.start()}),5)}attributeChangedCallback(t,e,n){e!==n&&(this.readAttributes(),this.isHoverPaused||this.isDragging||(this.stop(),this.start()))}initElements(){const t=this;if(t.style.overflow="hidden",t.track=t.querySelector("scrolling-track"),!t.track){for(t.track=document.createElement("scrolling-track");t.firstChild;)t.track.appendChild(t.firstChild);t.appendChild(t.track)}if(!t.track.querySelector("scrolling-item")){const e=document.createElement("scrolling-item");for(;t.track.firstChild;)e.appendChild(t.track.firstChild);t.track.appendChild(e)}t.items=Array.from(t.track.children),Object.assign(t.track.style,{display:"flex",willChange:"transform"}),t.containerWidth=t.getBoundingClientRect().width}readAttributes(){const t=this,e=(e,n)=>isNaN(parseFloat(t.getAttribute(e)))?n:parseFloat(t.getAttribute(e));t.mobileSpeed=e("data-mobile-speed",t.mobileSpeed),t.desktopSpeed=e("data-desktop-speed",t.desktopSpeed),t.breakpoint=e("data-breakpoint",t.breakpoint)}checkTrackWidth(){const t=this;if(!t.items.length)return;const e=t.items[0].getBoundingClientRect().width,n=getComputedStyle(t.track),s=parseFloat(n.gap)||0;t.loopDistance=e+s;const i=Math.ceil(2*t.containerWidth/e)+1;for(let e=t.items.length;e<i;e++){const e=t.items[0].cloneNode(!0);t.track.appendChild(e)}t.items=Array.from(t.track.children)}attachEvents(){const t=this;t.addEventListener("mouseenter",(()=>{t.isHoverPaused=!0,t.stop()})),t.addEventListener("mouseleave",(()=>{t.isHoverPaused=!1,t.isDragging||t.start()})),t.track.addEventListener("touchstart",(e=>t.onTouchStart(e)),{passive:!1}),t.track.addEventListener("touchmove",(e=>t.onTouchMove(e)),{passive:!1}),t.track.addEventListener("touchend",(e=>t.onTouchEnd(e))),t.track.addEventListener("touchcancel",(e=>t.onTouchEnd(e))),t.track.addEventListener("pointerdown",(e=>t.onPointerDown(e))),window.addEventListener("pointermove",(e=>t.onPointerMove(e))),window.addEventListener("pointerup",(e=>t.onPointerUp(e))),window.addEventListener("pointercancel",(e=>t.onPointerUp(e))),window.addEventListener("resize",t.resizeHandler),window.addEventListener("focus",t.focusHandler)}start(){const t=this;t.isRunning||(t.isRunning=!0,t.prevTime=performance.now(),t.rafId=requestAnimationFrame((e=>t.tick(e))))}stop(){const t=this;t.isRunning&&(cancelAnimationFrame(t.rafId),t.isRunning=!1,t.rafId=null)}tick(t){const e=this;if(!e.isRunning)return;const n=(t-e.prevTime)/1e3;e.prevTime=t,e.offsetX-=e.getCurrentSpeed()*n,e.offsetX<=-e.loopDistance&&(e.offsetX+=e.loopDistance),e.track.style.transform=`translateX(${e.offsetX}px)`,e.rafId=requestAnimationFrame((t=>e.tick(t)))}getCurrentSpeed(){return window.innerWidth<=this.breakpoint?this.mobileSpeed:this.desktopSpeed}onTouchStart(t){const e=this;1===t.touches.length&&(e.isDragging=!0,e.dragStartX=t.touches[0].screenX,e.dragStartY=t.touches[0].screenY,e.startOffset=e.offsetX,e.touchDirection=0,e.stop())}onTouchMove(t){const e=this;if(!e.isDragging||1!==t.touches.length)return;if(1===e.touchDirection){t.preventDefault();const n=t.touches[0].screenX-e.dragStartX;for(e.offsetX=e.startOffset+n;e.offsetX<=-e.loopDistance;)e.offsetX+=e.loopDistance;for(;e.offsetX>0;)e.offsetX-=e.loopDistance;return void(e.track.style.transform=`translateX(${e.offsetX}px)`)}const n=Math.abs(e.dragStartX-t.touches[0].screenX),s=Math.abs(e.dragStartY-t.touches[0].screenY);return 1.15*n>s&&(n>e.directionThreshold||s>e.directionThreshold)?(e.touchDirection=1,void t.preventDefault()):s>e.directionThreshold&&n<=s?(e.touchDirection=-1,e.isDragging=!1,void(e.isHoverPaused||e.start())):void t.preventDefault()}onTouchEnd(){const t=this;t.isDragging&&(t.isDragging=!1,t.touchDirection=0,t.isHoverPaused||t.start())}onPointerDown(t){const e=this;"touch"!==t.pointerType&&(e.isDragging=!0,e.dragStartX=t.clientX,e.dragStartY=t.clientY,e.startOffset=e.offsetX,e.touchDirection=0,e.stop(),e.track.setPointerCapture(t.pointerId))}onPointerMove(t){const e=this;if(!e.isDragging)return;if("touch"===t.pointerType)return;const n=t.clientX-e.dragStartX;for(e.offsetX=e.startOffset+n;e.offsetX<=-e.loopDistance;)e.offsetX+=e.loopDistance;for(;e.offsetX>0;)e.offsetX-=e.loopDistance;e.track.style.transform=`translateX(${e.offsetX}px)`}onPointerUp(t){const e=this;if(e.isDragging&&"touch"!==t.pointerType){e.isDragging=!1,e.touchDirection=0;try{e.track.releasePointerCapture(t.pointerId)}catch{}e.isHoverPaused||e.start()}}doRecalculateLayout(){const t=this;setTimeout((()=>{for(t.containerWidth=t.getBoundingClientRect().width,t.checkTrackWidth(),t.offsetX=t.offsetX%t.loopDistance;t.offsetX<=-t.loopDistance;)t.offsetX+=t.loopDistance;for(;t.offsetX>0;)t.offsetX-=t.loopDistance;t.track.style.transform=`translateX(${t.offsetX}px)`,t.isHoverPaused||t.isDragging||t.start()}),1)}handleResize(){const t=this;t.getBoundingClientRect().width!==t.containerWidth&&t.recalculateLayout()}handleFocus(){this.recalculateLayout()}}customElements.define("scrolling-content",ScrollingContent)}));