Created
July 26, 2018 20:38
-
-
Save l0gicgate/1a49b11597f03118dc7d8a21a79e8cc4 to your computer and use it in GitHub Desktop.
History.block behavior fix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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