UNPKG

9.73 kBJavaScriptView Raw
1// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
2import { createLocation, locationsAreEqual, createKey } from './location-utils';
3import { warning } from './log';
4import { addLeadingSlash, stripLeadingSlash, stripTrailingSlash, hasBasename, stripBasename, createPath } from './path-utils';
5import createTransitionManager from './createTransitionManager';
6import { getConfirmation, supportsGoWithoutReloadUsingHash } from './dom-utils';
7const HashChangeEvent = 'hashchange';
8const HashPathCoders = {
9 hashbang: {
10 encodePath: (path) => path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path),
11 decodePath: (path) => path.charAt(0) === '!' ? path.substr(1) : path
12 },
13 noslash: {
14 encodePath: stripLeadingSlash,
15 decodePath: addLeadingSlash
16 },
17 slash: {
18 encodePath: addLeadingSlash,
19 decodePath: addLeadingSlash
20 }
21};
22const createHashHistory = (win, props = {}) => {
23 let forceNextPop = false;
24 let ignorePath = null;
25 let listenerCount = 0;
26 let isBlocked = false;
27 const globalLocation = win.location;
28 const globalHistory = win.history;
29 const canGoWithoutReload = supportsGoWithoutReloadUsingHash(win.navigator);
30 const keyLength = (props.keyLength != null) ? props.keyLength : 6;
31 const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props;
32 const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
33 const { encodePath, decodePath } = HashPathCoders[hashType];
34 const getHashPath = () => {
35 // We can't use window.location.hash here because it's not
36 // consistent across browsers - Firefox will pre-decode it!
37 const href = globalLocation.href;
38 const hashIndex = href.indexOf('#');
39 return hashIndex === -1 ? '' : href.substring(hashIndex + 1);
40 };
41 const pushHashPath = (path) => (globalLocation.hash = path);
42 const replaceHashPath = (path) => {
43 const hashIndex = globalLocation.href.indexOf('#');
44 globalLocation.replace(globalLocation.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path);
45 };
46 const getDOMLocation = () => {
47 let path = decodePath(getHashPath());
48 warning((!basename || hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' +
49 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".');
50 if (basename) {
51 path = stripBasename(path, basename);
52 }
53 return createLocation(path, undefined, createKey(keyLength));
54 };
55 const transitionManager = createTransitionManager();
56 const setState = (nextState) => {
57 Object.assign(history, nextState);
58 history.length = globalHistory.length;
59 transitionManager.notifyListeners(history.location, history.action);
60 };
61 const handleHashChange = () => {
62 const path = getHashPath();
63 const encodedPath = encodePath(path);
64 if (path !== encodedPath) {
65 // Ensure we always have a properly-encoded hash.
66 replaceHashPath(encodedPath);
67 }
68 else {
69 const location = getDOMLocation();
70 const prevLocation = history.location;
71 if (!forceNextPop && locationsAreEqual(prevLocation, location)) {
72 return; // A hashchange doesn't always == location change.
73 }
74 if (ignorePath === createPath(location)) {
75 return; // Ignore this change; we already setState in push/replace.
76 }
77 ignorePath = null;
78 handlePop(location);
79 }
80 };
81 const handlePop = (location) => {
82 if (forceNextPop) {
83 forceNextPop = false;
84 setState();
85 }
86 else {
87 const action = 'POP';
88 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
89 if (ok) {
90 setState({ action, location });
91 }
92 else {
93 revertPop(location);
94 }
95 });
96 }
97 };
98 const revertPop = (fromLocation) => {
99 const toLocation = history.location;
100 // TODO: We could probably make this more reliable by
101 // keeping a list of paths we've seen in sessionStorage.
102 // Instead, we just default to 0 for paths we don't know.
103 let toIndex = allPaths.lastIndexOf(createPath(toLocation));
104 let fromIndex = allPaths.lastIndexOf(createPath(fromLocation));
105 if (toIndex === -1) {
106 toIndex = 0;
107 }
108 if (fromIndex === -1) {
109 fromIndex = 0;
110 }
111 const delta = toIndex - fromIndex;
112 if (delta) {
113 forceNextPop = true;
114 go(delta);
115 }
116 };
117 // Ensure the hash is encoded properly before doing anything else.
118 const path = getHashPath();
119 const encodedPath = encodePath(path);
120 if (path !== encodedPath) {
121 replaceHashPath(encodedPath);
122 }
123 const initialLocation = getDOMLocation();
124 let allPaths = [createPath(initialLocation)];
125 // Public interface
126 const createHref = (location) => ('#' + encodePath(basename + createPath(location)));
127 const push = (path, state) => {
128 warning(state === undefined, 'Hash history cannot push state; it is ignored');
129 const action = 'PUSH';
130 const location = createLocation(path, undefined, createKey(keyLength), history.location);
131 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
132 if (!ok) {
133 return;
134 }
135 const path = createPath(location);
136 const encodedPath = encodePath(basename + path);
137 const hashChanged = getHashPath() !== encodedPath;
138 if (hashChanged) {
139 // We cannot tell if a hashchange was caused by a PUSH, so we'd
140 // rather setState here and ignore the hashchange. The caveat here
141 // is that other hash histories in the page will consider it a POP.
142 ignorePath = path;
143 pushHashPath(encodedPath);
144 const prevIndex = allPaths.lastIndexOf(createPath(history.location));
145 const nextPaths = allPaths.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
146 nextPaths.push(path);
147 allPaths = nextPaths;
148 setState({ action, location });
149 }
150 else {
151 warning(false, 'Hash history cannot PUSH the same path; a new entry will not be added to the history stack');
152 setState();
153 }
154 });
155 };
156 const replace = (path, state) => {
157 warning(state === undefined, 'Hash history cannot replace state; it is ignored');
158 const action = 'REPLACE';
159 const location = createLocation(path, undefined, createKey(keyLength), history.location);
160 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
161 if (!ok) {
162 return;
163 }
164 const path = createPath(location);
165 const encodedPath = encodePath(basename + path);
166 const hashChanged = getHashPath() !== encodedPath;
167 if (hashChanged) {
168 // We cannot tell if a hashchange was caused by a REPLACE, so we'd
169 // rather setState here and ignore the hashchange. The caveat here
170 // is that other hash histories in the page will consider it a POP.
171 ignorePath = path;
172 replaceHashPath(encodedPath);
173 }
174 const prevIndex = allPaths.indexOf(createPath(history.location));
175 if (prevIndex !== -1) {
176 allPaths[prevIndex] = path;
177 }
178 setState({ action, location });
179 });
180 };
181 const go = (n) => {
182 warning(canGoWithoutReload, 'Hash history go(n) causes a full page reload in this browser');
183 globalHistory.go(n);
184 };
185 const goBack = () => go(-1);
186 const goForward = () => go(1);
187 const checkDOMListeners = (win, delta) => {
188 listenerCount += delta;
189 if (listenerCount === 1) {
190 win.addEventListener(HashChangeEvent, handleHashChange);
191 }
192 else if (listenerCount === 0) {
193 win.removeEventListener(HashChangeEvent, handleHashChange);
194 }
195 };
196 const block = (prompt = '') => {
197 const unblock = transitionManager.setPrompt(prompt);
198 if (!isBlocked) {
199 checkDOMListeners(win, 1);
200 isBlocked = true;
201 }
202 return () => {
203 if (isBlocked) {
204 isBlocked = false;
205 checkDOMListeners(win, -1);
206 }
207 return unblock();
208 };
209 };
210 const listen = (listener) => {
211 const unlisten = transitionManager.appendListener(listener);
212 checkDOMListeners(win, 1);
213 return () => {
214 checkDOMListeners(win, -1);
215 unlisten();
216 };
217 };
218 const history = {
219 length: globalHistory.length,
220 action: 'POP',
221 location: initialLocation,
222 createHref,
223 push,
224 replace,
225 go,
226 goBack,
227 goForward,
228 block,
229 listen,
230 win: win
231 };
232 return history;
233};
234export default createHashHistory;