feat: extract room code display into ES module component
Made-with: Cursor
This commit is contained in:
279
js/room-code-display.js
Normal file
279
js/room-code-display.js
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Room code lobby display: staggered opacity animation for header, footer, and two code lines.
|
||||
* Implements the overlay component contract: activate, deactivate, update, getStatus.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} RoomCodeDisplayElements
|
||||
* @property {HTMLElement} header
|
||||
* @property {HTMLElement} footer
|
||||
* @property {HTMLElement} codePart1
|
||||
* @property {HTMLElement} codePart2
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} RoomCodeDisplayInputs
|
||||
* @property {HTMLInputElement} code1
|
||||
* @property {HTMLInputElement} code2
|
||||
* @property {HTMLInputElement} color1
|
||||
* @property {HTMLInputElement} color2
|
||||
* @property {HTMLInputElement} offset1
|
||||
* @property {HTMLInputElement} offset2
|
||||
* @property {HTMLInputElement} size
|
||||
* @property {HTMLInputElement} cycle
|
||||
* @property {HTMLInputElement} headerText
|
||||
* @property {HTMLInputElement} headerColor
|
||||
* @property {HTMLInputElement} headerSize
|
||||
* @property {HTMLInputElement} headerOffset
|
||||
* @property {HTMLInputElement} footerText
|
||||
* @property {HTMLInputElement} footerColor
|
||||
* @property {HTMLInputElement} footerSize
|
||||
* @property {HTMLInputElement} footerOffset
|
||||
* @property {HTMLInputElement} headerAppearDelay
|
||||
* @property {HTMLInputElement} headerAppearDuration
|
||||
* @property {HTMLInputElement} headerHideTime
|
||||
* @property {HTMLInputElement} headerHideDuration
|
||||
* @property {HTMLInputElement} line1AppearDelay
|
||||
* @property {HTMLInputElement} line1AppearDuration
|
||||
* @property {HTMLInputElement} line1HideTime
|
||||
* @property {HTMLInputElement} line1HideDuration
|
||||
* @property {HTMLInputElement} line2AppearDelay
|
||||
* @property {HTMLInputElement} line2AppearDuration
|
||||
* @property {HTMLInputElement} line2HideTime
|
||||
* @property {HTMLInputElement} line2HideDuration
|
||||
*/
|
||||
|
||||
export class RoomCodeDisplay {
|
||||
/** @type {RoomCodeDisplayElements | null} */
|
||||
#elements = null;
|
||||
|
||||
/** @type {RoomCodeDisplayInputs | null} */
|
||||
#inputs = null;
|
||||
|
||||
/** @type {ReturnType<typeof setTimeout>[]} */
|
||||
#animationTimers = [];
|
||||
|
||||
/** @type {boolean} */
|
||||
#active = false;
|
||||
|
||||
/**
|
||||
* @param {RoomCodeDisplayElements} elements
|
||||
* @param {RoomCodeDisplayInputs} inputs
|
||||
*/
|
||||
init(elements, inputs) {
|
||||
this.#elements = elements;
|
||||
this.#inputs = inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ roomCode?: string, [key: string]: unknown }} ctx
|
||||
*/
|
||||
activate(ctx) {
|
||||
this.deactivate();
|
||||
|
||||
const inputs = this.#inputs;
|
||||
if (!inputs) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#active = true;
|
||||
|
||||
if (ctx?.roomCode != null && String(ctx.roomCode).trim().length > 0) {
|
||||
const raw = String(ctx.roomCode).trim().toUpperCase();
|
||||
const mid = Math.floor(raw.length / 2);
|
||||
inputs.code1.value = raw.slice(0, mid);
|
||||
inputs.code2.value = raw.slice(mid);
|
||||
}
|
||||
|
||||
this.#applySettings();
|
||||
this.#startAnimation();
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this.#active = false;
|
||||
this.#clearAnimationTimers();
|
||||
|
||||
const { header, footer, codePart1, codePart2 } = this.#elements ?? {};
|
||||
if (!header || !footer || !codePart1 || !codePart2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fadeTime = '0.3s';
|
||||
header.style.transition = `opacity ${fadeTime} ease-out`;
|
||||
footer.style.transition = `opacity ${fadeTime} ease-out`;
|
||||
codePart1.style.transition = `opacity ${fadeTime} ease-out`;
|
||||
codePart2.style.transition = `opacity ${fadeTime} ease-out`;
|
||||
|
||||
header.style.opacity = 0;
|
||||
footer.style.opacity = 0;
|
||||
codePart1.style.opacity = 0;
|
||||
codePart2.style.opacity = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ roomCode?: string, [key: string]: unknown }} ctx
|
||||
*/
|
||||
update(ctx) {
|
||||
if (!this.#inputs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = ctx?.roomCode != null ? String(ctx.roomCode).trim().toUpperCase() : '';
|
||||
const current =
|
||||
this.#inputs.code1.value.toUpperCase() + this.#inputs.code2.value.toUpperCase();
|
||||
|
||||
if (next !== current) {
|
||||
this.activate(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
const inputs = this.#inputs;
|
||||
const roomCode = inputs
|
||||
? (inputs.code1.value + inputs.code2.value).toUpperCase()
|
||||
: '';
|
||||
|
||||
return {
|
||||
active: this.#active,
|
||||
roomCode,
|
||||
timersRunning: this.#animationTimers.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
#clearAnimationTimers() {
|
||||
this.#animationTimers.forEach((id) => clearTimeout(id));
|
||||
this.#animationTimers = [];
|
||||
}
|
||||
|
||||
#applySettings() {
|
||||
const el = this.#elements;
|
||||
const inputs = this.#inputs;
|
||||
if (!el || !inputs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
header,
|
||||
footer,
|
||||
codePart1,
|
||||
codePart2,
|
||||
} = el;
|
||||
|
||||
codePart1.textContent = inputs.code1.value.toUpperCase();
|
||||
codePart2.textContent = inputs.code2.value.toUpperCase();
|
||||
|
||||
codePart1.style.color = inputs.color1.value;
|
||||
codePart2.style.color = inputs.color2.value;
|
||||
|
||||
codePart1.style.transform = `translateY(${inputs.offset1.value}px)`;
|
||||
codePart2.style.transform = `translateY(${inputs.offset2.value}px)`;
|
||||
|
||||
codePart1.style.fontSize = `${inputs.size.value}px`;
|
||||
codePart2.style.fontSize = `${inputs.size.value}px`;
|
||||
|
||||
header.textContent = inputs.headerText.value;
|
||||
header.style.color = inputs.headerColor.value;
|
||||
header.style.fontSize = `${inputs.headerSize.value}px`;
|
||||
header.style.transform = `translateY(${inputs.headerOffset.value}px)`;
|
||||
|
||||
footer.textContent = inputs.footerText.value;
|
||||
footer.style.color = inputs.footerColor.value;
|
||||
footer.style.fontSize = `${inputs.footerSize.value}px`;
|
||||
footer.style.transform = `translateY(${inputs.footerOffset.value}px)`;
|
||||
}
|
||||
|
||||
#startAnimation() {
|
||||
const el = this.#elements;
|
||||
const inputs = this.#inputs;
|
||||
if (!el || !inputs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { header, footer, codePart1, codePart2 } = el;
|
||||
|
||||
this.#clearAnimationTimers();
|
||||
|
||||
header.style.opacity = 0;
|
||||
footer.style.opacity = 0;
|
||||
codePart1.style.opacity = 0;
|
||||
codePart2.style.opacity = 0;
|
||||
|
||||
const sec = (input) => parseInt(input.value, 10) * 1000;
|
||||
|
||||
const cycleDuration = sec(inputs.cycle);
|
||||
|
||||
const headerAppearDelayMs = sec(inputs.headerAppearDelay);
|
||||
const headerAppearDurationMs = sec(inputs.headerAppearDuration);
|
||||
|
||||
this.#animationTimers.push(
|
||||
setTimeout(() => {
|
||||
header.style.transition = `opacity ${headerAppearDurationMs / 1000}s ease-out`;
|
||||
header.style.opacity = 1;
|
||||
|
||||
footer.style.transition = `opacity ${headerAppearDurationMs / 1000}s ease-out`;
|
||||
footer.style.opacity = 1;
|
||||
}, headerAppearDelayMs),
|
||||
);
|
||||
|
||||
const line1AppearDelayMs = sec(inputs.line1AppearDelay);
|
||||
const line1AppearDurationMs = sec(inputs.line1AppearDuration);
|
||||
|
||||
this.#animationTimers.push(
|
||||
setTimeout(() => {
|
||||
codePart1.style.transition = `opacity ${line1AppearDurationMs / 1000}s ease-out`;
|
||||
codePart1.style.opacity = 1;
|
||||
}, line1AppearDelayMs),
|
||||
);
|
||||
|
||||
const line1HideTimeMs = sec(inputs.line1HideTime);
|
||||
const line1HideDurationMs = sec(inputs.line1HideDuration);
|
||||
|
||||
this.#animationTimers.push(
|
||||
setTimeout(() => {
|
||||
codePart1.style.transition = `opacity ${line1HideDurationMs / 1000}s ease-out`;
|
||||
codePart1.style.opacity = 0;
|
||||
}, line1HideTimeMs),
|
||||
);
|
||||
|
||||
const line2AppearDelayMs = sec(inputs.line2AppearDelay);
|
||||
const line2AppearDurationMs = sec(inputs.line2AppearDuration);
|
||||
|
||||
this.#animationTimers.push(
|
||||
setTimeout(() => {
|
||||
codePart2.style.transition = `opacity ${line2AppearDurationMs / 1000}s ease-out`;
|
||||
codePart2.style.opacity = 1;
|
||||
}, line2AppearDelayMs),
|
||||
);
|
||||
|
||||
const line2HideTimeMs = sec(inputs.line2HideTime);
|
||||
const line2HideDurationMs = sec(inputs.line2HideDuration);
|
||||
|
||||
this.#animationTimers.push(
|
||||
setTimeout(() => {
|
||||
codePart2.style.transition = `opacity ${line2HideDurationMs / 1000}s ease-out`;
|
||||
codePart2.style.opacity = 0;
|
||||
}, line2HideTimeMs),
|
||||
);
|
||||
|
||||
const headerHideTimeMs = sec(inputs.headerHideTime);
|
||||
const headerHideDurationMs = sec(inputs.headerHideDuration);
|
||||
|
||||
this.#animationTimers.push(
|
||||
setTimeout(() => {
|
||||
header.style.transition = `opacity ${headerHideDurationMs / 1000}s ease-out`;
|
||||
header.style.opacity = 0;
|
||||
|
||||
footer.style.transition = `opacity ${headerHideDurationMs / 1000}s ease-out`;
|
||||
footer.style.opacity = 0;
|
||||
}, headerHideTimeMs),
|
||||
);
|
||||
|
||||
this.#animationTimers.push(
|
||||
setTimeout(() => {
|
||||
if (this.#active) {
|
||||
this.#startAnimation();
|
||||
}
|
||||
}, cycleDuration),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user