/** * `materialFab`: `` — M3's extended FAB that collapses to a FAB while the * page scrolls down and extends again on the way back up, or once the page is at the top. * * Only the flag lives here. The morph itself is CSS (resources/css/components/actions.css), which * is how it stays on the spatial spring and how reduced motion makes it instant without a second * path through this file. * * It watches the window, which is what a FAB pinned to the corner of the page scrolls against. A * FAB inside a scrolling pane of its own is not this. */ // Scrolling is noisy — a wheel's own wobble, a rubber-band bounce at either end — and a FAB that // flipped on every pixel would never be still. A move has to be worth this much to count as a // direction, and one worth less is kept and added to the next. const STEP_PX = 8 // Within this much of the top the FAB is extended whichever way the page was last going: M3 // re-expands it "at the bottom of the view", which on a web page is where the page begins. const TOP_PX = 24 document.addEventListener('alpine:init', () => { window.Alpine.data('materialFab', () => ({ collapsed: false, lastY: 0, ticking: false, listeners: [], init() { this.lastY = Math.max(window.scrollY, 0) this.listen(window, 'scroll', () => this.queue(), { passive: true }) }, /** One reading a frame: `scroll` fires far more often than anything can be drawn. */ queue() { if (this.ticking) { return } this.ticking = true requestAnimationFrame(() => { this.ticking = false this.measure() }) }, measure() { const y = Math.max(window.scrollY, 0) const moved = y - this.lastY if (y <= TOP_PX) { this.collapsed = false } else if (moved > STEP_PX) { this.collapsed = true } else if (moved < -STEP_PX) { this.collapsed = false } if (Math.abs(moved) > STEP_PX || y <= TOP_PX) { this.lastY = y } }, listen(target, type, handler, options) { target.addEventListener(type, handler, options) this.listeners.push(() => target.removeEventListener(type, handler, options)) }, destroy() { this.listeners.forEach((remove) => remove()) }, })) })