Skip to content

Instantly share code, notes, and snippets.

@l0gicgate
Created July 26, 2018 20:38
Show Gist options
  • Save l0gicgate/1a49b11597f03118dc7d8a21a79e8cc4 to your computer and use it in GitHub Desktop.
Save l0gicgate/1a49b11597f03118dc7d8a21a79e8cc4 to your computer and use it in GitHub Desktop.
History.block behavior fix
import { createLocation } from 'history/LocationUtils';
import { addLeadingSlash, stripTrailingSlash, stripBasename, createPath } from 'history/PathUtils';
import { getConfirmation, supportsHistory, supportsPopStateOnHashChange, isExtraneousPopstateEvent } from 'history/DOMUtils';
import createTransitionManager from './createTransitionManager';
const PopStateEvent = 'popstate';
const HashChangeEvent = 'hashchange';
const getHistoryState = () => {
try {
return window.history.state || {};
} catch (e) {
// IE 11 sometimes throws when accessing window.history.state
// See https://github.com/ReactTraining/history/pull/289
return {};
}
};
/**
* Creates a history object that uses the HTML5 history API including
* pushState, replaceState, and the popstate event.
*/
const createBrowserHistory = (props = {}) => {
let history = {};
const globalHistory = window.history;
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
const { forceRefresh = false, getUserConfirmation = getConfirmation, keyLength = 6 } = props;
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename))
: '';
let forceNextPop = false;
const getDOMLocation = (historyState) => {
const { key, state } = historyState || {};
const { pathname, search, hash } = window.location;
let path = pathname + search + hash;
if (basename) {
path = stripBasename(path, basename);
}
return createLocation(path, state, key);
};
const createKey = () =>
Math.random()
.toString(36)
.substr(2, keyLength);
const transitionManager = createTransitionManager();
const setState = (nextState) => {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
};
const initialLocation = getDOMLocation(getHistoryState());
let allKeys = [initialLocation.key];
// Public interface
const createHref = location => basename + createPath(location);
const confirmBeforeTransition = (requestedLocation, handler) => {
if (history.locked && history.confirm) {
if (!history.requestedLocation) {
history.requestedLocation = requestedLocation;
}
history.go(1);
const transitionConfirmed = () => {
handler(history.requestedLocation);
history.confirm = null;
history.requestedLocation = null;
};
if (typeof history.confirm === 'function' && history.confirm(transitionConfirmed)) {
transitionConfirmed();
}
} else {
handler(requestedLocation);
}
};
const push = (path, newState) => {
const action = 'PUSH';
const location = createLocation(path, newState, createKey(), history.location);
const handler = () => transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
(ok) => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
const prevIndex = allKeys.indexOf(history.location.key);
const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action, location });
}
} else {
window.location.href = href;
}
},
);
confirmBeforeTransition(location, handler);
};
const replace = (path, newState) => {
const action = 'REPLACE';
const location = createLocation(path, newState, createKey(), history.location);
const handler = () => transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
(ok) => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.replaceState({ key, state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
const prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({ action, location });
}
} else {
window.location.replace(href);
}
},
);
confirmBeforeTransition(location, handler);
};
const go = n => globalHistory.go(n);
const goBack = () => go(-1);
const goForward = () => go(1);
const revertPop = (fromLocation) => {
const toLocation = history.location;
let toIndex = allKeys.indexOf(toLocation.key);
if (toIndex === -1) {
toIndex = 0;
}
let fromIndex = allKeys.indexOf(fromLocation.key);
if (fromIndex === -1) {
fromIndex = 0;
}
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
};
const 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);
}
},
);
}
};
const handlePopState = (event) => {
const requestedLocation = getDOMLocation(event.state);
const handler = (nextLocation) => {
if (isExtraneousPopstateEvent(event)) {
return;
}
handlePop(nextLocation);
};
confirmBeforeTransition(requestedLocation, handler);
};
const handleHashChange = () => {
handlePop(getDOMLocation(getHistoryState()));
};
let listenerCount = 0;
const checkDOMListeners = (delta) => {
listenerCount += delta;
if (listenerCount === 1) {
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) {
window.addEventListener(HashChangeEvent, handleHashChange);
}
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) {
window.removeEventListener(HashChangeEvent, handleHashChange);
}
}
};
let isBlocked = false;
const block = (prompt = false) => {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
return unblock();
};
};
const listen = (listener) => {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
};
const lock = (confirm) => {
history.locked = true;
history.confirm = confirm;
};
const unlock = () => {
history.locked = false;
history.confirm = null;
};
history = {
action: 'POP',
block,
createHref,
confirm: null,
go,
goBack,
goForward,
listen,
length: globalHistory.length,
location: initialLocation,
lock,
locked: false,
push,
replace,
requestedLocation: null,
unlock,
};
return history;
};
export default createBrowserHistory;
const createTransitionManager = () => {
let prompt = null;
const setPrompt = (nextPrompt) => {
prompt = nextPrompt;
return () => {
if (prompt === nextPrompt) prompt = null;
};
};
const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
// TODO: If another transition starts while we're still confirming
// the previous one, we may end up in a weird state. Figure out the
// best way to handle this.
if (prompt != null) {
const result =
typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback);
} else {
callback(true);
}
} else {
// Return false from a transition hook to cancel the transition.
callback(result !== false);
}
} else {
callback(true);
}
};
let listeners = [];
const appendListener = (fn) => {
let isActive = true;
const listener = (...args) => {
if (isActive) fn(...args);
};
listeners.push(listener);
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
};
};
const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args));
};
return {
setPrompt,
confirmTransitionTo,
appendListener,
notifyListeners,
};
};
export default createTransitionManager;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment