1 |
|
2 | import { createLocation, locationsAreEqual, createKey } from './location-utils';
|
3 | import { warning } from './log';
|
4 | import { addLeadingSlash, stripLeadingSlash, stripTrailingSlash, hasBasename, stripBasename, createPath } from './path-utils';
|
5 | import createTransitionManager from './createTransitionManager';
|
6 | import { getConfirmation, supportsGoWithoutReloadUsingHash } from './dom-utils';
|
7 | const HashChangeEvent = 'hashchange';
|
8 | const 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 | };
|
22 | const 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 |
|
36 |
|
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 |
|
66 | replaceHashPath(encodedPath);
|
67 | }
|
68 | else {
|
69 | const location = getDOMLocation();
|
70 | const prevLocation = history.location;
|
71 | if (!forceNextPop && locationsAreEqual(prevLocation, location)) {
|
72 | return;
|
73 | }
|
74 | if (ignorePath === createPath(location)) {
|
75 | return;
|
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 |
|
101 |
|
102 |
|
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 |
|
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 |
|
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 |
|
140 |
|
141 |
|
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 |
|
169 |
|
170 |
|
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 | };
|
234 | export default createHashHistory;
|