/** * `materialAppBar`: says when content is under `` (`scrolled`) and, for a medium or * large bar, when it has collapsed to its row (`collapsed`). The collapsing itself is CSS * (resources/css/components/app-bar.css); this only reads the bar's position and height, once per * frame while the page scrolls or the bar resizes — its height, because a long title wraps and the * bar must stick where exactly its row is left showing. */ document.addEventListener('alpine:init', () => { window.Alpine.data('materialAppBar', () => ({ scrolled: false, collapsed: false, height: null, frame: null, // Set in init(). Declared here, or Alpine writes them to the outermost x-data scope, // where a second app bar in the same page scope would take the first one's observer. schedule: null, resizes: null, init() { this.measure = this.measure.bind(this) this.schedule = () => { this.frame ??= requestAnimationFrame(this.measure) } this.resizes = new ResizeObserver(this.schedule) this.resizes.observe(this.$root) window.addEventListener('scroll', this.schedule, { passive: true }) this.measure() }, destroy() { this.resizes.disconnect() window.removeEventListener('scroll', this.schedule) cancelAnimationFrame(this.frame) }, // Bound as the bar's style, so a Livewire morph keeps it. get measured() { return this.height === null ? {} : { '--app-bar-measured': `${this.height}px` } }, measure() { this.frame = null const bar = this.$root this.height = bar.offsetHeight const style = getComputedStyle(bar) if (style.position !== 'sticky') { this.scrolled = this.collapsed = false return } const top = bar.getBoundingClientRect().top const stuckAt = parseFloat(style.top) || 0 const stuck = window.scrollY > 0 && top <= stuckAt + 0.5 this.collapsed = stuck && stuckAt < 0 this.scrolled = stuck }, })) })