generated from boboko/starter
product category page: front-end sorting and filtering using turboframes
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import "./bootstrap";
|
||||
import "./utils/strip-accents";
|
||||
import "./utils/refresh-scroll";
|
||||
|
||||
// Frames only — no site-wide Turbo Drive. <turbo-frame> navigations still work
|
||||
// (that's how the category listing reloads); every other link and form on the
|
||||
// site keeps its normal full-page browser behaviour.
|
||||
import "@hotwired/turbo";
|
||||
window.Turbo.session.drive = false;
|
||||
|
||||
import { Application } from "@hotwired/stimulus";
|
||||
import { registerControllers } from "./stimulus/index";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
// Submits the host <form> a short beat after a control inside it changes,
|
||||
// coalescing a burst — rapid slider nudges, or holding an arrow key on a range
|
||||
// input — into a single submit. Wire it on the <form>:
|
||||
//
|
||||
// <form data-controller="auto-submit"
|
||||
// data-action="change->auto-submit#submit range-slider:change->auto-submit#submit"
|
||||
// data-auto-submit-delay-value="300"> (delay optional, ms)
|
||||
//
|
||||
// `change` covers native inputs (checkbox, select); the range slider emits its
|
||||
// own `range-slider:change` on commit. Uses requestSubmit() (not submit()) so a
|
||||
// <turbo-frame> around the form still captures the navigation and validation runs.
|
||||
export default class extends Controller {
|
||||
static values = { delay: { type: Number, default: 300 } }
|
||||
|
||||
submit() {
|
||||
clearTimeout(this.#timer)
|
||||
this.#timer = setTimeout(() => this.element.requestSubmit(), this.delayValue)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
clearTimeout(this.#timer)
|
||||
}
|
||||
|
||||
#timer
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
// application.register('hello', HelloController);
|
||||
|
||||
import AppearController from './appear-controller'
|
||||
import AutoSubmitController from './auto-submit-controller'
|
||||
import BackToTopController from './back-to-top-controller'
|
||||
import CarouselController from './carousel-controller'
|
||||
import DropdownController from './dropdown-controller'
|
||||
@@ -16,6 +17,7 @@ import TabsController from './tabs-controller'
|
||||
|
||||
export function registerControllers(application) {
|
||||
application.register('appear', AppearController)
|
||||
application.register('auto-submit', AutoSubmitController)
|
||||
application.register('back-to-top', BackToTopController)
|
||||
application.register('carousel', CarouselController)
|
||||
application.register('dropdown', DropdownController)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Controller } from '@hotwired/stimulus'
|
||||
// max and vice versa. Emits `range-slider:input` while dragging and
|
||||
// `range-slider:change` on commit, both with { min, max }.
|
||||
export default class extends Controller {
|
||||
static targets = ['minInput', 'maxInput', 'field', 'track', 'minThumb', 'maxThumb', 'output', 'reset']
|
||||
static targets = ['minInput', 'maxInput', 'field', 'track', 'minThumb', 'maxThumb', 'output']
|
||||
static values = {
|
||||
min: Number,
|
||||
max: Number,
|
||||
@@ -26,7 +26,6 @@ export default class extends Controller {
|
||||
|
||||
connect() {
|
||||
this.#clamp()
|
||||
this.defaults = { min: this.#lo, max: this.#hi }
|
||||
this.fieldTargets.forEach((field) => field.classList.add('sr-only'))
|
||||
this.#render()
|
||||
}
|
||||
@@ -78,13 +77,6 @@ export default class extends Controller {
|
||||
this.#startDrag(event, input)
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.minInputTarget.value = this.defaults.min
|
||||
this.maxInputTarget.value = this.defaults.max
|
||||
this.#render()
|
||||
this.#emit('change')
|
||||
}
|
||||
|
||||
// ── internals ──────────────────────────────────────────────────────
|
||||
|
||||
#startDrag(event, input) {
|
||||
@@ -158,10 +150,6 @@ export default class extends Controller {
|
||||
const fmt = (v) => `${this.prefixValue}${v}${this.suffixValue}`
|
||||
this.outputTarget.textContent = fmt(lo) + this.separatorValue + fmt(hi)
|
||||
}
|
||||
|
||||
if (this.hasResetTarget) {
|
||||
this.resetTarget.hidden = lo === this.defaults.min && hi === this.defaults.max
|
||||
}
|
||||
}
|
||||
|
||||
#emit(name) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Make a refresh land back where you were — accurately.
|
||||
//
|
||||
// Turbo Drive is off site-wide (see app.js), so a refresh is a full browser
|
||||
// load. The browser restores the scroll position early in that load — before
|
||||
// the Manrope web fonts swap in and reflow the header, <h1> and result count
|
||||
// above the product grid — so it settles a bit too low. We record the position
|
||||
// ourselves and re-apply it once the layout has actually stopped moving.
|
||||
//
|
||||
// Separately: drop focus on the way out. Otherwise the browser re-focuses
|
||||
// whatever filter control was active and scrolls it into view on reload, and
|
||||
// that sidebar stacks below the grid on narrow screens — hence the jump to the
|
||||
// bottom.
|
||||
//
|
||||
// The real fix for the drift is preloading the above-the-fold font weights so
|
||||
// there's no reflow to chase; this keeps the restore correct until then, and
|
||||
// harmless after.
|
||||
|
||||
const key = 'scrollY:' + location.pathname + location.search
|
||||
|
||||
let frame = 0
|
||||
window.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
if (frame) return
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = 0
|
||||
try {
|
||||
sessionStorage.setItem(key, String(Math.round(window.scrollY)))
|
||||
} catch {}
|
||||
})
|
||||
},
|
||||
{ passive: true },
|
||||
)
|
||||
|
||||
window.addEventListener('pagehide', () => {
|
||||
const el = document.activeElement
|
||||
if (el && el !== document.body) el.blur()
|
||||
})
|
||||
|
||||
// Only reloads and back/forward should resume a position; a fresh visit to the
|
||||
// page starts where it naturally would.
|
||||
const [nav] = performance.getEntriesByType('navigation')
|
||||
if (nav && (nav.type === 'reload' || nav.type === 'back_forward')) {
|
||||
let saved = null
|
||||
try {
|
||||
saved = sessionStorage.getItem(key)
|
||||
} catch {}
|
||||
|
||||
if (saved !== null) {
|
||||
const y = Number(saved)
|
||||
const apply = () => window.scrollTo(0, y)
|
||||
|
||||
window.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
apply()
|
||||
// Fonts (and any late above-the-fold image) can still nudge
|
||||
// layout a frame or two after load — re-apply once they settle.
|
||||
document.fonts?.ready.then(() => requestAnimationFrame(apply))
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user