UNPKG

9.04 kBJavaScriptView Raw
1var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
2
3var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
4
5import warning from 'warning';
6import invariant from 'invariant';
7import { createLocation } from './LocationUtils';
8import { addLeadingSlash, stripTrailingSlash, stripPrefix, parsePath, createPath } from './PathUtils';
9import createTransitionManager from './createTransitionManager';
10import { canUseDOM, addEventListener, removeEventListener, getConfirmation, supportsHistory, supportsPopStateOnHashChange, isExtraneousPopstateEvent } from './DOMUtils';
11
12var PopStateEvent = 'popstate';
13var HashChangeEvent = 'hashchange';
14
15var getHistoryState = function getHistoryState() {
16 try {
17 return window.history.state || {};
18 } catch (e) {
19 // IE 11 sometimes throws when accessing window.history.state
20 // See https://github.com/ReactTraining/history/pull/289
21 return {};
22 }
23};
24
25/**
26 * Creates a history object that uses the HTML5 history API including
27 * pushState, replaceState, and the popstate event.
28 */
29var createBrowserHistory = function createBrowserHistory() {
30 var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
31
32 invariant(canUseDOM, 'Browser history needs a DOM');
33
34 var globalHistory = window.history;
35 var canUseHistory = supportsHistory();
36 var needsHashChangeListener = !supportsPopStateOnHashChange();
37
38 var _props$forceRefresh = props.forceRefresh,
39 forceRefresh = _props$forceRefresh === undefined ? false : _props$forceRefresh,
40 _props$getUserConfirm = props.getUserConfirmation,
41 getUserConfirmation = _props$getUserConfirm === undefined ? getConfirmation : _props$getUserConfirm,
42 _props$keyLength = props.keyLength,
43 keyLength = _props$keyLength === undefined ? 6 : _props$keyLength;
44
45 var basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
46
47 var getDOMLocation = function getDOMLocation(historyState) {
48 var _ref = historyState || {},
49 key = _ref.key,
50 state = _ref.state;
51
52 var _window$location = window.location,
53 pathname = _window$location.pathname,
54 search = _window$location.search,
55 hash = _window$location.hash;
56
57
58 var path = pathname + search + hash;
59
60 if (basename) path = stripPrefix(path, basename);
61
62 return _extends({}, parsePath(path), {
63 state: state,
64 key: key
65 });
66 };
67
68 var createKey = function createKey() {
69 return Math.random().toString(36).substr(2, keyLength);
70 };
71
72 var transitionManager = createTransitionManager();
73
74 var setState = function setState(nextState) {
75 _extends(history, nextState);
76
77 history.length = globalHistory.length;
78
79 transitionManager.notifyListeners(history.location, history.action);
80 };
81
82 var handlePopState = function handlePopState(event) {
83 // Ignore extraneous popstate events in WebKit.
84 if (isExtraneousPopstateEvent(event)) return;
85
86 handlePop(getDOMLocation(event.state));
87 };
88
89 var handleHashChange = function handleHashChange() {
90 handlePop(getDOMLocation(getHistoryState()));
91 };
92
93 var forceNextPop = false;
94
95 var handlePop = function handlePop(location) {
96 if (forceNextPop) {
97 forceNextPop = false;
98 setState();
99 } else {
100 var action = 'POP';
101
102 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
103 if (ok) {
104 setState({ action: action, location: location });
105 } else {
106 revertPop(location);
107 }
108 });
109 }
110 };
111
112 var revertPop = function revertPop(fromLocation) {
113 var toLocation = history.location;
114
115 // TODO: We could probably make this more reliable by
116 // keeping a list of keys we've seen in sessionStorage.
117 // Instead, we just default to 0 for keys we don't know.
118
119 var toIndex = allKeys.indexOf(toLocation.key);
120
121 if (toIndex === -1) toIndex = 0;
122
123 var fromIndex = allKeys.indexOf(fromLocation.key);
124
125 if (fromIndex === -1) fromIndex = 0;
126
127 var delta = toIndex - fromIndex;
128
129 if (delta) {
130 forceNextPop = true;
131 go(delta);
132 }
133 };
134
135 var initialLocation = getDOMLocation(getHistoryState());
136 var allKeys = [initialLocation.key];
137
138 // Public interface
139
140 var createHref = function createHref(location) {
141 return basename + createPath(location);
142 };
143
144 var push = function push(path, state) {
145 warning(!((typeof path === 'undefined' ? 'undefined' : _typeof(path)) === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to push when the 1st ' + 'argument is a location-like object that already has state; it is ignored');
146
147 var action = 'PUSH';
148 var location = createLocation(path, state, createKey(), history.location);
149
150 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
151 if (!ok) return;
152
153 var href = createHref(location);
154 var key = location.key,
155 state = location.state;
156
157
158 if (canUseHistory) {
159 globalHistory.pushState({ key: key, state: state }, null, href);
160
161 if (forceRefresh) {
162 window.location.href = href;
163 } else {
164 var prevIndex = allKeys.indexOf(history.location.key);
165 var nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
166
167 nextKeys.push(location.key);
168 allKeys = nextKeys;
169
170 setState({ action: action, location: location });
171 }
172 } else {
173 warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
174
175 window.location.href = href;
176 }
177 });
178 };
179
180 var replace = function replace(path, state) {
181 warning(!((typeof path === 'undefined' ? 'undefined' : _typeof(path)) === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to replace when the 1st ' + 'argument is a location-like object that already has state; it is ignored');
182
183 var action = 'REPLACE';
184 var location = createLocation(path, state, createKey(), history.location);
185
186 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
187 if (!ok) return;
188
189 var href = createHref(location);
190 var key = location.key,
191 state = location.state;
192
193
194 if (canUseHistory) {
195 globalHistory.replaceState({ key: key, state: state }, null, href);
196
197 if (forceRefresh) {
198 window.location.replace(href);
199 } else {
200 var prevIndex = allKeys.indexOf(history.location.key);
201
202 if (prevIndex !== -1) allKeys[prevIndex] = location.key;
203
204 setState({ action: action, location: location });
205 }
206 } else {
207 warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history');
208
209 window.location.replace(href);
210 }
211 });
212 };
213
214 var go = function go(n) {
215 globalHistory.go(n);
216 };
217
218 var goBack = function goBack() {
219 return go(-1);
220 };
221
222 var goForward = function goForward() {
223 return go(1);
224 };
225
226 var listenerCount = 0;
227
228 var checkDOMListeners = function checkDOMListeners(delta) {
229 listenerCount += delta;
230
231 if (listenerCount === 1) {
232 addEventListener(window, PopStateEvent, handlePopState);
233
234 if (needsHashChangeListener) addEventListener(window, HashChangeEvent, handleHashChange);
235 } else if (listenerCount === 0) {
236 removeEventListener(window, PopStateEvent, handlePopState);
237
238 if (needsHashChangeListener) removeEventListener(window, HashChangeEvent, handleHashChange);
239 }
240 };
241
242 var isBlocked = false;
243
244 var block = function block() {
245 var prompt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
246
247 var unblock = transitionManager.setPrompt(prompt);
248
249 if (!isBlocked) {
250 checkDOMListeners(1);
251 isBlocked = true;
252 }
253
254 return function () {
255 if (isBlocked) {
256 isBlocked = false;
257 checkDOMListeners(-1);
258 }
259
260 return unblock();
261 };
262 };
263
264 var listen = function listen(listener) {
265 var unlisten = transitionManager.appendListener(listener);
266 checkDOMListeners(1);
267
268 return function () {
269 checkDOMListeners(-1);
270 unlisten();
271 };
272 };
273
274 var history = {
275 length: globalHistory.length,
276 action: 'POP',
277 location: initialLocation,
278 createHref: createHref,
279 push: push,
280 replace: replace,
281 go: go,
282 goBack: goBack,
283 goForward: goForward,
284 block: block,
285 listen: listen
286 };
287
288 return history;
289};
290
291export default createBrowserHistory;
\No newline at end of file