UNPKG

25.2 kBJavaScriptView Raw
1import { r as registerInstance, c as getContext, h, g as getElement } from './stencilrouter-1307249c.js';
2import { A as ActiveRouter } from './chunk-cfc6485e.js';
3import { s as stripTrailingSlash, c as createLocation, a as addLeadingSlash, b as stripBasename, d as createKey, e as createPath, h as hasBasename, f as stripLeadingSlash, l as locationsAreEqual } from './chunk-d2e78d53.js';
4import { s as storageAvailable, a as supportsHistory, b as supportsPopStateOnHashChange, g as getConfirmation, c as isExtraneousPopstateEvent, d as supportsGoWithoutReloadUsingHash } from './chunk-4eecdc1a.js';
5
6const warning = (value, ...args) => {
7 if (!value) {
8 console.warn(...args);
9 }
10};
11
12// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
13const createTransitionManager = () => {
14 let prompt;
15 let listeners = [];
16 const setPrompt = (nextPrompt) => {
17 warning(prompt == null, 'A history supports only one prompt at a time');
18 prompt = nextPrompt;
19 return () => {
20 if (prompt === nextPrompt) {
21 prompt = null;
22 }
23 };
24 };
25 const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
26 // TODO: If another transition starts while we're still confirming
27 // the previous one, we may end up in a weird state. Figure out the
28 // best way to handle this.
29 if (prompt != null) {
30 const result = typeof prompt === 'function' ? prompt(location, action) : prompt;
31 if (typeof result === 'string') {
32 if (typeof getUserConfirmation === 'function') {
33 getUserConfirmation(result, callback);
34 }
35 else {
36 warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message');
37 callback(true);
38 }
39 }
40 else {
41 // Return false from a transition hook to cancel the transition.
42 callback(result !== false);
43 }
44 }
45 else {
46 callback(true);
47 }
48 };
49 const appendListener = (fn) => {
50 let isActive = true;
51 const listener = (...args) => {
52 if (isActive) {
53 fn(...args);
54 }
55 };
56 listeners.push(listener);
57 return () => {
58 isActive = false;
59 listeners = listeners.filter(item => item !== listener);
60 };
61 };
62 const notifyListeners = (...args) => {
63 listeners.forEach(listener => listener(...args));
64 };
65 return {
66 setPrompt,
67 confirmTransitionTo,
68 appendListener,
69 notifyListeners
70 };
71};
72
73const createScrollHistory = (win, applicationScrollKey = 'scrollPositions') => {
74 let scrollPositions = new Map();
75 const set = (key, value) => {
76 scrollPositions.set(key, value);
77 if (storageAvailable(win, 'sessionStorage')) {
78 const arrayData = [];
79 scrollPositions.forEach((value, key) => {
80 arrayData.push([key, value]);
81 });
82 win.sessionStorage.setItem('scrollPositions', JSON.stringify(arrayData));
83 }
84 };
85 const get = (key) => {
86 return scrollPositions.get(key);
87 };
88 const has = (key) => {
89 return scrollPositions.has(key);
90 };
91 const capture = (key) => {
92 set(key, [win.scrollX, win.scrollY]);
93 };
94 if (storageAvailable(win, 'sessionStorage')) {
95 const scrollData = win.sessionStorage.getItem(applicationScrollKey);
96 scrollPositions = scrollData ?
97 new Map(JSON.parse(scrollData)) :
98 scrollPositions;
99 }
100 if ('scrollRestoration' in win.history) {
101 history.scrollRestoration = 'manual';
102 }
103 return {
104 set,
105 get,
106 has,
107 capture
108 };
109};
110
111// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
112const PopStateEvent = 'popstate';
113const HashChangeEvent = 'hashchange';
114/**
115 * Creates a history object that uses the HTML5 history API including
116 * pushState, replaceState, and the popstate event.
117 */
118const createBrowserHistory = (win, props = {}) => {
119 let forceNextPop = false;
120 const globalHistory = win.history;
121 const globalLocation = win.location;
122 const globalNavigator = win.navigator;
123 const canUseHistory = supportsHistory(win);
124 const needsHashChangeListener = !supportsPopStateOnHashChange(globalNavigator);
125 const scrollHistory = createScrollHistory(win);
126 const forceRefresh = (props.forceRefresh != null) ? props.forceRefresh : false;
127 const getUserConfirmation = (props.getUserConfirmation != null) ? props.getUserConfirmation : getConfirmation;
128 const keyLength = (props.keyLength != null) ? props.keyLength : 6;
129 const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
130 const getHistoryState = () => {
131 try {
132 return win.history.state || {};
133 }
134 catch (e) {
135 // IE 11 sometimes throws when accessing window.history.state
136 // See https://github.com/ReactTraining/history/pull/289
137 return {};
138 }
139 };
140 const getDOMLocation = (historyState) => {
141 historyState = historyState || {};
142 const { key, state } = historyState;
143 const { pathname, search, hash } = globalLocation;
144 let path = pathname + search + hash;
145 warning((!basename || hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' +
146 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".');
147 if (basename) {
148 path = stripBasename(path, basename);
149 }
150 return createLocation(path, state, key || createKey(keyLength));
151 };
152 const transitionManager = createTransitionManager();
153 const setState = (nextState) => {
154 // Capture location for the view before changing history.
155 scrollHistory.capture(history.location.key);
156 Object.assign(history, nextState);
157 // Set scroll position based on its previous storage value
158 history.location.scrollPosition = scrollHistory.get(history.location.key);
159 history.length = globalHistory.length;
160 transitionManager.notifyListeners(history.location, history.action);
161 };
162 const handlePopState = (event) => {
163 // Ignore extraneous popstate events in WebKit.
164 if (!isExtraneousPopstateEvent(globalNavigator, event)) {
165 handlePop(getDOMLocation(event.state));
166 }
167 };
168 const handleHashChange = () => {
169 handlePop(getDOMLocation(getHistoryState()));
170 };
171 const handlePop = (location) => {
172 if (forceNextPop) {
173 forceNextPop = false;
174 setState();
175 }
176 else {
177 const action = 'POP';
178 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
179 if (ok) {
180 setState({ action, location });
181 }
182 else {
183 revertPop(location);
184 }
185 });
186 }
187 };
188 const revertPop = (fromLocation) => {
189 const toLocation = history.location;
190 // TODO: We could probably make this more reliable by
191 // keeping a list of keys we've seen in sessionStorage.
192 // Instead, we just default to 0 for keys we don't know.
193 let toIndex = allKeys.indexOf(toLocation.key);
194 let fromIndex = allKeys.indexOf(fromLocation.key);
195 if (toIndex === -1) {
196 toIndex = 0;
197 }
198 if (fromIndex === -1) {
199 fromIndex = 0;
200 }
201 const delta = toIndex - fromIndex;
202 if (delta) {
203 forceNextPop = true;
204 go(delta);
205 }
206 };
207 const initialLocation = getDOMLocation(getHistoryState());
208 let allKeys = [initialLocation.key];
209 let listenerCount = 0;
210 let isBlocked = false;
211 // Public interface
212 const createHref = (location) => {
213 return basename + createPath(location);
214 };
215 const push = (path, state) => {
216 warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to push when the 1st ' +
217 'argument is a location-like object that already has state; it is ignored');
218 const action = 'PUSH';
219 const location = createLocation(path, state, createKey(keyLength), history.location);
220 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
221 if (!ok) {
222 return;
223 }
224 const href = createHref(location);
225 const { key, state } = location;
226 if (canUseHistory) {
227 globalHistory.pushState({ key, state }, '', href);
228 if (forceRefresh) {
229 globalLocation.href = href;
230 }
231 else {
232 const prevIndex = allKeys.indexOf(history.location.key);
233 const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
234 nextKeys.push(location.key);
235 allKeys = nextKeys;
236 setState({ action, location });
237 }
238 }
239 else {
240 warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
241 globalLocation.href = href;
242 }
243 });
244 };
245 const replace = (path, state) => {
246 warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to replace when the 1st ' +
247 'argument is a location-like object that already has state; it is ignored');
248 const action = 'REPLACE';
249 const location = createLocation(path, state, createKey(keyLength), history.location);
250 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
251 if (!ok) {
252 return;
253 }
254 const href = createHref(location);
255 const { key, state } = location;
256 if (canUseHistory) {
257 globalHistory.replaceState({ key, state }, '', href);
258 if (forceRefresh) {
259 globalLocation.replace(href);
260 }
261 else {
262 const prevIndex = allKeys.indexOf(history.location.key);
263 if (prevIndex !== -1) {
264 allKeys[prevIndex] = location.key;
265 }
266 setState({ action, location });
267 }
268 }
269 else {
270 warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history');
271 globalLocation.replace(href);
272 }
273 });
274 };
275 const go = (n) => {
276 globalHistory.go(n);
277 };
278 const goBack = () => go(-1);
279 const goForward = () => go(1);
280 const checkDOMListeners = (delta) => {
281 listenerCount += delta;
282 if (listenerCount === 1) {
283 win.addEventListener(PopStateEvent, handlePopState);
284 if (needsHashChangeListener) {
285 win.addEventListener(HashChangeEvent, handleHashChange);
286 }
287 }
288 else if (listenerCount === 0) {
289 win.removeEventListener(PopStateEvent, handlePopState);
290 if (needsHashChangeListener) {
291 win.removeEventListener(HashChangeEvent, handleHashChange);
292 }
293 }
294 };
295 const block = (prompt = '') => {
296 const unblock = transitionManager.setPrompt(prompt);
297 if (!isBlocked) {
298 checkDOMListeners(1);
299 isBlocked = true;
300 }
301 return () => {
302 if (isBlocked) {
303 isBlocked = false;
304 checkDOMListeners(-1);
305 }
306 return unblock();
307 };
308 };
309 const listen = (listener) => {
310 const unlisten = transitionManager.appendListener(listener);
311 checkDOMListeners(1);
312 return () => {
313 checkDOMListeners(-1);
314 unlisten();
315 };
316 };
317 const history = {
318 length: globalHistory.length,
319 action: 'POP',
320 location: initialLocation,
321 createHref,
322 push,
323 replace,
324 go,
325 goBack,
326 goForward,
327 block,
328 listen,
329 win: win
330 };
331 return history;
332};
333
334// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
335const HashChangeEvent$1 = 'hashchange';
336const HashPathCoders = {
337 hashbang: {
338 encodePath: (path) => path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path),
339 decodePath: (path) => path.charAt(0) === '!' ? path.substr(1) : path
340 },
341 noslash: {
342 encodePath: stripLeadingSlash,
343 decodePath: addLeadingSlash
344 },
345 slash: {
346 encodePath: addLeadingSlash,
347 decodePath: addLeadingSlash
348 }
349};
350const createHashHistory = (win, props = {}) => {
351 let forceNextPop = false;
352 let ignorePath = null;
353 let listenerCount = 0;
354 let isBlocked = false;
355 const globalLocation = win.location;
356 const globalHistory = win.history;
357 const canGoWithoutReload = supportsGoWithoutReloadUsingHash(win.navigator);
358 const keyLength = (props.keyLength != null) ? props.keyLength : 6;
359 const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props;
360 const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
361 const { encodePath, decodePath } = HashPathCoders[hashType];
362 const getHashPath = () => {
363 // We can't use window.location.hash here because it's not
364 // consistent across browsers - Firefox will pre-decode it!
365 const href = globalLocation.href;
366 const hashIndex = href.indexOf('#');
367 return hashIndex === -1 ? '' : href.substring(hashIndex + 1);
368 };
369 const pushHashPath = (path) => (globalLocation.hash = path);
370 const replaceHashPath = (path) => {
371 const hashIndex = globalLocation.href.indexOf('#');
372 globalLocation.replace(globalLocation.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path);
373 };
374 const getDOMLocation = () => {
375 let path = decodePath(getHashPath());
376 warning((!basename || hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' +
377 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".');
378 if (basename) {
379 path = stripBasename(path, basename);
380 }
381 return createLocation(path, undefined, createKey(keyLength));
382 };
383 const transitionManager = createTransitionManager();
384 const setState = (nextState) => {
385 Object.assign(history, nextState);
386 history.length = globalHistory.length;
387 transitionManager.notifyListeners(history.location, history.action);
388 };
389 const handleHashChange = () => {
390 const path = getHashPath();
391 const encodedPath = encodePath(path);
392 if (path !== encodedPath) {
393 // Ensure we always have a properly-encoded hash.
394 replaceHashPath(encodedPath);
395 }
396 else {
397 const location = getDOMLocation();
398 const prevLocation = history.location;
399 if (!forceNextPop && locationsAreEqual(prevLocation, location)) {
400 return; // A hashchange doesn't always == location change.
401 }
402 if (ignorePath === createPath(location)) {
403 return; // Ignore this change; we already setState in push/replace.
404 }
405 ignorePath = null;
406 handlePop(location);
407 }
408 };
409 const handlePop = (location) => {
410 if (forceNextPop) {
411 forceNextPop = false;
412 setState();
413 }
414 else {
415 const action = 'POP';
416 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
417 if (ok) {
418 setState({ action, location });
419 }
420 else {
421 revertPop(location);
422 }
423 });
424 }
425 };
426 const revertPop = (fromLocation) => {
427 const toLocation = history.location;
428 // TODO: We could probably make this more reliable by
429 // keeping a list of paths we've seen in sessionStorage.
430 // Instead, we just default to 0 for paths we don't know.
431 let toIndex = allPaths.lastIndexOf(createPath(toLocation));
432 let fromIndex = allPaths.lastIndexOf(createPath(fromLocation));
433 if (toIndex === -1) {
434 toIndex = 0;
435 }
436 if (fromIndex === -1) {
437 fromIndex = 0;
438 }
439 const delta = toIndex - fromIndex;
440 if (delta) {
441 forceNextPop = true;
442 go(delta);
443 }
444 };
445 // Ensure the hash is encoded properly before doing anything else.
446 const path = getHashPath();
447 const encodedPath = encodePath(path);
448 if (path !== encodedPath) {
449 replaceHashPath(encodedPath);
450 }
451 const initialLocation = getDOMLocation();
452 let allPaths = [createPath(initialLocation)];
453 // Public interface
454 const createHref = (location) => ('#' + encodePath(basename + createPath(location)));
455 const push = (path, state) => {
456 warning(state === undefined, 'Hash history cannot push state; it is ignored');
457 const action = 'PUSH';
458 const location = createLocation(path, undefined, createKey(keyLength), history.location);
459 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
460 if (!ok) {
461 return;
462 }
463 const path = createPath(location);
464 const encodedPath = encodePath(basename + path);
465 const hashChanged = getHashPath() !== encodedPath;
466 if (hashChanged) {
467 // We cannot tell if a hashchange was caused by a PUSH, so we'd
468 // rather setState here and ignore the hashchange. The caveat here
469 // is that other hash histories in the page will consider it a POP.
470 ignorePath = path;
471 pushHashPath(encodedPath);
472 const prevIndex = allPaths.lastIndexOf(createPath(history.location));
473 const nextPaths = allPaths.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
474 nextPaths.push(path);
475 allPaths = nextPaths;
476 setState({ action, location });
477 }
478 else {
479 warning(false, 'Hash history cannot PUSH the same path; a new entry will not be added to the history stack');
480 setState();
481 }
482 });
483 };
484 const replace = (path, state) => {
485 warning(state === undefined, 'Hash history cannot replace state; it is ignored');
486 const action = 'REPLACE';
487 const location = createLocation(path, undefined, createKey(keyLength), history.location);
488 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
489 if (!ok) {
490 return;
491 }
492 const path = createPath(location);
493 const encodedPath = encodePath(basename + path);
494 const hashChanged = getHashPath() !== encodedPath;
495 if (hashChanged) {
496 // We cannot tell if a hashchange was caused by a REPLACE, so we'd
497 // rather setState here and ignore the hashchange. The caveat here
498 // is that other hash histories in the page will consider it a POP.
499 ignorePath = path;
500 replaceHashPath(encodedPath);
501 }
502 const prevIndex = allPaths.indexOf(createPath(history.location));
503 if (prevIndex !== -1) {
504 allPaths[prevIndex] = path;
505 }
506 setState({ action, location });
507 });
508 };
509 const go = (n) => {
510 warning(canGoWithoutReload, 'Hash history go(n) causes a full page reload in this browser');
511 globalHistory.go(n);
512 };
513 const goBack = () => go(-1);
514 const goForward = () => go(1);
515 const checkDOMListeners = (win, delta) => {
516 listenerCount += delta;
517 if (listenerCount === 1) {
518 win.addEventListener(HashChangeEvent$1, handleHashChange);
519 }
520 else if (listenerCount === 0) {
521 win.removeEventListener(HashChangeEvent$1, handleHashChange);
522 }
523 };
524 const block = (prompt = '') => {
525 const unblock = transitionManager.setPrompt(prompt);
526 if (!isBlocked) {
527 checkDOMListeners(win, 1);
528 isBlocked = true;
529 }
530 return () => {
531 if (isBlocked) {
532 isBlocked = false;
533 checkDOMListeners(win, -1);
534 }
535 return unblock();
536 };
537 };
538 const listen = (listener) => {
539 const unlisten = transitionManager.appendListener(listener);
540 checkDOMListeners(win, 1);
541 return () => {
542 checkDOMListeners(win, -1);
543 unlisten();
544 };
545 };
546 const history = {
547 length: globalHistory.length,
548 action: 'POP',
549 location: initialLocation,
550 createHref,
551 push,
552 replace,
553 go,
554 goBack,
555 goForward,
556 block,
557 listen,
558 win: win
559 };
560 return history;
561};
562
563const getLocation = (location, root) => {
564 // Remove the root URL if found at beginning of string
565 const pathname = location.pathname.indexOf(root) == 0 ?
566 '/' + location.pathname.slice(root.length) :
567 location.pathname;
568 return Object.assign({}, location, { pathname });
569};
570const HISTORIES = {
571 'browser': createBrowserHistory,
572 'hash': createHashHistory
573};
574/**
575 * @name Router
576 * @module ionic
577 * @description
578 */
579class Router {
580 constructor(hostRef) {
581 registerInstance(this, hostRef);
582 this.root = '/';
583 this.historyType = 'browser';
584 // A suffix to append to the page title whenever
585 // it's updated through RouteTitle
586 this.titleSuffix = '';
587 this.routeViewsUpdated = (options = {}) => {
588 if (this.history && options.scrollToId && this.historyType === 'browser') {
589 const elm = this.history.win.document.getElementById(options.scrollToId);
590 if (elm) {
591 return elm.scrollIntoView();
592 }
593 }
594 this.scrollTo(options.scrollTopOffset || this.scrollTopOffset);
595 };
596 this.isServer = getContext(this, "isServer");
597 this.queue = getContext(this, "queue");
598 }
599 componentWillLoad() {
600 this.history = HISTORIES[this.historyType](this.el.ownerDocument.defaultView);
601 this.history.listen((location) => {
602 location = getLocation(location, this.root);
603 this.location = location;
604 });
605 this.location = getLocation(this.history.location, this.root);
606 }
607 scrollTo(scrollToLocation) {
608 const history = this.history;
609 if (scrollToLocation == null || this.isServer || !history) {
610 return;
611 }
612 if (history.action === 'POP' && Array.isArray(history.location.scrollPosition)) {
613 return this.queue.write(() => {
614 if (history && history.location && Array.isArray(history.location.scrollPosition)) {
615 history.win.scrollTo(history.location.scrollPosition[0], history.location.scrollPosition[1]);
616 }
617 });
618 }
619 // okay, the frame has passed. Go ahead and render now
620 return this.queue.write(() => {
621 history.win.scrollTo(0, scrollToLocation);
622 });
623 }
624 render() {
625 if (!this.location || !this.history) {
626 return;
627 }
628 const state = {
629 historyType: this.historyType,
630 location: this.location,
631 titleSuffix: this.titleSuffix,
632 root: this.root,
633 history: this.history,
634 routeViewsUpdated: this.routeViewsUpdated
635 };
636 return (h(ActiveRouter.Provider, { state: state }, h("slot", null)));
637 }
638 get el() { return getElement(this); }
639}
640
641export { Router as stencil_router };