@stencil/router
Version:
646 lines (636 loc) • 25.4 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
const __chunk_1 = require('./stencilrouter-a3d77a87.js');
const __chunk_2 = require('./chunk-94c92d88.js');
const __chunk_4 = require('./chunk-e6311a56.js');
const __chunk_5 = require('./chunk-b9bd6b52.js');
const warning = (value, ...args) => {
if (!value) {
console.warn(...args);
}
};
// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
const createTransitionManager = () => {
let prompt;
let listeners = [];
const setPrompt = (nextPrompt) => {
warning(prompt == null, 'A history supports only one prompt at a time');
prompt = nextPrompt;
return () => {
if (prompt === nextPrompt) {
prompt = null;
}
};
};
const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
// TODO: If another transition starts while we're still confirming
// the previous one, we may end up in a weird state. Figure out the
// best way to handle this.
if (prompt != null) {
const result = typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback);
}
else {
warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message');
callback(true);
}
}
else {
// Return false from a transition hook to cancel the transition.
callback(result !== false);
}
}
else {
callback(true);
}
};
const appendListener = (fn) => {
let isActive = true;
const listener = (...args) => {
if (isActive) {
fn(...args);
}
};
listeners.push(listener);
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
};
};
const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args));
};
return {
setPrompt,
confirmTransitionTo,
appendListener,
notifyListeners
};
};
const createScrollHistory = (win, applicationScrollKey = 'scrollPositions') => {
let scrollPositions = new Map();
const set = (key, value) => {
scrollPositions.set(key, value);
if (__chunk_5.storageAvailable(win, 'sessionStorage')) {
const arrayData = [];
scrollPositions.forEach((value, key) => {
arrayData.push([key, value]);
});
win.sessionStorage.setItem('scrollPositions', JSON.stringify(arrayData));
}
};
const get = (key) => {
return scrollPositions.get(key);
};
const has = (key) => {
return scrollPositions.has(key);
};
const capture = (key) => {
set(key, [win.scrollX, win.scrollY]);
};
if (__chunk_5.storageAvailable(win, 'sessionStorage')) {
const scrollData = win.sessionStorage.getItem(applicationScrollKey);
scrollPositions = scrollData ?
new Map(JSON.parse(scrollData)) :
scrollPositions;
}
if ('scrollRestoration' in win.history) {
history.scrollRestoration = 'manual';
}
return {
set,
get,
has,
capture
};
};
// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
const PopStateEvent = 'popstate';
const HashChangeEvent = 'hashchange';
/**
* Creates a history object that uses the HTML5 history API including
* pushState, replaceState, and the popstate event.
*/
const createBrowserHistory = (win, props = {}) => {
let forceNextPop = false;
const globalHistory = win.history;
const globalLocation = win.location;
const globalNavigator = win.navigator;
const canUseHistory = __chunk_5.supportsHistory(win);
const needsHashChangeListener = !__chunk_5.supportsPopStateOnHashChange(globalNavigator);
const scrollHistory = createScrollHistory(win);
const forceRefresh = (props.forceRefresh != null) ? props.forceRefresh : false;
const getUserConfirmation = (props.getUserConfirmation != null) ? props.getUserConfirmation : __chunk_5.getConfirmation;
const keyLength = (props.keyLength != null) ? props.keyLength : 6;
const basename = props.basename ? __chunk_4.stripTrailingSlash(__chunk_4.addLeadingSlash(props.basename)) : '';
const getHistoryState = () => {
try {
return win.history.state || {};
}
catch (e) {
// IE 11 sometimes throws when accessing window.history.state
// See https://github.com/ReactTraining/history/pull/289
return {};
}
};
const getDOMLocation = (historyState) => {
historyState = historyState || {};
const { key, state } = historyState;
const { pathname, search, hash } = globalLocation;
let path = pathname + search + hash;
warning((!basename || __chunk_4.hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' +
'with the basename. Expected path "' + path + '" to begin with "' + basename + '".');
if (basename) {
path = __chunk_4.stripBasename(path, basename);
}
return __chunk_4.createLocation(path, state, key || __chunk_4.createKey(keyLength));
};
const transitionManager = createTransitionManager();
const setState = (nextState) => {
// Capture location for the view before changing history.
scrollHistory.capture(history.location.key);
Object.assign(history, nextState);
// Set scroll position based on its previous storage value
history.location.scrollPosition = scrollHistory.get(history.location.key);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
};
const handlePopState = (event) => {
// Ignore extraneous popstate events in WebKit.
if (!__chunk_5.isExtraneousPopstateEvent(globalNavigator, event)) {
handlePop(getDOMLocation(event.state));
}
};
const handleHashChange = () => {
handlePop(getDOMLocation(getHistoryState()));
};
const handlePop = (location) => {
if (forceNextPop) {
forceNextPop = false;
setState();
}
else {
const action = 'POP';
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location });
}
else {
revertPop(location);
}
});
}
};
const revertPop = (fromLocation) => {
const toLocation = history.location;
// TODO: We could probably make this more reliable by
// keeping a list of keys we've seen in sessionStorage.
// Instead, we just default to 0 for keys we don't know.
let toIndex = allKeys.indexOf(toLocation.key);
let fromIndex = allKeys.indexOf(fromLocation.key);
if (toIndex === -1) {
toIndex = 0;
}
if (fromIndex === -1) {
fromIndex = 0;
}
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
};
const initialLocation = getDOMLocation(getHistoryState());
let allKeys = [initialLocation.key];
let listenerCount = 0;
let isBlocked = false;
// Public interface
const createHref = (location) => {
return basename + __chunk_4.createPath(location);
};
const push = (path, state) => {
warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to push when the 1st ' +
'argument is a location-like object that already has state; it is ignored');
const action = 'PUSH';
const location = __chunk_4.createLocation(path, state, __chunk_4.createKey(keyLength), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (!ok) {
return;
}
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.pushState({ key, state }, '', href);
if (forceRefresh) {
globalLocation.href = href;
}
else {
const prevIndex = allKeys.indexOf(history.location.key);
const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action, location });
}
}
else {
warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
globalLocation.href = href;
}
});
};
const replace = (path, state) => {
warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to replace when the 1st ' +
'argument is a location-like object that already has state; it is ignored');
const action = 'REPLACE';
const location = __chunk_4.createLocation(path, state, __chunk_4.createKey(keyLength), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (!ok) {
return;
}
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.replaceState({ key, state }, '', href);
if (forceRefresh) {
globalLocation.replace(href);
}
else {
const prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) {
allKeys[prevIndex] = location.key;
}
setState({ action, location });
}
}
else {
warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history');
globalLocation.replace(href);
}
});
};
const go = (n) => {
globalHistory.go(n);
};
const goBack = () => go(-1);
const goForward = () => go(1);
const checkDOMListeners = (delta) => {
listenerCount += delta;
if (listenerCount === 1) {
win.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) {
win.addEventListener(HashChangeEvent, handleHashChange);
}
}
else if (listenerCount === 0) {
win.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) {
win.removeEventListener(HashChangeEvent, handleHashChange);
}
}
};
const block = (prompt = '') => {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
return unblock();
};
};
const listen = (listener) => {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
};
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen,
win: win
};
return history;
};
// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript
const HashChangeEvent$1 = 'hashchange';
const HashPathCoders = {
hashbang: {
encodePath: (path) => path.charAt(0) === '!' ? path : '!/' + __chunk_4.stripLeadingSlash(path),
decodePath: (path) => path.charAt(0) === '!' ? path.substr(1) : path
},
noslash: {
encodePath: __chunk_4.stripLeadingSlash,
decodePath: __chunk_4.addLeadingSlash
},
slash: {
encodePath: __chunk_4.addLeadingSlash,
decodePath: __chunk_4.addLeadingSlash
}
};
const createHashHistory = (win, props = {}) => {
let forceNextPop = false;
let ignorePath = null;
let listenerCount = 0;
let isBlocked = false;
const globalLocation = win.location;
const globalHistory = win.history;
const canGoWithoutReload = __chunk_5.supportsGoWithoutReloadUsingHash(win.navigator);
const keyLength = (props.keyLength != null) ? props.keyLength : 6;
const { getUserConfirmation = __chunk_5.getConfirmation, hashType = 'slash' } = props;
const basename = props.basename ? __chunk_4.stripTrailingSlash(__chunk_4.addLeadingSlash(props.basename)) : '';
const { encodePath, decodePath } = HashPathCoders[hashType];
const getHashPath = () => {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
const href = globalLocation.href;
const hashIndex = href.indexOf('#');
return hashIndex === -1 ? '' : href.substring(hashIndex + 1);
};
const pushHashPath = (path) => (globalLocation.hash = path);
const replaceHashPath = (path) => {
const hashIndex = globalLocation.href.indexOf('#');
globalLocation.replace(globalLocation.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path);
};
const getDOMLocation = () => {
let path = decodePath(getHashPath());
warning((!basename || __chunk_4.hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' +
'with the basename. Expected path "' + path + '" to begin with "' + basename + '".');
if (basename) {
path = __chunk_4.stripBasename(path, basename);
}
return __chunk_4.createLocation(path, undefined, __chunk_4.createKey(keyLength));
};
const transitionManager = createTransitionManager();
const setState = (nextState) => {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
};
const handleHashChange = () => {
const path = getHashPath();
const encodedPath = encodePath(path);
if (path !== encodedPath) {
// Ensure we always have a properly-encoded hash.
replaceHashPath(encodedPath);
}
else {
const location = getDOMLocation();
const prevLocation = history.location;
if (!forceNextPop && __chunk_4.locationsAreEqual(prevLocation, location)) {
return; // A hashchange doesn't always == location change.
}
if (ignorePath === __chunk_4.createPath(location)) {
return; // Ignore this change; we already setState in push/replace.
}
ignorePath = null;
handlePop(location);
}
};
const handlePop = (location) => {
if (forceNextPop) {
forceNextPop = false;
setState();
}
else {
const action = 'POP';
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location });
}
else {
revertPop(location);
}
});
}
};
const revertPop = (fromLocation) => {
const toLocation = history.location;
// TODO: We could probably make this more reliable by
// keeping a list of paths we've seen in sessionStorage.
// Instead, we just default to 0 for paths we don't know.
let toIndex = allPaths.lastIndexOf(__chunk_4.createPath(toLocation));
let fromIndex = allPaths.lastIndexOf(__chunk_4.createPath(fromLocation));
if (toIndex === -1) {
toIndex = 0;
}
if (fromIndex === -1) {
fromIndex = 0;
}
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
};
// Ensure the hash is encoded properly before doing anything else.
const path = getHashPath();
const encodedPath = encodePath(path);
if (path !== encodedPath) {
replaceHashPath(encodedPath);
}
const initialLocation = getDOMLocation();
let allPaths = [__chunk_4.createPath(initialLocation)];
// Public interface
const createHref = (location) => ('#' + encodePath(basename + __chunk_4.createPath(location)));
const push = (path, state) => {
warning(state === undefined, 'Hash history cannot push state; it is ignored');
const action = 'PUSH';
const location = __chunk_4.createLocation(path, undefined, __chunk_4.createKey(keyLength), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (!ok) {
return;
}
const path = __chunk_4.createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
// We cannot tell if a hashchange was caused by a PUSH, so we'd
// rather setState here and ignore the hashchange. The caveat here
// is that other hash histories in the page will consider it a POP.
ignorePath = path;
pushHashPath(encodedPath);
const prevIndex = allPaths.lastIndexOf(__chunk_4.createPath(history.location));
const nextPaths = allPaths.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
nextPaths.push(path);
allPaths = nextPaths;
setState({ action, location });
}
else {
warning(false, 'Hash history cannot PUSH the same path; a new entry will not be added to the history stack');
setState();
}
});
};
const replace = (path, state) => {
warning(state === undefined, 'Hash history cannot replace state; it is ignored');
const action = 'REPLACE';
const location = __chunk_4.createLocation(path, undefined, __chunk_4.createKey(keyLength), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (!ok) {
return;
}
const path = __chunk_4.createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
// We cannot tell if a hashchange was caused by a REPLACE, so we'd
// rather setState here and ignore the hashchange. The caveat here
// is that other hash histories in the page will consider it a POP.
ignorePath = path;
replaceHashPath(encodedPath);
}
const prevIndex = allPaths.indexOf(__chunk_4.createPath(history.location));
if (prevIndex !== -1) {
allPaths[prevIndex] = path;
}
setState({ action, location });
});
};
const go = (n) => {
warning(canGoWithoutReload, 'Hash history go(n) causes a full page reload in this browser');
globalHistory.go(n);
};
const goBack = () => go(-1);
const goForward = () => go(1);
const checkDOMListeners = (win, delta) => {
listenerCount += delta;
if (listenerCount === 1) {
win.addEventListener(HashChangeEvent$1, handleHashChange);
}
else if (listenerCount === 0) {
win.removeEventListener(HashChangeEvent$1, handleHashChange);
}
};
const block = (prompt = '') => {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(win, 1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(win, -1);
}
return unblock();
};
};
const listen = (listener) => {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(win, 1);
return () => {
checkDOMListeners(win, -1);
unlisten();
};
};
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen,
win: win
};
return history;
};
const getLocation = (location, root) => {
// Remove the root URL if found at beginning of string
const pathname = location.pathname.indexOf(root) == 0 ?
'/' + location.pathname.slice(root.length) :
location.pathname;
return Object.assign({}, location, { pathname });
};
const HISTORIES = {
'browser': createBrowserHistory,
'hash': createHashHistory
};
/**
* @name Router
* @module ionic
* @description
*/
class Router {
constructor(hostRef) {
__chunk_1.registerInstance(this, hostRef);
this.root = '/';
this.historyType = 'browser';
// A suffix to append to the page title whenever
// it's updated through RouteTitle
this.titleSuffix = '';
this.routeViewsUpdated = (options = {}) => {
if (this.history && options.scrollToId && this.historyType === 'browser') {
const elm = this.history.win.document.getElementById(options.scrollToId);
if (elm) {
return elm.scrollIntoView();
}
}
this.scrollTo(options.scrollTopOffset || this.scrollTopOffset);
};
this.isServer = __chunk_1.getContext(this, "isServer");
this.queue = __chunk_1.getContext(this, "queue");
}
componentWillLoad() {
this.history = HISTORIES[this.historyType](this.el.ownerDocument.defaultView);
this.history.listen((location) => {
location = getLocation(location, this.root);
this.location = location;
});
this.location = getLocation(this.history.location, this.root);
}
scrollTo(scrollToLocation) {
const history = this.history;
if (scrollToLocation == null || this.isServer || !history) {
return;
}
if (history.action === 'POP' && Array.isArray(history.location.scrollPosition)) {
return this.queue.write(() => {
if (history && history.location && Array.isArray(history.location.scrollPosition)) {
history.win.scrollTo(history.location.scrollPosition[0], history.location.scrollPosition[1]);
}
});
}
// okay, the frame has passed. Go ahead and render now
return this.queue.write(() => {
history.win.scrollTo(0, scrollToLocation);
});
}
render() {
if (!this.location || !this.history) {
return;
}
const state = {
historyType: this.historyType,
location: this.location,
titleSuffix: this.titleSuffix,
root: this.root,
history: this.history,
routeViewsUpdated: this.routeViewsUpdated
};
return (__chunk_1.h(__chunk_2.ActiveRouter.Provider, { state: state }, __chunk_1.h("slot", null)));
}
get el() { return __chunk_1.getElement(this); }
}
exports.stencil_router = Router;
;