UNPKG

9.51 kBJavaScriptView Raw
1// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
2import { createLocation, createKey } from './location-utils';
3import { warning } from './log';
4import { addLeadingSlash, stripTrailingSlash, hasBasename, stripBasename, createPath } from './path-utils';
5import createTransitionManager from './createTransitionManager';
6import createScrollHistory from './createScrollHistory';
7import { getConfirmation, supportsHistory, supportsPopStateOnHashChange, isExtraneousPopstateEvent } from './dom-utils';
8const PopStateEvent = 'popstate';
9const HashChangeEvent = 'hashchange';
10/**
11 * Creates a history object that uses the HTML5 history API including
12 * pushState, replaceState, and the popstate event.
13 */
14const createBrowserHistory = (win, props = {}) => {
15 let forceNextPop = false;
16 const globalHistory = win.history;
17 const globalLocation = win.location;
18 const globalNavigator = win.navigator;
19 const canUseHistory = supportsHistory(win);
20 const needsHashChangeListener = !supportsPopStateOnHashChange(globalNavigator);
21 const scrollHistory = createScrollHistory(win);
22 const forceRefresh = (props.forceRefresh != null) ? props.forceRefresh : false;
23 const getUserConfirmation = (props.getUserConfirmation != null) ? props.getUserConfirmation : getConfirmation;
24 const keyLength = (props.keyLength != null) ? props.keyLength : 6;
25 const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
26 const getHistoryState = () => {
27 try {
28 return win.history.state || {};
29 }
30 catch (e) {
31 // IE 11 sometimes throws when accessing window.history.state
32 // See https://github.com/ReactTraining/history/pull/289
33 return {};
34 }
35 };
36 const getDOMLocation = (historyState) => {
37 historyState = historyState || {};
38 const { key, state } = historyState;
39 const { pathname, search, hash } = globalLocation;
40 let path = pathname + search + hash;
41 warning((!basename || hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' +
42 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".');
43 if (basename) {
44 path = stripBasename(path, basename);
45 }
46 return createLocation(path, state, key || createKey(keyLength));
47 };
48 const transitionManager = createTransitionManager();
49 const setState = (nextState) => {
50 // Capture location for the view before changing history.
51 scrollHistory.capture(history.location.key);
52 Object.assign(history, nextState);
53 // Set scroll position based on its previous storage value
54 history.location.scrollPosition = scrollHistory.get(history.location.key);
55 history.length = globalHistory.length;
56 transitionManager.notifyListeners(history.location, history.action);
57 };
58 const handlePopState = (event) => {
59 // Ignore extraneous popstate events in WebKit.
60 if (!isExtraneousPopstateEvent(globalNavigator, event)) {
61 handlePop(getDOMLocation(event.state));
62 }
63 };
64 const handleHashChange = () => {
65 handlePop(getDOMLocation(getHistoryState()));
66 };
67 const handlePop = (location) => {
68 if (forceNextPop) {
69 forceNextPop = false;
70 setState();
71 }
72 else {
73 const action = 'POP';
74 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
75 if (ok) {
76 setState({ action, location });
77 }
78 else {
79 revertPop(location);
80 }
81 });
82 }
83 };
84 const revertPop = (fromLocation) => {
85 const toLocation = history.location;
86 // TODO: We could probably make this more reliable by
87 // keeping a list of keys we've seen in sessionStorage.
88 // Instead, we just default to 0 for keys we don't know.
89 let toIndex = allKeys.indexOf(toLocation.key);
90 let fromIndex = allKeys.indexOf(fromLocation.key);
91 if (toIndex === -1) {
92 toIndex = 0;
93 }
94 if (fromIndex === -1) {
95 fromIndex = 0;
96 }
97 const delta = toIndex - fromIndex;
98 if (delta) {
99 forceNextPop = true;
100 go(delta);
101 }
102 };
103 const initialLocation = getDOMLocation(getHistoryState());
104 let allKeys = [initialLocation.key];
105 let listenerCount = 0;
106 let isBlocked = false;
107 // Public interface
108 const createHref = (location) => {
109 return basename + createPath(location);
110 };
111 const push = (path, state) => {
112 warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to push when the 1st ' +
113 'argument is a location-like object that already has state; it is ignored');
114 const action = 'PUSH';
115 const location = createLocation(path, state, createKey(keyLength), history.location);
116 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
117 if (!ok) {
118 return;
119 }
120 const href = createHref(location);
121 const { key, state } = location;
122 if (canUseHistory) {
123 globalHistory.pushState({ key, state }, '', href);
124 if (forceRefresh) {
125 globalLocation.href = href;
126 }
127 else {
128 const prevIndex = allKeys.indexOf(history.location.key);
129 const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
130 nextKeys.push(location.key);
131 allKeys = nextKeys;
132 setState({ action, location });
133 }
134 }
135 else {
136 warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
137 globalLocation.href = href;
138 }
139 });
140 };
141 const replace = (path, state) => {
142 warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to replace when the 1st ' +
143 'argument is a location-like object that already has state; it is ignored');
144 const action = 'REPLACE';
145 const location = createLocation(path, state, createKey(keyLength), history.location);
146 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
147 if (!ok) {
148 return;
149 }
150 const href = createHref(location);
151 const { key, state } = location;
152 if (canUseHistory) {
153 globalHistory.replaceState({ key, state }, '', href);
154 if (forceRefresh) {
155 globalLocation.replace(href);
156 }
157 else {
158 const prevIndex = allKeys.indexOf(history.location.key);
159 if (prevIndex !== -1) {
160 allKeys[prevIndex] = location.key;
161 }
162 setState({ action, location });
163 }
164 }
165 else {
166 warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history');
167 globalLocation.replace(href);
168 }
169 });
170 };
171 const go = (n) => {
172 globalHistory.go(n);
173 };
174 const goBack = () => go(-1);
175 const goForward = () => go(1);
176 const checkDOMListeners = (delta) => {
177 listenerCount += delta;
178 if (listenerCount === 1) {
179 win.addEventListener(PopStateEvent, handlePopState);
180 if (needsHashChangeListener) {
181 win.addEventListener(HashChangeEvent, handleHashChange);
182 }
183 }
184 else if (listenerCount === 0) {
185 win.removeEventListener(PopStateEvent, handlePopState);
186 if (needsHashChangeListener) {
187 win.removeEventListener(HashChangeEvent, handleHashChange);
188 }
189 }
190 };
191 const block = (prompt = '') => {
192 const unblock = transitionManager.setPrompt(prompt);
193 if (!isBlocked) {
194 checkDOMListeners(1);
195 isBlocked = true;
196 }
197 return () => {
198 if (isBlocked) {
199 isBlocked = false;
200 checkDOMListeners(-1);
201 }
202 return unblock();
203 };
204 };
205 const listen = (listener) => {
206 const unlisten = transitionManager.appendListener(listener);
207 checkDOMListeners(1);
208 return () => {
209 checkDOMListeners(-1);
210 unlisten();
211 };
212 };
213 const history = {
214 length: globalHistory.length,
215 action: 'POP',
216 location: initialLocation,
217 createHref,
218 push,
219 replace,
220 go,
221 goBack,
222 goForward,
223 block,
224 listen,
225 win: win
226 };
227 return history;
228};
229export default createBrowserHistory;