/** * Navigation: the rail's state, shared by every rail and menu button on the page. * * `$store.rail.collapsed` is the visitor's choice for a collapsible rail, remembered in * localStorage. has already applied it before the first paint as * , which is what the stylesheet keys on (the * `rail-collapsed:` variant); the store starts from that attribute and writes it back. * `$store.rail.auto` is true while nothing is stored — the value is `rail.default`, not a choice — * and the adaptive rail then takes its window size class's default instead: collapsed in the * expanded class (840–1199), expanded from `large` (1200), as M3 asks. The first `set()` drops it. * * `$store.rail.open` is the modal rail: on a window too narrow for an expanded rail, a menu * button opens it over a scrim (`show()`), and Escape, the scrim or leaving the page closes it * (`hide()`). It is never remembered. * * `materialNavigationRail` is one rail's view of the store for its `mode` — see * resources/views/components/navigation-rail.blade.php. */ import { from } from './breakpoints.js' /* * The active indicator grows out of its centre when a page arrives through wire:navigate. The * new page's indicator is new markup, so the only way to animate it is a starting style — and * only while a navigation swaps the page in, or every full load would animate it too. The sheet * is adopted as the navigation starts and dropped two frames after it ends; the transition itself * is resources/css/components/navigation.css. */ const arriving = new CSSStyleSheet() arriving.replaceSync(`@starting-style { :is([data-navigation-bar-item], [data-navigation-rail-item])[data-active], :is([data-navigation-bar-item], [data-navigation-rail-item])[data-active] :is([data-navigation-indicator], [data-navigation-pill]) { background-size: 0% 100%; } }`) document.addEventListener('livewire:navigating', () => { if (!document.adoptedStyleSheets.includes(arriving)) { document.adoptedStyleSheets = [...document.adoptedStyleSheets, arriving] } }) document.addEventListener('livewire:navigated', () => { requestAnimationFrame(() => requestAnimationFrame(() => { document.adoptedStyleSheets = document.adoptedStyleSheets.filter((sheet) => sheet !== arriving) })) }) document.addEventListener('alpine:init', () => { const root = document.documentElement window.Alpine.store('rail', { collapsed: root.dataset.rail === 'collapsed', // Nothing stored yet: `collapsed` is only `rail.default`, so an adaptive rail may still // take its window size class's own default. The first choice made here clears it. auto: root.hasAttribute('data-rail-auto'), open: false, toggle() { this.set(!this.collapsed) }, collapse() { this.set(true) }, expand() { this.set(false) }, set(collapsed) { this.collapsed = collapsed this.auto = false root.dataset.rail = collapsed ? 'collapsed' : 'expanded' root.removeAttribute('data-rail-auto') try { localStorage.setItem(root.dataset.railKey || 'material-rail', root.dataset.rail) } catch { // Blocked storage: the rail still toggles, it just will not remember. } }, show() { this.open = true }, hide() { this.open = false }, }) // A destination chosen in the modal rail leaves the page; the next one starts with it shut. document.addEventListener('livewire:navigating', () => window.Alpine.store('rail').hide()) window.Alpine.data('materialNavigationRail', (mode) => ({ wide: mode === 'adaptive' ? from('expanded').matches : false, roomy: mode === 'adaptive' ? from('large').matches : false, queries: [], listeners: [], init() { if (mode !== 'adaptive') { return } // From `expanded` (840px) the adaptive rail is a standard, collapsible rail — what M3 // asks for at expanded and above. A modal left open while the window widens is shut, // or its focus trap would hold a page that has no scrim. this.watch(from('expanded'), (matches) => { this.wide = matches if (matches) { this.$store.rail.hide() } }) // From `large` (1200px) it starts expanded rather than collapsed, until someone // chooses otherwise; below that the expanded class starts it collapsed. Both mirror // the `rail-collapsed:` variant in resources/css/components/navigation.css. this.watch(from('large'), (matches) => (this.roomy = matches)) }, watch(query, onChange) { const listener = (event) => onChange(event.matches) query.addEventListener('change', listener) this.queries.push(query) this.listeners.push(listener) }, destroy() { this.queries.forEach((query, index) => query.removeEventListener('change', this.listeners[index])) }, /** Whether this rail expands over a scrim rather than in the layout. */ get modal() { return mode === 'modal' || (mode === 'adaptive' && !this.wide) }, get open() { return this.modal && this.$store.rail.open }, get expanded() { if (this.open || mode === 'expanded') { return true } if (this.$store.rail.collapsed) { return false } if (mode === 'adaptive') { // A standard rail from `expanded`; with no choice stored it is the window size // class that decides, and only `large` and above start it expanded. return this.wide && (this.roomy || !this.$store.rail.auto) } return mode === 'collapsible' }, /** The rail's own menu button: open or close the modal, or collapse and expand in place. */ menu() { if (this.modal) { this.$store.rail.open ? this.$store.rail.hide() : this.$store.rail.show() } else { // `set`, not `toggle`: with nothing stored the store's `collapsed` is only // `rail.default`, so the button has to flip what is actually drawn. this.$store.rail.set(this.expanded) } }, })) })