vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
180 lines (179 loc) • 7.44 kB
JavaScript
import '../assertEnvClient.js';
export { pushHistoryState };
export { replaceHistoryStateOriginal };
export { onPopStateBegin };
export { saveScrollPosition };
export { initHistory };
import { getCurrentUrl } from '../shared/getCurrentUrl.js';
import { assert, assertUsage } from '../../utils/assert.js';
import { getGlobalObject } from '../../utils/getGlobalObject.js';
import { isObject } from '../../utils/isObject.js';
import { redirectHard } from '../../utils/redirectHard.js';
const globalObject = getGlobalObject('history.ts', {
monkeyPatched: false,
previous: undefined,
});
initHistory(); // we redundantly call initHistory() to ensure it's called early
globalObject.previous = getHistoryInfo();
// `window.history.state === null` when:
// - The very first render
// - Click on `<a href="#some-hash" />`
// - `location.hash = 'some-hash'`
function enhance() {
if (isEnhanced(window.history.state))
return;
const stateEnhanced = {
vike: {
timestamp: getTimestamp(),
scrollPosition: getScrollPosition(),
triggeredBy: 'browser',
},
};
replaceHistoryState(stateEnhanced);
}
function getState() {
const state = window.history.state;
// *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()
assertIsEnhanced(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();
// Don't overwrite history.state if it was set by a non-Vike history.pushState() call.
// https://github.com/vikejs/vike/issues/2801#issuecomment-3490431479
if (!isEnhanced(window.history.state))
return;
const state = getState();
replaceHistoryState({ ...state, vike: { ...state.vike, scrollPosition } });
}
function pushHistoryState(url, overwriteLastHistoryEntry) {
if (!overwriteLastHistoryEntry) {
const state = {
vike: {
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 renderPageClient() is finished and the browser updating the scroll position.) Anyways, it seems like autoSaveScrollPosition() is enough.
scrollPosition: null,
triggeredBy: 'vike',
},
};
// Calling the monkey patched history.pushState() (and not the original) 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_);
assertIsEnhanced(window.history.state);
}
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() {
if (globalObject.monkeyPatched)
return;
globalObject.monkeyPatched = true;
['pushState', 'replaceState'].forEach((funcName) => {
const funcOriginal = window.history[funcName].bind(window.history);
window.history[funcName] = (stateFromUser = {}, ...rest) => {
assertUsage(stateFromUser === undefined || stateFromUser === null || isObject(stateFromUser), `history.${funcName}(state) argument state must be an object`);
const state = isEnhanced(stateFromUser)
? stateFromUser
: {
...stateFromUser,
vike: {
scrollPosition: getScrollPosition(),
timestamp: getTimestamp(),
triggeredBy: 'user',
},
};
funcOriginal(state, ...rest);
assertIsEnhanced(window.history.state);
globalObject.previous = getHistoryInfo();
// Workaround https://github.com/vikejs/vike/issues/2504#issuecomment-3149764736
queueMicrotask(() => {
if (isEnhanced(window.history.state))
return;
Object.assign(state, window.history.state);
replaceHistoryStateOriginal(state);
});
};
});
}
function isEnhanced(state) {
if (state?.vike) {
/* We don't use the assert() below to save client-side KBs.
const vikeData = state.vike
assert(isObject(vikeData))
assert(hasProp(vikeData, 'timestamp', 'number'))
assert(hasProp(vikeData, 'scrollPosition'))
if (vikeData.scrollPosition !== null) {
assert(hasProp(vikeData, 'scrollPosition', 'object'))
assert(hasProp(vikeData.scrollPosition, 'x', 'number') && hasProp(vikeData.scrollPosition, 'y', 'number'))
}
//*/
return true;
}
return false;
}
function assertIsEnhanced(state) {
if (isEnhanced(state))
return;
assert(false, { state });
}
function getHistoryInfo() {
return {
url: getCurrentUrl(),
state: getState(),
};
}
function onPopStateBegin() {
const { previous } = globalObject;
const isStateEnhanced = isEnhanced(window.history.state);
// Either:
// - `window.history.pushState(null, '', '/some-path')` , or
// - hash navigation
// - Click on `<a href="#some-hash">`
// - Using the `location` API (only hash navigation)
// See comments a the top of the ./initOnPopState.ts file.
const isStatePristine = window.history.state === null;
if (!isStateEnhanced && !isStatePristine) {
// Going to a history entry not created by Vike — entering another "SPA realm" => hard reload
// https://github.com/vikejs/vike/issues/2801#issuecomment-3490431479
redirectHard(getCurrentUrl());
return { skip: true };
}
if (!isStateEnhanced)
enhance();
const current = getHistoryInfo();
globalObject.previous = current;
// Let the browser handle hash navigations.
// - Upon hash navigation: `isHistoryStatePristine===true` (see comment above).
if (isStatePristine) {
return { skip: true };
}
return { previous, current };
}
function initHistory() {
monkeyPatchHistoryAPI(); // the earlier we call it the better (Vike can workaround erroneous library monkey patches if Vike is the last one in the monkey patch chain)
enhance(); // enhance very first window.history.state which is `null`
}