lexivo/app/javascript/controllers/flashcard_controller.js
viktorvsk 79d244fe2a
Some checks failed
CI / Lint & Test (push) Has been cancelled
Deploy Status Page / Build & Deploy (push) Has been cancelled
feat: add key 3=Good 4=Easy in review; form-q4 support
2026-04-23 04:52:59 +00:00

129 lines
3.8 KiB
JavaScript

import { Controller } from "@hotwired/stimulus"
// Handles flashcard flip + keyboard + swipe for both filter and review pages.
// Expected DOM:
// data-controller="flashcard"
// data-flashcard-target="card" (the 3D flippable element)
//
// Hidden forms in the same element:
// id="form-right" — known / easy
// id="form-left" — learning / forgot
// id="form-skip" — skip (filter only)
// id="form-q3" — hard (review only)
// id="form-q4" — good (review only)
export default class extends Controller {
static targets = ["card"]
connect() {
this.flipped = false
this.animating = false
this.touchStartX = 0
this.touchStartY = 0
this._keydown = this.handleKeydown.bind(this)
document.addEventListener("keydown", this._keydown)
}
disconnect() {
document.removeEventListener("keydown", this._keydown)
}
// ── Keyboard ────────────────────────────────────────────────────────────────
handleKeydown(e) {
if (this.animating) return
switch (e.key) {
case " ":
e.preventDefault()
this.flip()
break
case "ArrowDown":
e.preventDefault()
this.flip()
break
case "ArrowRight":
e.preventDefault()
this.slideAndSubmit("slide-right", "form-right")
break
case "ArrowLeft":
e.preventDefault()
this.slideAndSubmit("slide-left", "form-left")
break
case "s":
case "S":
case "ArrowUp":
e.preventDefault()
this.slideAndSubmit("slide-up", "form-skip")
break
case "1":
e.preventDefault()
this.slideAndSubmit("slide-left", "form-left")
break
case "2":
e.preventDefault()
this.slideAndSubmit("slide-up", "form-q3")
break
case "3":
e.preventDefault()
this.slideAndSubmit("slide-up", "form-q4")
break
case "4":
e.preventDefault()
this.slideAndSubmit("slide-right", "form-right")
break
}
}
// ── Touch / swipe ───────────────────────────────────────────────────────────
touchstart(e) {
const t = e.changedTouches[0]
this.touchStartX = t.clientX
this.touchStartY = t.clientY
}
touchend(e) {
if (this.animating) return
const t = e.changedTouches[0]
const dx = t.clientX - this.touchStartX
const dy = t.clientY - this.touchStartY
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return
e.preventDefault()
if (Math.abs(dx) >= Math.abs(dy) && Math.abs(dx) > 40) {
dx > 0
? this.slideAndSubmit("slide-right", "form-right")
: this.slideAndSubmit("slide-left", "form-left")
}
}
// ── Card flip ───────────────────────────────────────────────────────────────
flip(e) {
if (e) e.stopPropagation()
this.flipped = !this.flipped
this.cardTarget.classList.toggle("is-flipped", this.flipped)
}
// ── Form submission ─────────────────────────────────────────────────────────
slideAndSubmit(animClass, formId) {
this.animating = true
this.cardTarget.classList.add(animClass)
setTimeout(() => {
const form = this.element.querySelector(`#${formId}`)
if (form) {
form.requestSubmit()
} else {
this.animating = false
this.cardTarget.classList.remove(animClass)
}
}, 220)
}
}