UNPKG

9.15 kBJavaScriptView Raw
1var _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; };
2
3import warning from 'warning';
4import invariant from 'invariant';
5import { createLocation, locationsAreEqual } from './LocationUtils';
6import { addLeadingSlash, stripLeadingSlash, stripTrailingSlash, stripPrefix, parsePath, createPath } from './PathUtils';
7import createTransitionManager from './createTransitionManager';
8import { canUseDOM, addEventListener, removeEventListener, getConfirmation, supportsGoWithoutReloadUsingHash } from './DOMUtils';
9
10var HashChangeEvent = 'hashchange';
11
12var HashPathCoders = {
13 hashbang: {
14 encodePath: function encodePath(path) {
15 return path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path);
16 },
17 decodePath: function decodePath(path) {
18 return path.charAt(0) === '!' ? path.substr(1) : path;
19 }
20 },
21 noslash: {
22 encodePath: stripLeadingSlash,
23 decodePath: addLeadingSlash
24 },
25 slash: {
26 encodePath: addLeadingSlash,
27 decodePath: addLeadingSlash
28 }
29};
30
31var getHashPath = function getHashPath() {
32 // We can't use window.location.hash here because it's not
33 // consistent across browsers - Firefox will pre-decode it!
34 var href = window.location.href;
35 var hashIndex = href.indexOf('#');
36 return hashIndex === -1 ? '' : href.substring(hashIndex + 1);
37};
38
39var pushHashPath = function pushHashPath(path) {
40 return window.location.hash = path;
41};
42
43var replaceHashPath = function replaceHashPath(path) {
44 var hashIndex = window.location.href.indexOf('#');
45
46 window.location.replace(window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path);
47};
48
49var createHashHistory = function createHashHistory() {
50 var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
51
52 invariant(canUseDOM, 'Hash history needs a DOM');
53
54 var globalHistory = window.history;
55 var canGoWithoutReload = supportsGoWithoutReloadUsingHash();
56
57 var _props$getUserConfirm = props.getUserConfirmation,
58 getUserConfirmation = _props$getUserConfirm === undefined ? getConfirmation : _props$getUserConfirm,
59 _props$hashType = props.hashType,
60 hashType = _props$hashType === undefined ? 'slash' : _props$hashType;
61
62 var basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
63
64 var _HashPathCoders$hashT = HashPathCoders[hashType],
65 encodePath = _HashPathCoders$hashT.encodePath,
66 decodePath = _HashPathCoders$hashT.decodePath;
67
68
69 var getDOMLocation = function getDOMLocation() {
70 var path = decodePath(getHashPath());
71
72 if (basename) path = stripPrefix(path, basename);
73
74 return parsePath(path);
75 };
76
77 var transitionManager = createTransitionManager();
78
79 var setState = function setState(nextState) {
80 _extends(history, nextState);
81
82 history.length = globalHistory.length;
83
84 transitionManager.notifyListeners(history.location, history.action);
85 };
86
87 var forceNextPop = false;
88 var ignorePath = null;
89
90 var handleHashChange = function handleHashChange() {
91 var path = getHashPath();
92 var encodedPath = encodePath(path);
93
94 if (path !== encodedPath) {
95 // Ensure we always have a properly-encoded hash.
96 replaceHashPath(encodedPath);
97 } else {
98 var location = getDOMLocation();
99 var prevLocation = history.location;
100
101 if (!forceNextPop && locationsAreEqual(prevLocation, location)) return; // A hashchange doesn't always == location change.
102
103 if (ignorePath === createPath(location)) return; // Ignore this change; we already setState in push/replace.
104
105 ignorePath = null;
106
107 handlePop(location);
108 }
109 };
110
111 var handlePop = function handlePop(location) {
112 if (forceNextPop) {
113 forceNextPop = false;
114 setState();
115 } else {
116 var action = 'POP';
117
118 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
119 if (ok) {
120 setState({ action: action, location: location });
121 } else {
122 revertPop(location);
123 }
124 });
125 }
126 };
127
128 var revertPop = function revertPop(fromLocation) {
129 var toLocation = history.location;
130
131 // TODO: We could probably make this more reliable by
132 // keeping a list of paths we've seen in sessionStorage.
133 // Instead, we just default to 0 for paths we don't know.
134
135 var toIndex = allPaths.lastIndexOf(createPath(toLocation));
136
137 if (toIndex === -1) toIndex = 0;
138
139 var fromIndex = allPaths.lastIndexOf(createPath(fromLocation));
140
141 if (fromIndex === -1) fromIndex = 0;
142
143 var delta = toIndex - fromIndex;
144
145 if (delta) {
146 forceNextPop = true;
147 go(delta);
148 }
149 };
150
151 // Ensure the hash is encoded properly before doing anything else.
152 var path = getHashPath();
153 var encodedPath = encodePath(path);
154
155 if (path !== encodedPath) replaceHashPath(encodedPath);
156
157 var initialLocation = getDOMLocation();
158 var allPaths = [createPath(initialLocation)];
159
160 // Public interface
161
162 var createHref = function createHref(location) {
163 return '#' + encodePath(basename + createPath(location));
164 };
165
166 var push = function push(path, state) {
167 warning(state === undefined, 'Hash history cannot push state; it is ignored');
168
169 var action = 'PUSH';
170 var location = createLocation(path, undefined, undefined, history.location);
171
172 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
173 if (!ok) return;
174
175 var path = createPath(location);
176 var encodedPath = encodePath(basename + path);
177 var hashChanged = getHashPath() !== encodedPath;
178
179 if (hashChanged) {
180 // We cannot tell if a hashchange was caused by a PUSH, so we'd
181 // rather setState here and ignore the hashchange. The caveat here
182 // is that other hash histories in the page will consider it a POP.
183 ignorePath = path;
184 pushHashPath(encodedPath);
185
186 var prevIndex = allPaths.lastIndexOf(createPath(history.location));
187 var nextPaths = allPaths.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
188
189 nextPaths.push(path);
190 allPaths = nextPaths;
191
192 setState({ action: action, location: location });
193 } else {
194 warning(false, 'Hash history cannot PUSH the same path; a new entry will not be added to the history stack');
195
196 setState();
197 }
198 });
199 };
200
201 var replace = function replace(path, state) {
202 warning(state === undefined, 'Hash history cannot replace state; it is ignored');
203
204 var action = 'REPLACE';
205 var location = createLocation(path, undefined, undefined, history.location);
206
207 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
208 if (!ok) return;
209
210 var path = createPath(location);
211 var encodedPath = encodePath(basename + path);
212 var hashChanged = getHashPath() !== encodedPath;
213
214 if (hashChanged) {
215 // We cannot tell if a hashchange was caused by a REPLACE, so we'd
216 // rather setState here and ignore the hashchange. The caveat here
217 // is that other hash histories in the page will consider it a POP.
218 ignorePath = path;
219 replaceHashPath(encodedPath);
220 }
221
222 var prevIndex = allPaths.indexOf(createPath(history.location));
223
224 if (prevIndex !== -1) allPaths[prevIndex] = path;
225
226 setState({ action: action, location: location });
227 });
228 };
229
230 var go = function go(n) {
231 warning(canGoWithoutReload, 'Hash history go(n) causes a full page reload in this browser');
232
233 globalHistory.go(n);
234 };
235
236 var goBack = function goBack() {
237 return go(-1);
238 };
239
240 var goForward = function goForward() {
241 return go(1);
242 };
243
244 var listenerCount = 0;
245
246 var checkDOMListeners = function checkDOMListeners(delta) {
247 listenerCount += delta;
248
249 if (listenerCount === 1) {
250 addEventListener(window, HashChangeEvent, handleHashChange);
251 } else if (listenerCount === 0) {
252 removeEventListener(window, HashChangeEvent, handleHashChange);
253 }
254 };
255
256 var isBlocked = false;
257
258 var block = function block() {
259 var prompt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
260
261 var unblock = transitionManager.setPrompt(prompt);
262
263 if (!isBlocked) {
264 checkDOMListeners(1);
265 isBlocked = true;
266 }
267
268 return function () {
269 if (isBlocked) {
270 isBlocked = false;
271 checkDOMListeners(-1);
272 }
273
274 return unblock();
275 };
276 };
277
278 var listen = function listen(listener) {
279 var unlisten = transitionManager.appendListener(listener);
280 checkDOMListeners(1);
281
282 return function () {
283 checkDOMListeners(-1);
284 unlisten();
285 };
286 };
287
288 var history = {
289 length: globalHistory.length,
290 action: 'POP',
291 location: initialLocation,
292 createHref: createHref,
293 push: push,
294 replace: replace,
295 go: go,
296 goBack: goBack,
297 goForward: goForward,
298 block: block,
299 listen: listen
300 };
301
302 return history;
303};
304
305export default createHashHistory;
\No newline at end of file