import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["dialog"] confirm(event) { // The second pass -- after the player confirmed -- falls through to Turbo. if (this.confirmed) return event.preventDefault() this.pendingForm = event.target this.dialogTarget.classList.remove("is-confirming", "is-leaving") this.leaving = false this.dialogTarget.showModal() } // The dialog leaves upwards before the answer is sent, so the wait is however // long that slide takes -- and nothing at all where the slide doesn't exist. async ok() { if (this.leaving) return await this.#slideOut("is-confirming") // requestSubmit(), not submit(): the native call skips the submit event, so // Turbo would miss it and navigate the whole page out of the game frame. this.confirmed = true this.pendingForm?.requestSubmit() } async cancel() { if (this.leaving) return await this.#slideOut("is-leaving") this.pendingForm = null } backdropClick(event) { if (event.target === this.dialogTarget) this.cancel() } // The slide runs while the dialog is still open, so close() only ever lands // once it is off screen: nothing is left behind for the browser to lay out, // which is what Safari does with a dialog that outlives its top layer. async #slideOut(state) { this.leaving = true this.dialogTarget.classList.add(state) // Reading a style forces the class change to resolve, so the transition // exists by the time we ask for it. getComputedStyle(this.dialogTarget).translate const running = this.dialogTarget.getAnimations({ subtree: false }) await Promise.all(running.map((animation) => animation.finished)).catch(() => {}) this.dialogTarget.close() } }