const TEMPLATE = document.createElement("template"); TEMPLATE.innerHTML = ``; const CHECK_OPTIONS_INTERVAL = 100; class MarkeeComponent extends HTMLElement { #options = { speed: 0, scrollPauseDelay: 1000, playing: true, scrollPlaying: true, }; #lastFrame = 0; #animationFrameRequest = -1; #checkOptionInterval = -1; #scrollPlayingTimeout = -1; /** @type {{element: Element, rect: DOMRect, margin: number}?} */ #nextCollectedElement = null; constructor() { super(); this.requestTick = this.requestTick.bind(this); this.updateOptions = this.updateOptions.bind(this); this.attachShadow({ mode: "open" }); this.shadowRoot.appendChild(TEMPLATE.content.cloneNode(true)); } tick() { let now = Date.now(); let delta = (now - this.#lastFrame) / 1000; this.#lastFrame = now; if (!this.#options.playing || !this.#options.scrollPlaying || this.scrollWidth - 800 <= this.clientWidth) return; this.scrollLeft += Math.max(this.#options.speed * delta, 1); this.checkElementRecycling(); } requestTick() { this.tick(); if (this.#options.playing) { this.#animationFrameRequest = requestAnimationFrame(this.requestTick, 30); } } updateOptions() { let style = window.getComputedStyle(this); let speed = parseFloat(style.getPropertyValue("--markee-speed")); if (!Number.isNaN(speed)) { this.#options.speed = speed; } let wasPlaying = this.#options.playing; let playState = style.getPropertyValue("--markee-play-state"); if (playState) { this.#options.playing = playState != "paused"; } else { this.#options.playing = true; } let scrollPauseDelay = style.getPropertyValue( "--markee-interaction-pause-delay" ); if (scrollPauseDelay) { let value = parseFloat(scrollPauseDelay); if (scrollPauseDelay.endsWith("ms")) { this.#options.scrollPauseDelay = value; } else if (scrollPauseDelay.endsWith("s")) { this.#options.scrollPauseDelay = value * 1000; } else { this.#options.scrollPauseDelay = 1000; } } if (wasPlaying != this.#options.playing) { this.#lastFrame = Date.now(); cancelAnimationFrame(this.#animationFrameRequest); this.requestTick(); } } checkElementRecycling() { if ( this.children.length > 0 && (!this.#nextCollectedElement || this.#nextCollectedElement.element != this.children[0]) ) { let element = this.children[0]; { let i = 0; while(element.style.display == "none" && i < this.children.length){ i += 1; element = this.children[i] } } let style = window.getComputedStyle(element); this.#nextCollectedElement = { element, rect: element.getBoundingClientRect(), margin: parseFloat(style.marginLeft) + parseFloat(style.marginRight), }; } if (!this.#nextCollectedElement) return; let lastElement = this.children[this.children.length-1]; { let i = lastElement.children.length-1; while(lastElement && lastElement.style.display == "none" && i >= 0){ i -= 1; lastElement = this.children[i] } } if (lastElement) { let rect = lastElement.getBoundingClientRect(); let thisRect = this.getBoundingClientRect(); let style = window.getComputedStyle(lastElement); let margin = parseFloat(style.marginLeft) + parseFloat(style.marginRight); if (thisRect.width == 0) { return; } if (rect.right + margin < thisRect.left + this.clientWidth) { return; } } if ( this.scrollLeft >= this.scrollWidth - this.clientWidth - this.#nextCollectedElement.rect.width - this.#nextCollectedElement.margin ) { this.scrollLeft -= this.#nextCollectedElement.rect.width + this.#nextCollectedElement.margin; this.appendChild(this.#nextCollectedElement.element); this.#nextCollectedElement = null; } } handlePointer(e) { this.#options.scrollPlaying = false; clearTimeout(this.#scrollPlayingTimeout); this.#scrollPlayingTimeout = setTimeout(() => { this.#options.scrollPlaying = true; }, this.#options.scrollPauseDelay); } connectedCallback() { this.updateOptions(); this.#lastFrame = Date.now(); this.requestTick(); this.#checkOptionInterval = setInterval( this.updateOptions, CHECK_OPTIONS_INTERVAL ); } disconnectedCallback() { clearInterval(this.#checkOptionInterval); } } customElements.define("gavle-marquee", MarkeeComponent);