206 lines
5.1 KiB
JavaScript
206 lines
5.1 KiB
JavaScript
const TEMPLATE = document.createElement("template");
|
|
TEMPLATE.innerHTML = `<style>
|
|
:host {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: flex-start;
|
|
align-items: flex-start;
|
|
|
|
overflow: auto;
|
|
width: 100%;
|
|
scrollbar-width: none;
|
|
|
|
--markee-speed: 100;
|
|
--markee-play-state: running;
|
|
--markee-interaction-pause-delay: 3s;
|
|
}
|
|
|
|
.host::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
::slotted(*) {
|
|
flex-shrink: 0;
|
|
width: fit-content;
|
|
}
|
|
</style><slot></slot>`;
|
|
|
|
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);
|