UNPKG

vike

Version:

The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.

158 lines (157 loc) 6.51 kB
export { pushHistoryState }; export { replaceHistoryStateOriginal }; export { onPopStateBegin }; export { saveScrollPosition }; export { initHistoryState }; export { monkeyPatchHistoryAPI }; import { getCurrentUrl } from '../shared/getCurrentUrl.js'; import { assert, assertUsage, getGlobalObject, isObject } from './utils.js'; initHistoryState(); // we redundantly call initHistoryState() to ensure it's called early const globalObject = getGlobalObject('client-routing-runtime/history.ts', { previous: getHistoryInfo() }); // `window.history.state === null` when: // - The very first render // - Click on `<a href="#some-hash" />` // - `location.hash = 'some-hash'` function enhanceHistoryState() { const stateNotEnhanced = getStateNotEnhanced(); if (isVikeEnhanced(stateNotEnhanced)) return; const stateVikeEnhanced = enhance(stateNotEnhanced); replaceHistoryState(stateVikeEnhanced); } function enhance(stateNotEnhanced) { const timestamp = getTimestamp(); const scrollPosition = getScrollPosition(); const triggeredBy = 'browser'; let stateVikeEnhanced; if (!stateNotEnhanced) { stateVikeEnhanced = { timestamp, scrollPosition, triggeredBy, _isVikeEnhanced: true }; } else { // State information may be incomplete if `window.history.state` is set by an old Vike version. (E.g. `state.timestamp` was introduced for `pageContext.isBackwardNavigation` in `0.4.19`.) stateVikeEnhanced = { timestamp: stateNotEnhanced.timestamp ?? timestamp, scrollPosition: stateNotEnhanced.scrollPosition ?? scrollPosition, triggeredBy: stateNotEnhanced.triggeredBy ?? triggeredBy, _isVikeEnhanced: true }; } assert(isVikeEnhanced(stateVikeEnhanced)); return stateVikeEnhanced; } function getState() { const state = getStateNotEnhanced(); // *Every* state added to the history needs to go through Vike. // - Otherwise Vike's `popstate` listener won't work. (Because, for example, if globalObject.previous is outdated => isHashNavigation faulty => client-side navigation is wrongfully skipped.) // - Therefore, we have to monkey patch history.pushState() and history.replaceState() // - Therefore, we need the assert() below to ensure history.state has been enhanced by Vike // - If users stumble upon this assert() then let's make it a assertUsage() assert(isVikeEnhanced(state), { state }); return state; } function getStateNotEnhanced() { const state = window.history.state; return state; } function getScrollPosition() { const scrollPosition = { x: window.scrollX, y: window.scrollY }; return scrollPosition; } function getTimestamp() { return new Date().getTime(); } function saveScrollPosition() { const scrollPosition = getScrollPosition(); const state = getState(); replaceHistoryState({ ...state, scrollPosition }); } function pushHistoryState(url, overwriteLastHistoryEntry) { if (!overwriteLastHistoryEntry) { const state = { timestamp: getTimestamp(), // I don't remember why I set it to `null`, maybe because setting it now would be too early? (Maybe there is a delay between renderPageClientSide() is finished and the browser updating the scroll position.) Anyways, it seems like autoSaveScrollPosition() is enough. scrollPosition: null, triggeredBy: 'vike', _isVikeEnhanced: true }; // Calling the monkey patched history.pushState() (and not the orignal) so that other tools (e.g. user tracking) can listen to Vike's pushState() calls. // - https://github.com/vikejs/vike/issues/1582 window.history.pushState(state, '', url); } else { replaceHistoryState(getState(), url); } } function replaceHistoryState(state, url) { const url_ = url ?? null; // Passing `undefined` chokes older Edge versions. window.history.replaceState(state, '', url_); } function replaceHistoryStateOriginal(state, url) { // Bypass all monkey patches. // - Useful, for example, to avoid other tools listening to history.replaceState() calls History.prototype.replaceState.bind(window.history)(state, '', url); } // Monkey patch: // - history.pushState() // - history.replaceState() function monkeyPatchHistoryAPI() { ; ['pushState', 'replaceState'].forEach((funcName) => { const funcOriginal = window.history[funcName].bind(window.history); window.history[funcName] = (stateOriginal = {}, ...rest) => { assertUsage(stateOriginal === undefined || stateOriginal === null || isObject(stateOriginal), `history.${funcName}(state) argument state must be an object`); const stateEnhanced = isVikeEnhanced(stateOriginal) ? stateOriginal : { _isVikeEnhanced: true, scrollPosition: getScrollPosition(), timestamp: getTimestamp(), triggeredBy: 'user', ...stateOriginal }; assert(isVikeEnhanced(stateEnhanced)); const ret = funcOriginal(stateEnhanced, ...rest); globalObject.previous = getHistoryInfo(); return ret; }; }); } function isVikeEnhanced(state) { if (isObject(state) && '_isVikeEnhanced' in state) { /* We don't use the assert() below to save client-side KBs. assert(hasProp(state, '_isVikeEnhanced', 'true')) assert(hasProp(state, 'timestamp', 'number')) assert(hasProp(state, 'scrollPosition')) if (state.scrollPosition !== null) { assert(hasProp(state, 'scrollPosition', 'object')) assert(hasProp(state.scrollPosition, 'x', 'number') && hasProp(state.scrollPosition, 'y', 'number')) } //*/ return true; } return false; } function getHistoryInfo() { return { url: getCurrentUrl(), state: getState() }; } function onPopStateBegin() { const { previous } = globalObject; const isHistoryStateEnhanced = window.history.state !== null; if (!isHistoryStateEnhanced) enhanceHistoryState(); assert(isVikeEnhanced(window.history.state)); const current = getHistoryInfo(); globalObject.previous = current; return { isHistoryStateEnhanced, previous, current }; } function initHistoryState() { enhanceHistoryState(); }