createHashHistory
const HashChangeEvent = "hashchange";
const HashPathCoders = {
hashbang: {
encodePath: path =>
path.charAt(0) === "!" ? path : "!/" + stripLeadingSlash(path),
decodePath: path => (path.charAt(0) === "!" ? path.substr(1) : path)
},
noslash: {
encodePath: stripLeadingSlash,
decodePath: addLeadingSlash
},
slash: {
encodePath: addLeadingSlash,
decodePath: addLeadingSlash
}
};
function stripHash(url) {
const hashIndex = url.indexOf("#");
return hashIndex === -1 ? url : url.slice(0, hashIndex);
}
function getHashPath() {
const href = window.location.href;
const hashIndex = href.indexOf("#");
return hashIndex === -1 ? "" : href.substring(hashIndex + 1);
}
function pushHashPath(path) {
window.location.hash = path;
}
function replaceHashPath(path) {
window.location.replace(stripHash(window.location.href) + "#" + path);
}
function createHashHistory(props = {}) {
invariant(canUseDOM, "Hash history needs a DOM");
const globalHistory = window.history;
const canGoWithoutReload = supportsGoWithoutReloadUsingHash();
const { getUserConfirmation = getConfirmation, hashType = "slash" } = props;
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename))
: "";
const { encodePath, decodePath } = HashPathCoders[hashType];
function getDOMLocation() {
let path = decodePath(getHashPath());
warning(
!basename || hasBasename(path, basename),
"You are attempting to use a basename on a page whose URL path does not begin " +
'with the basename. Expected path "' +
path +
'" to begin with "' +
basename +
'".'
);
if (basename) path = stripBasename(path, basename);
return createLocation(path);
}
const transitionManager = createTransitionManager();
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
let forceNextPop = false;
let ignorePath = null;
function locationsAreEqual(a, b) {
return (
a.pathname === b.pathname &&
a.search === b.search &&
a.hash === b.hash
);
}
function handleHashChange() {
const path = getHashPath();
const encodedPath = encodePath(path);
if (path !== encodedPath) {
replaceHashPath(encodedPath);
} else {
const location = getDOMLocation();
const prevLocation = history.location;
if (!forceNextPop && locationsAreEqual(prevLocation, location))
return;
if (ignorePath === createPath(location)) return;
ignorePath = null;
handlePop(location);
}
}
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
const action = "POP";
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
}
}
);
}
}
function revertPop(fromLocation) {
const toLocation = history.location;
let toIndex = allPaths.lastIndexOf(createPath(toLocation));
if (toIndex === -1) toIndex = 0;
let fromIndex = allPaths.lastIndexOf(createPath(fromLocation));
if (fromIndex === -1) fromIndex = 0;
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
}
const path = getHashPath();
const encodedPath = encodePath(path);
if (path !== encodedPath) replaceHashPath(encodedPath);
const initialLocation = getDOMLocation();
let allPaths = [createPath(initialLocation)];
function createHref(location) {
const baseTag = document.querySelector("base");
let href = "";
if (baseTag && baseTag.getAttribute("href")) {
href = stripHash(window.location.href);
}
return href + "#" + encodePath(basename + createPath(location));
}
function push(path, state) {
warning(
state === undefined,
"Hash history cannot push state; it is ignored"
);
const action = "PUSH";
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const path = createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
ignorePath = path;
pushHashPath(encodedPath);
const prevIndex = allPaths.lastIndexOf(
createPath(history.location)
);
const nextPaths = allPaths.slice(0, prevIndex + 1);
nextPaths.push(path);
allPaths = nextPaths;
setState({ action, location });
} else {
warning(
false,
"Hash history cannot PUSH the same path; a new entry will not be added to the history stack"
);
setState();
}
}
);
}
function replace(path, state) {
warning(
state === undefined,
"Hash history cannot replace state; it is ignored"
);
const action = "REPLACE";
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const path = createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
ignorePath = path;
replaceHashPath(encodedPath);
}
const prevIndex = allPaths.indexOf(
createPath(history.location)
);
if (prevIndex !== -1) allPaths[prevIndex] = path;
setState({ action, location });
}
);
}
function go(n) {
warning(
canGoWithoutReload,
"Hash history go(n) causes a full page reload in this browser"
);
globalHistory.go(n);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
let listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(HashChangeEvent, handleHashChange);
}
}
let isBlocked = false;
function block(prompt = false) {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
return unblock();
};
}
function listen(listener) {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
}
const history = {
length: globalHistory.length,
action: "POP",
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
}