import "@hotwired/turbo-rails" import { Application } from "@hotwired/stimulus" import LanguageMenuController from "language_menu_controller" import CarouselController from "carousel_controller" import IntroController from "intro_controller" import ChanceController from "chance_controller" import ShareController from "share_controller" import ModalController from "modal_controller" import StartController from "start_controller" import AnswerController from "answer_controller" const application = Application.start() application.register("language-menu", LanguageMenuController) application.register("carousel", CarouselController) application.register("intro", IntroController) application.register("chance", ChanceController) application.register("share", ShareController) application.register("modal", ModalController) application.register("start", StartController) application.register("answer", AnswerController) // The whole game runs inside one turbo-frame so the URL never changes. Frame // navigations don't reset scroll the way a full page visit does, so each new // screen would otherwise open at the previous screen's scroll offset. document.addEventListener("turbo:frame-render", (event) => { if (event.target.id === "game") window.scrollTo(0, 0) }) // Which screen pairs swap with a view transition instead of appearing outright, // keyed by the template class the layout puts on
. The value names the // animation, which lives in application.css under [data-screen-transition]. const SCREEN_TRANSITIONS = { "intro>stage": "slide-left", "stage>good_answer": "answer", "stage>bad_answer": "answer", "good_answer>stage": "stage-in", "bad_answer>stage": "stage-in", "good_answer>last_save": "last-save-in", "bad_answer>last_save": "last-save-in", "last_save>good_answer": "answer", "last_save>bad_answer": "answer", "good_answer>results": "slide-up", "bad_answer>results": "slide-up" } // The desktop layout is a different composition -- two columns rather than one // stack -- so the pairs that animate there, and how, are their own map. const WIDE_SCREEN_TRANSITIONS = { "intro>stage": "stage-in-wide", "stage>good_answer": "answer-wide", "stage>bad_answer": "answer-wide", "good_answer>stage": "stage-in-wide", "bad_answer>stage": "stage-in-wide", "good_answer>last_save": "stage-in-wide", "bad_answer>last_save": "stage-in-wide", "last_save>good_answer": "answer-wide", "last_save>bad_answer": "answer-wide", "good_answer>results": "results-in-wide", "bad_answer>results": "results-in-wide" } const TRANSITION_MEDIA = "(max-width: 1023.98px) and (prefers-reduced-motion: no-preference)" const WIDE_TRANSITION_MEDIA = "(min-width: 1024px) and (prefers-reduced-motion: no-preference)" function screenTransition(newFrame) { if (!document.startViewTransition) return null const from = document.querySelector("turbo-frame#game main")?.classList[0] const to = newFrame.querySelector("main")?.classList[0] const pair = `${from}>${to}` if (window.matchMedia(TRANSITION_MEDIA).matches) return SCREEN_TRANSITIONS[pair] if (window.matchMedia(WIDE_TRANSITION_MEDIA).matches) return WIDE_SCREEN_TRANSITIONS[pair] return null } // Turbo hands over the function that swaps the frame's contents, so running it // inside a view transition puts the old and the new screen on screen together // for as long as the animation lasts. document.addEventListener("turbo:before-frame-render", (event) => { if (event.target.id !== "game") return const name = screenTransition(event.detail.newFrame) if (!name) return const render = event.detail.render event.detail.render = (currentElement, newElement) => { document.documentElement.dataset.screenTransition = name // Two navigations at once -- the intro video ending on top of a tap -- leave // the first transition aborted, so the settled state is reached either way. const clear = () => delete document.documentElement.dataset.screenTransition // Anything a screen wants to animate once it has landed hangs off this: the // snapshots are gone by then, so it plays on the real elements. const settle = () => { clear() document.querySelector("turbo-frame#game main")?.classList.add("is-settling") } const transition = document.startViewTransition(() => render(currentElement, newElement)) transition.finished.then(settle, clear) return transition.updateCallbackDone.catch(clear) } }) // Configure Stimulus development experience application.debug = false window.Stimulus = application export { application }