vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
158 lines (157 loc) • 6.51 kB
JavaScript
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();
}