1 |
|
2 | import { createLocation, createKey } from './location-utils';
|
3 | import { warning } from './log';
|
4 | import { addLeadingSlash, stripTrailingSlash, hasBasename, stripBasename, createPath } from './path-utils';
|
5 | import createTransitionManager from './createTransitionManager';
|
6 | import createScrollHistory from './createScrollHistory';
|
7 | import { getConfirmation, supportsHistory, supportsPopStateOnHashChange, isExtraneousPopstateEvent } from './dom-utils';
|
8 | const PopStateEvent = 'popstate';
|
9 | const HashChangeEvent = 'hashchange';
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | const 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 |
|
32 |
|
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 |
|
51 | scrollHistory.capture(history.location.key);
|
52 | Object.assign(history, nextState);
|
53 |
|
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 |
|
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 |
|
87 |
|
88 |
|
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 |
|
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 | };
|
229 | export default createBrowserHistory;
|