remarqueeble
Version:
Custom element tribute to the cursed glory of marquee.
63 lines (57 loc) • 7.55 kB
JavaScript
/*! remarqueeble v0.6.0 | (c) 2026 Rémino Rem <https://remino.net/> | ISC Licence */
(function(){var e=6,t=`${e}px`,n=85,r=60,i=`200px`,a=`calc(100% - (var(--attr-hspace, 0px) * 2))`,o=`direction`,s=`behavior`,c=`animate`,l=`scrollamount`,u=`scrolldelay`,d=`truespeed`,f=`loop`,p=`bgcolor`,m=`width`,h=`height`,g=`hspace`,_=`vspace`,v=`--attr-width`,y=`--attr-height`,b=`--attr-hspace`,x=`--attr-vspace`,S=`--attr-bgcolor`,C=`--animation-duration`,w=`--animation-direction`,T=`--animation-iteration-count`,E=`--animation-play-state`,D=`--animation-timing-function`,O=`--translate-x-end`,k=`--translate-x-start`,A=`--translate-y-end`,j=`--translate-y-start`,M=globalThis.HTMLElement??class{},N=e=>{if(e===null)return null;let t=e.trim();return t===``?null:/^[+-]?(?:\d+|\d*\.\d+)$/.test(t)?`${t}px`:globalThis.CSS?.supports(`width`,t)?t:null},P=e=>{if(e===null)return null;let t=e.trim();return t===``?null:/^[+-]?(?:\d+|\d*\.\d+)$/.test(t)?Number(t)>=0?`${t}px`:null:globalThis.CSS?.supports(`width`,t)?t:null},F=[{attribute:m,cssVar:v,parser:N},{attribute:h,cssVar:y,parser:N,fallback(e){return e.isVerticalDirection&&!e.hasAttribute(h)?i:null}},{attribute:g,cssVar:b,parser:N},{attribute:_,cssVar:x,parser:N},{attribute:p,cssVar:S,parser:e=>{if(e===null)return null;let t=e.trim();return t===``?null:globalThis.CSS?.supports(`background-color`,t)?t:null}}],I=class extends M{static observedAttributes=[o,s,c,l,u,d,f,p,m,h,g,_];track;scrollAmountProbe;running=!1;constructor(){super();let e=this.attachShadow({mode:`open`});e.innerHTML=`
<style>
:host {
display: inline-block;
text-align: initial;
overflow: hidden !important;
white-space: nowrap;
width: var(${v}, ${a});
height: var(${y}, auto);
margin-inline: var(${b}, 0px);
margin-block: var(${x}, 0px);
background-color: var(${S}, transparent);
box-sizing: border-box;
}
:host([direction="up"]),
:host([direction="down"]) {
white-space: normal;
}
.track {
animation: remarqueeble-motion
var(${C}, 10s)
var(${D}, linear)
var(${T}, infinite)
var(${w}, normal)
both;
animation-play-state: var(${E}, running);
display: inline-block;
will-change: transform;
}
.scrollamount-probe {
block-size: 0;
display: block;
inline-size: ${t};
overflow: hidden;
pointer-events: none;
position: absolute;
visibility: hidden;
}
remarqueeble-motion {
from {
transform: translate(
var(${k}, 100%),
var(${j}, 0px)
);
}
to {
transform: translate(
var(${O}, -100%),
var(${A}, 0px)
);
}
}
</style>
<span class="track"><slot></slot></span>
<span class="scrollamount-probe" aria-hidden="true"></span>
`;let n=e.querySelector(`.track`);if(!n)throw Error(`Remarqueeble track element was not created.`);let r=e.querySelector(`.scrollamount-probe`);if(!r)throw Error(`Remarqueeble scrollamount probe was not created.`);this.track=n,this.scrollAmountProbe=r,this.track.addEventListener(`animationend`,()=>this.handleAnimationEnd())}connectedCallback(){this.running=!0,this.syncPresentationalHints(),requestAnimationFrame(()=>{!this.isConnected||!this.running||this.reset()})}disconnectedCallback(){this.running=!1}attributeChangedCallback(e,t,n){t!==n&&(this.syncPresentationalHints(),this.isConnected&&this.reset())}get direction(){return this.getAttribute(o)||`left`}get behavior(){return this.getAttribute(s)||`scroll`}get scrollAmount(){let n=P(this.getAttribute(l))??t;this.scrollAmountProbe.style.inlineSize=n;let r=this.scrollAmountProbe.getBoundingClientRect().width;return Number.isFinite(r)&&r>=0?r:e}get scrollDelay(){let e=this.getAttribute(u),t=e===null||e.trim()===``?NaN:Number(e),i=Number.isFinite(t)&&t>=0?t:n;return this.hasAttribute(d)?i:Math.max(i,r)}get loop(){let e=this.getAttribute(f);return e===null?-1:Number(e)}get directionSign(){return this.direction===`right`||this.direction===`down`?1:-1}get isVerticalDirection(){return this.direction===`up`||this.direction===`down`}start(){this.running||(this.running=!0,this.reset(),this.syncAnimationPlayState())}stop(){this.running=!1,this.syncAnimationPlayState()}syncPresentationalHints(){for(let e of F)this.syncVar(e);this.syncAnimationPlayState()}syncVar(e){let t=this.getAttribute(e.attribute),n=e.parser(t),r=e.fallback?e.fallback(this):null;if(n==null){r==null?this.style.removeProperty(e.cssVar):this.style.setProperty(e.cssVar,r);return}this.style.setProperty(e.cssVar,n)}reset(){let e=this.getHostSize(),t=this.getTrackSize();this.syncAnimation(e,t)}getHostSize(){return this.isVerticalDirection?this.clientHeight:this.clientWidth}getTrackSize(){return this.isVerticalDirection?this.track.offsetHeight:this.track.offsetWidth}getStartPosition(e,t){return this.directionSign<0?e:-t}getFlushEndPosition(e,t){return this.directionSign<0?0:e-t}getOffEndPosition(e,t){return this.directionSign<0?-t:e}getSlideEndPosition(e,t){return this.directionSign<0?0:e-t}getAlternateStartPosition(e,t){return this.directionSign<0?e-t:0}syncAnimationPlayState(){this.style.setProperty(E,this.running?`running`:`paused`)}syncAnimation(e,t){if(!this.shouldAnimate(e,t)||this.scrollAmount===0){this.syncStaticAnimation();return}let n=this.behavior===`alternate`?this.getAlternateStartPosition(e,t):this.getStartPosition(e,t),r=this.behavior===`slide`?this.getSlideEndPosition(e,t):this.behavior===`alternate`?this.getFlushEndPosition(e,t):this.getOffEndPosition(e,t),i=Math.abs(r-n),a=Math.max(1,Math.ceil(i/Math.max(1,this.scrollAmount))),o=Math.max(1,a*this.scrollDelay),s=this.getCssIterationCount();this.track.style.removeProperty(`transform`),this.style.setProperty(C,`${o}ms`),this.style.setProperty(w,this.behavior===`alternate`?`alternate`:`normal`),this.style.setProperty(T,s),this.style.setProperty(D,`steps(${a}, end)`),this.isVerticalDirection?(this.style.setProperty(k,`0px`),this.style.setProperty(O,`0px`),this.style.setProperty(j,`${n}px`),this.style.setProperty(A,`${r}px`)):(this.style.setProperty(k,`${n}px`),this.style.setProperty(O,`${r}px`),this.style.setProperty(j,`0px`),this.style.setProperty(A,`0px`)),this.restartAnimation()}shouldAnimate(e,t){return this.animationMode===`never`?!1:this.animationMode===`overflow`?t>e:!0}get animationMode(){let e=this.getAttribute(c);return e===`overflow`||e===`never`?e:`always`}syncStaticAnimation(){this.style.setProperty(C,`0ms`),this.style.setProperty(w,`normal`),this.style.setProperty(T,`1`),this.style.setProperty(D,`linear`),this.style.setProperty(k,`0px`),this.style.setProperty(O,`0px`),this.style.setProperty(j,`0px`),this.style.setProperty(A,`0px`),this.track.style.animationName=`none`,this.track.style.transform=`translate(0px, 0px)`}getCssIterationCount(){return this.behavior===`slide`&&!this.hasAttribute(f)?`1`:this.hasAttribute(f)&&Number.isFinite(this.loop)&&this.loop>0?String(this.loop):`infinite`}handleAnimationEnd(){this.hasFiniteAnimation()&&(this.running=!1,this.syncAnimationPlayState())}restartAnimation(){this.track.style.animationName=`none`,this.track.offsetWidth,this.track.style.removeProperty(`animation-name`)}hasFiniteAnimation(){return this.behavior===`slide`&&!this.hasAttribute(f)?!0:this.hasAttribute(f)&&Number.isFinite(this.loop)&&this.loop>0}},L=class extends I{},R=class extends I{};typeof customElements>`u`||(customElements.get(`re-marquee`)||customElements.define(`re-marquee`,L),customElements.get(`re-marquee-ble`)||customElements.define(`re-marquee-ble`,R))})();