// usage: // import WebComponentBase from "../web-component-base/web-component-base.js"; import { Assert } from "arslib"; class ArsDataRoller extends WebComponentBase { [key: string]: any; // ---- PRIVATE STATIC UTILITY METHODS ---- static #DEFAULT_INTERVAL = 3000; static #DEFAULT_ANIMATION_DURATION = 500; static #createRollerItem(item: unknown) { if (typeof item === 'string') { return `${item}`; } else if (typeof item === 'object' && item !== null && 'title' in item && 'value' in item) { const obj = item as { title: string; value: unknown }; return `${obj.title}: ${obj.value}`; } else if (typeof item === 'object' && item !== null) { return `${Object.entries(item).map(([k, v]) => `${k}: ${v}`).join(' | ')}`; } return `${JSON.stringify(item)}`; } static #createTemplate() { return `
`; } static #parseDataAttribute(value: string) { try { if (!value || value.trim() === '') return []; Assert.assertIsValidJSON(value, '[ars-data-roller] data attribute is not valid JSON'); return JSON.parse(value); } catch (e) { console.error('[ars-data-roller] Invalid JSON for data attribute:', value, e); return []; } } static #parseIntervalAttribute(value: string) { const parsed = parseInt(value); return isNaN(parsed) ? ArsDataRoller.#DEFAULT_INTERVAL : parsed; } static #parseAnimationDurationAttribute(value: string) { const parsed = parseInt(value); return isNaN(parsed) ? ArsDataRoller.#DEFAULT_ANIMATION_DURATION : parsed; } // ---- PRIVATE INSTANCE METHODS ---- #initializeRoller() { if (!this.shadowRoot) { this.attachShadow({ mode: "open" }); } const shadowRoot = this.shadowRoot; if (!shadowRoot) return; shadowRoot.innerHTML = ArsDataRoller.#createTemplate(); this.#render(); this.#startRolling(); } #render() { if (!this.shadowRoot) return; const current = this.shadowRoot.querySelector('.roller-item.current'); const next = this.shadowRoot.querySelector('.roller-item.next'); if (!current || !next) return; const currentItem = this.data[this.currentIndex] || ''; const nextItem = this.data[(this.currentIndex + 1) % this.data.length] || ''; current.innerHTML = ArsDataRoller.#createRollerItem(currentItem); next.innerHTML = ArsDataRoller.#createRollerItem(nextItem); } #startRolling() { this.#stopRolling(); if (this.data.length > 1) { this.interval = setInterval(() => this.#nextItem(), this.intervalMs); } } #stopRolling() { if (this.interval) { clearInterval(this.interval); this.interval = null; } } #nextItem() { if (this.animating || this.data.length < 2) return; this.animating = true; this.nextIndex = (this.currentIndex + 1) % this.data.length; this.#animateRoll(); } #animateRoll() { const shadowRoot = this.shadowRoot; if (!shadowRoot) { this.animating = false; return; } const current = shadowRoot.querySelector('.roller-item.current'); const next = shadowRoot.querySelector('.roller-item.next'); if (!current || !next) { this.animating = false; return; } // Reset styles before animation (current as HTMLElement).style.transition = 'none'; (current as HTMLElement).style.transform = 'translateY(0%) rotateX(0deg)'; (current as HTMLElement).style.opacity = '1'; (next as HTMLElement).style.transition = 'none'; (next as HTMLElement).style.transform = 'translateY(60%) rotateX(-90deg)'; (next as HTMLElement).style.opacity = '0'; (next as HTMLElement).style.zIndex = '2'; (next as HTMLElement).style.display = 'block'; // Animate both setTimeout(() => { (current as HTMLElement).style.transition = `transform ${this.animationDuration}ms cubic-bezier(0.4,0.2,0.2,1), opacity ${this.animationDuration}ms`; (next as HTMLElement).style.transition = `transform ${this.animationDuration}ms cubic-bezier(0.4,0.2,0.2,1), opacity ${this.animationDuration}ms`; (current as HTMLElement).style.transform = 'translateY(-60%) rotateX(90deg)'; (current as HTMLElement).style.opacity = '0'; (next as HTMLElement).style.transform = 'translateY(0%) rotateX(0deg)'; (next as HTMLElement).style.opacity = '1'; // After animation, swap and reset styles for next cycle setTimeout(() => { this.currentIndex = this.nextIndex; this.animating = false; this.#render(); // Reset styles for next cycle const newCurrent = shadowRoot.querySelector('.roller-item.current'); const newNext = shadowRoot.querySelector('.roller-item.next'); if (newCurrent && newNext) { (newCurrent as HTMLElement).style.transition = 'none'; (newCurrent as HTMLElement).style.transform = 'translateY(0%) rotateX(0deg)'; (newCurrent as HTMLElement).style.opacity = '1'; (newNext as HTMLElement).style.transition = 'none'; (newNext as HTMLElement).style.transform = 'translateY(60%) rotateX(-90deg)'; (newNext as HTMLElement).style.opacity = '0'; (newNext as HTMLElement).style.zIndex = '2'; (newNext as HTMLElement).style.display = 'block'; } }, this.animationDuration); }, 10); } // ---- CONSTRUCTOR AND LIFECYCLE ---- constructor() { super(); this.currentIndex = 0; this.interval = null; this.data = []; this.intervalMs = ArsDataRoller.#DEFAULT_INTERVAL; this.animationDuration = ArsDataRoller.#DEFAULT_ANIMATION_DURATION; this.animating = false; this.nextIndex = null; } connectedCallback() { super.connectedCallback(); this.#initializeRoller(); } disconnectedCallback() { super.disconnectedCallback(); this.#stopRolling(); } static get observedAttributes() { return ["data", "interval", "animation-duration"]; } static defaultAttributeValue(name: string) { switch (name) { case "data": return "[]"; case "interval": return ArsDataRoller.#DEFAULT_INTERVAL.toString(); case "animation-duration": return ArsDataRoller.#DEFAULT_ANIMATION_DURATION.toString(); default: return null; } } static parseAttributeValue(name: string, value: string) { switch (name) { case "data": return ArsDataRoller.#parseDataAttribute(value); case "interval": return ArsDataRoller.#parseIntervalAttribute(value); case "animation-duration": return ArsDataRoller.#parseAnimationDurationAttribute(value); default: return super.parseAttributeValue(name, value); } } attributeChangedCallback(attrName: string, oldVal: string | null, newVal: string | null) { super.attributeChangedCallback(attrName, oldVal, newVal); if (oldVal === newVal) return; // Only update UI if shadowRoot is ready const shadowReady = this.shadowRoot && this.shadowRoot.querySelector('.roller-item.current'); switch (attrName) { case "data": this.currentIndex = 0; this.data = ArsDataRoller.#parseDataAttribute(newVal ?? "[]"); if (shadowReady) { this.#render(); this.#startRolling(); } break; case "interval": this.intervalMs = ArsDataRoller.#parseIntervalAttribute(newVal ?? "3000"); if (shadowReady) this.#startRolling(); break; case "animation-duration": this.animationDuration = ArsDataRoller.#parseAnimationDurationAttribute(newVal ?? "500"); break; } } allAttributesChangedCallback(attributes: Record) { this.data = attributes.data || []; this.intervalMs = attributes.interval || ArsDataRoller.#DEFAULT_INTERVAL; this.animationDuration = attributes["animation-duration"] || ArsDataRoller.#DEFAULT_ANIMATION_DURATION; this.currentIndex = 0; // Only update UI if shadowRoot is ready if (this.shadowRoot && this.shadowRoot.querySelector('.roller-item.current')) { this.#render(); this.#startRolling(); } } // ---- PUBLIC INSTANCE METHODS ---- startRolling() { this.#startRolling(); } stopRolling() { this.#stopRolling(); } restartRolling() { this.#stopRolling(); this.#startRolling(); } nextItem() { this.#nextItem(); } setData(newData: unknown[]) { const current = this.getAttribute('data'); const newStr = JSON.stringify(newData); if (current !== newStr) { this.setAttribute('data', newStr); } } setInterval(intervalMs: number) { this.setAttribute('interval', intervalMs.toString()); } setAnimationDuration(durationMs: number) { this.setAttribute('animation-duration', durationMs.toString()); } } if (document.createElement("ars-data-roller").constructor === HTMLElement) { } export { ArsDataRoller, ArsDataRoller as default };