You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

122 lines
4.3 KiB

import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["dialog"]
connect() {
this.#openWhenSettled()
}
close() {
this.dialogTarget.close()
}
// A click whose target is the <dialog> itself landed on the backdrop
// (the panel fills the content box), so dismiss.
backdropClose(event) {
if (event.target === this.dialogTarget) this.close()
}
remove() {
this.dispatch("closed", { target: window })
this.element.remove()
}
// The panel sits above the game frame rather than inside it, so the next
// screen renders behind it. Once it is there, the panel drops off the bottom
// and leaves the intro playing underneath.
leave() {
if (this.leaving) return
this.leaving = true
document.addEventListener("turbo:frame-render", this.#slideAway, { once: true })
}
#slideAway = async () => {
this.element.classList.add("is-leaving")
await this.#settle(this.element, "transitionend")
this.close()
}
// The screen underneath may still be animating out. The wave is the last
// thing to leave, so waiting on it holds the modal until the exit is done.
async #openWhenSettled() {
const wave = document.querySelector(".is-exiting .hero-container svg.ico-wave")
if (wave) await this.#settle(wave, "animationend")
if (!this.element.isConnected) return
this.dialogTarget.showModal()
this.dispatch("opened", { target: window })
this.#playEntrance()
}
// The hero opens full-bleed, pushing the copy and the markers off the bottom.
// Once the wave has landed, the carousel and the hero shrink back to their
// measured resting heights, which carries everything below up into view.
// Sizing both is what hides the slide's own copy: the li clips whatever the
// hero pushes past it. The wave rides the same shrink from the screen edge
// up to the seam, since it no longer sits inside the hero it draws under.
async #playEntrance() {
const panel = this.element.querySelector(".modal-panel")
const frame = panel?.querySelector(".carousel-frame")
const carousel = frame?.querySelector(".carousel")
if (!carousel) return
const heroes = Array.from(carousel.querySelectorAll(":scope > li > .hero-container"))
const boxes = [carousel, ...heroes]
const rest = new Map(boxes.map((box) => [box, box.offsetHeight]))
// Border-box, not clientHeight: a scrollbar must not shorten the hero and
// leave a strip of panel showing under it.
const full = this.dialogTarget.getBoundingClientRect().height
const wave = frame.querySelector(":scope > svg.ico-wave")
panel.classList.add("is-entering")
boxes.forEach((box) => {
box.style.transition = "none"
box.style.height = `${full}px`
})
if (wave) {
wave.style.transition = "none"
wave.style.bottom = "0px"
}
getComputedStyle(carousel).height
boxes.forEach((box) => (box.style.transition = ""))
if (wave) wave.style.transition = ""
if (wave) await this.#settle(wave, "animationend")
boxes.forEach((box) => (box.style.height = `${rest.get(box)}px`))
if (wave) wave.style.bottom = ""
await this.#settle(carousel, "transitionend")
panel.classList.remove("is-entering")
boxes.forEach((box) => (box.style.height = ""))
}
// Resolves when the element's animation or transition ends. Safari doesn't
// reliably have one registered at the moment we ask for it, so the declared
// timing is used as a fallback rather than getAnimations().
#settle(element, eventName) {
return new Promise((resolve) => {
const style = getComputedStyle(element)
const seconds =
eventName === "animationend"
? parseFloat(style.animationDuration) + parseFloat(style.animationDelay)
: parseFloat(style.transitionDuration) + parseFloat(style.transitionDelay)
// Descendants and ::backdrop send their own events up through the same
// element, and either would settle this early.
const finish = (event) => {
if (event && (event.target !== element || event.pseudoElement)) return
element.removeEventListener(eventName, finish)
clearTimeout(timer)
resolve()
}
const timer = setTimeout(finish, (seconds || 0) * 1000 + 50)
element.addEventListener(eventName, finish)
})
}
}