@astrojs/starlight
Version:
Build beautiful, high-performance documentation websites with Astro
73 lines (64 loc) • 2.72 kB
text/typescript
// Collect required elements from the DOM.
const scroller = document.getElementById('starlight__sidebar');
const target = scroller?.querySelector<HTMLElement>('sl-sidebar-state-persist');
/** Starlight uses this key to store sidebar state in `sessionStorage`. */
const storageKey = 'sl-sidebar-state';
/** The shape used to persist sidebar state across a user’s session. */
interface SidebarState {
hash: string;
open: Array<boolean | null>;
scroll: number;
}
/**
* Get the current sidebar state.
*
* The `open` state is loaded from session storage when the sidebar hashes match, while `scroll`
* and `hash` are read from the current page.
*/
const getState = (): SidebarState => {
let open: SidebarState['open'] = [];
const hash = target?.dataset.hash || '';
try {
const rawStoredState = sessionStorage.getItem(storageKey);
const storedState = JSON.parse(rawStoredState || '{}') as SidebarState;
if (Array.isArray(storedState.open) && storedState.hash === hash) open = storedState.open;
} catch {}
return {
hash,
open,
scroll: scroller?.scrollTop || 0,
};
};
/** Store the passed sidebar state in session storage. */
const storeState = (state: SidebarState): void => {
try {
sessionStorage.setItem(storageKey, JSON.stringify(state));
} catch {}
};
/** Updates sidebar state in session storage without modifying `open` state. */
const updateState = (): void => storeState(getState());
/** Updates sidebar state in session storage to include a new value for a specific `<details>` element. */
const setToggleState = (open: boolean, detailsIndex: number): void => {
const state = getState();
state.open[detailsIndex] = open;
storeState(state);
};
// Store the current `open` state whenever a user interacts with one of the `<details>` groups.
target?.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) return;
// Query for the nearest `<summary>` and then its parent `<details>`.
// This excludes clicks outside of the `<summary>`, which don’t trigger toggles.
const toggledDetails = event.target.closest('summary')?.closest('details');
if (!toggledDetails) return;
const restoreElement = toggledDetails.querySelector<HTMLElement>('sl-sidebar-restore');
const index = parseInt(restoreElement?.dataset.index || '');
if (isNaN(index)) return;
setToggleState(!toggledDetails.open, index);
});
// Store sidebar state before navigating. These will also store it on tab blur etc.,
// but avoid using the `beforeunload` event, which can cause issues with back/forward cache
// on some browsers.
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') updateState();
});
addEventListener('pageHide', updateState);