/** * 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.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. */ const WIDE = '(min-width: 64rem)' /* * 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', open: false, toggle() { this.set(!this.collapsed) }, collapse() { this.set(true) }, expand() { this.set(false) }, set(collapsed) { this.collapsed = collapsed root.dataset.rail = collapsed ? 'collapsed' : 'expanded' 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' ? window.matchMedia(WIDE).matches : false, query: null, onWidth: null, init() { if (mode !== 'adaptive') { return } // From lg the adaptive rail is a standard, collapsible rail: a modal left open while // the window widens is shut, or its focus trap would hold a page that has no scrim. this.query = window.matchMedia(WIDE) this.onWidth = (event) => { this.wide = event.matches if (event.matches) { this.$store.rail.hide() } } this.query.addEventListener('change', this.onWidth) }, destroy() { this.query?.removeEventListener('change', this.onWidth) }, /** 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 } return (mode === 'collapsible' || (mode === 'adaptive' && this.wide)) && !this.$store.rail.collapsed }, /** 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 { this.$store.rail.toggle() } }, })) })