123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- import { DIRECTION_BACK, convertToViews, isNav, isTab, isTabs } from './nav-util';
- import { isArray, isPresent } from '../util/util';
- import { formatUrlPart } from './url-serializer';
- import { ViewController } from './view-controller';
- /**
- * @hidden
- */
- export class DeepLinker {
- constructor(_app, _serializer, _location, _moduleLoader, _baseCfr) {
- this._app = _app;
- this._serializer = _serializer;
- this._location = _location;
- this._moduleLoader = _moduleLoader;
- this._baseCfr = _baseCfr;
- /** @internal */
- this._history = [];
- }
- /**
- * @internal
- */
- init() {
- // scenario 1: Initial load of all navs from the initial browser URL
- const browserUrl = normalizeUrl(this._location.path());
- (void 0) /* console.debug */;
- // remember this URL in our internal history stack
- this._historyPush(browserUrl);
- // listen for browser URL changes
- this._location.subscribe((locationChg) => {
- this._urlChange(normalizeUrl(locationChg.url));
- });
- }
- /**
- * The browser's location has been updated somehow.
- * @internal
- */
- _urlChange(browserUrl) {
- // do nothing if this url is the same as the current one
- if (!this._isCurrentUrl(browserUrl)) {
- let isGoingBack = true;
- if (this._isBackUrl(browserUrl)) {
- // scenario 2: user clicked the browser back button
- // scenario 4: user changed the browser URL to what was the back url was
- // scenario 5: user clicked a link href that was the back url
- (void 0) /* console.debug */;
- this._historyPop();
- }
- else {
- // scenario 3: user click forward button
- // scenario 4: user changed browser URL that wasn't the back url
- // scenario 5: user clicked a link href that wasn't the back url
- isGoingBack = false;
- (void 0) /* console.debug */;
- this._historyPush(browserUrl);
- }
- // get the app's root nav container
- const activeNavContainers = this._app.getActiveNavContainers();
- if (activeNavContainers && activeNavContainers.length) {
- if (browserUrl === '/') {
- // a url change to the index url
- if (isPresent(this._indexAliasUrl)) {
- // we already know the indexAliasUrl
- // update the url to use the know alias
- browserUrl = this._indexAliasUrl;
- }
- else {
- // the url change is to the root but we don't
- // already know the url used. So let's just
- // reset the root nav to its root page
- activeNavContainers.forEach((navContainer) => {
- navContainer.goToRoot({
- updateUrl: false,
- isNavRoot: true
- });
- });
- return;
- }
- }
- // normal url
- const segments = this.getCurrentSegments(browserUrl);
- segments
- .map(segment => {
- // find the matching nav container
- for (const navContainer of activeNavContainers) {
- const nav = getNavFromTree(navContainer, segment.navId);
- if (nav) {
- return {
- segment: segment,
- navContainer: nav
- };
- }
- }
- })
- .filter(pair => !!pair)
- .forEach(pair => {
- this._loadViewForSegment(pair.navContainer, pair.segment, () => { });
- });
- }
- }
- }
- getCurrentSegments(browserUrl) {
- if (!browserUrl) {
- browserUrl = normalizeUrl(this._location.path());
- }
- return this._serializer.parse(browserUrl);
- }
- /**
- * Update the deep linker using the NavController's current active view.
- * @internal
- */
- navChange(direction) {
- if (direction) {
- const activeNavContainers = this._app.getActiveNavContainers();
- // the only time you'll ever get a TABS here is when loading directly from a URL
- // this method will be called again when the TAB is loaded
- // so just don't worry about the TABS for now
- // if you encounter a TABS, just return
- for (const activeNavContainer of activeNavContainers) {
- if (isTabs(activeNavContainer) || activeNavContainer.isTransitioning()) {
- return;
- }
- }
- // okay, get the root navs and build the segments up
- let segments = [];
- const navContainers = this._app.getRootNavs();
- for (const navContainer of navContainers) {
- const segmentsForNav = this.getSegmentsFromNav(navContainer);
- segments = segments.concat(segmentsForNav);
- }
- segments = segments.filter(segment => !!segment);
- if (segments.length) {
- const browserUrl = this._serializer.serialize(segments);
- this._updateLocation(browserUrl, direction);
- }
- }
- }
- getSegmentsFromNav(nav) {
- let segments = [];
- if (isNav(nav)) {
- segments.push(this.getSegmentFromNav(nav));
- }
- else if (isTab(nav)) {
- segments.push(this.getSegmentFromTab(nav));
- }
- nav.getActiveChildNavs().forEach(child => {
- segments = segments.concat(this.getSegmentsFromNav(child));
- });
- return segments;
- }
- getSegmentFromNav(nav, component, data) {
- if (!component) {
- const viewController = nav.getActive(true);
- if (viewController) {
- component = viewController.component;
- data = viewController.data;
- }
- }
- return this._serializer.serializeComponent(nav, component, data);
- }
- getSegmentFromTab(navContainer, component, data) {
- if (navContainer && navContainer.parent) {
- const tabsNavContainer = navContainer.parent;
- const activeChildNavs = tabsNavContainer.getActiveChildNavs();
- if (activeChildNavs && activeChildNavs.length) {
- const activeChildNav = activeChildNavs[0];
- const viewController = activeChildNav.getActive(true);
- if (viewController) {
- component = viewController.component;
- data = viewController.data;
- }
- return this._serializer.serializeComponent(tabsNavContainer, component, data);
- }
- }
- }
- /**
- * @internal
- */
- _updateLocation(browserUrl, direction) {
- if (this._indexAliasUrl === browserUrl) {
- browserUrl = '/';
- }
- if (direction === DIRECTION_BACK && this._isBackUrl(browserUrl)) {
- // this URL is exactly the same as the back URL
- // it's safe to use the browser's location.back()
- (void 0) /* console.debug */;
- this._historyPop();
- this._location.back();
- }
- else if (!this._isCurrentUrl(browserUrl)) {
- // probably navigating forward
- (void 0) /* console.debug */;
- this._historyPush(browserUrl);
- this._location.go(browserUrl);
- }
- }
- getComponentFromName(componentName) {
- const link = this._serializer.getLinkFromName(componentName);
- if (link) {
- // cool, we found the right link for this component name
- return this.getNavLinkComponent(link);
- }
- // umm, idk
- return Promise.reject(`invalid link: ${componentName}`);
- }
- getNavLinkComponent(link) {
- if (link.component) {
- // sweet, we're already got a component loaded for this link
- return Promise.resolve(link.component);
- }
- if (link.loadChildren) {
- // awesome, looks like we'll lazy load this component
- // using loadChildren as the URL to request
- return this._moduleLoader.load(link.loadChildren).then((response) => {
- link.component = response.component;
- return response.component;
- });
- }
- return Promise.reject(`invalid link component: ${link.name}`);
- }
- /**
- * @internal
- */
- resolveComponent(component) {
- let cfr = this._moduleLoader.getComponentFactoryResolver(component);
- if (!cfr) {
- cfr = this._baseCfr;
- }
- return cfr.resolveComponentFactory(component);
- }
- /**
- * @internal
- */
- createUrl(navContainer, nameOrComponent, _data, prepareExternalUrl = true) {
- // create a segment out of just the passed in name
- const segment = this._serializer.createSegmentFromName(navContainer, nameOrComponent);
- const allSegments = this.getCurrentSegments();
- if (segment) {
- for (let i = 0; i < allSegments.length; i++) {
- if (allSegments[i].navId === navContainer.name || allSegments[i].navId === navContainer.id) {
- allSegments[i] = segment;
- const url = this._serializer.serialize(allSegments);
- return prepareExternalUrl ? this._location.prepareExternalUrl(url) : url;
- }
- }
- }
- return '';
- }
- /**
- * Each NavController will call this method when it initializes for
- * the first time. This allows each NavController to figure out
- * where it lives in the path and load up the correct component.
- * @internal
- */
- getSegmentByNavIdOrName(navId, name) {
- const browserUrl = normalizeUrl(this._location.path());
- const segments = this._serializer.parse(browserUrl);
- for (const segment of segments) {
- if (segment.navId === navId || segment.navId === name) {
- return segment;
- }
- }
- return null;
- }
- /**
- * @internal
- */
- initViews(segment) {
- const link = this._serializer.getLinkFromName(segment.name);
- return this.getNavLinkComponent(link).then((component) => {
- segment.component = component;
- const view = new ViewController(component, segment.data);
- view.id = segment.id;
- if (isArray(segment.defaultHistory)) {
- return convertToViews(this, segment.defaultHistory).then(views => {
- views.push(view);
- return views;
- });
- }
- return [view];
- });
- }
- /**
- * @internal
- */
- _isBackUrl(browserUrl) {
- return (browserUrl === this._history[this._history.length - 2]);
- }
- /**
- * @internal
- */
- _isCurrentUrl(browserUrl) {
- return (browserUrl === this._history[this._history.length - 1]);
- }
- /**
- * @internal
- */
- _historyPush(browserUrl) {
- if (!this._isCurrentUrl(browserUrl)) {
- this._history.push(browserUrl);
- if (this._history.length > 30) {
- this._history.shift();
- }
- }
- }
- /**
- * @internal
- */
- _historyPop() {
- this._history.pop();
- if (!this._history.length) {
- this._historyPush(this._location.path());
- }
- }
- /**
- * @internal
- */
- _getTabSelector(tab) {
- if (isPresent(tab.tabUrlPath)) {
- return tab.tabUrlPath;
- }
- if (isPresent(tab.tabTitle)) {
- return formatUrlPart(tab.tabTitle);
- }
- return `tab-${tab.index}`;
- }
- /**
- * Using the known Path of Segments, walk down all descendents
- * from the root NavController and load each NavController according
- * to each Segment. This is usually called after a browser URL and
- * Path changes and needs to update all NavControllers to match
- * the new browser URL. Because the URL is already known, it will
- * not update the browser's URL when transitions have completed.
- *
- * @internal
- */
- _loadViewForSegment(navContainer, segment, done) {
- if (!segment) {
- return done(false, false);
- }
- if (isTabs(navContainer) || (isTab(navContainer) && navContainer.parent)) {
- const tabs = (isTabs(navContainer) ? navContainer : navContainer.parent);
- const selectedIndex = tabs._getSelectedTabIndex(segment.secondaryId);
- const tab = tabs.getByIndex(selectedIndex);
- tab._segment = segment;
- tabs.select(tab, {
- updateUrl: false,
- animate: false
- }, true);
- return done(false, false);
- }
- const navController = navContainer;
- const numViews = navController.length() - 1;
- // walk backwards to see if the exact view we want to show here
- // is already in the stack that we can just pop back to
- for (let i = numViews; i >= 0; i--) {
- const viewController = navController.getByIndex(i);
- if (viewController && (viewController.id === segment.id || viewController.id === segment.name)) {
- // hooray! we've already got a view loaded in the stack
- // matching the view they wanted to show
- if (i === numViews) {
- // this is the last view in the stack and it's the same
- // as the segment so there's no change needed
- return done(false, false);
- }
- else {
- // it's not the exact view as the end
- // let's have this nav go back to this exact view
- return navController.popTo(viewController, {
- animate: false,
- updateUrl: false,
- }, done);
- }
- }
- }
- // ok, so we don't know about a view that they're navigating to
- // so we might as well just call setRoot and make tthe view the first view
- // this seems like the least bad option
- return navController.setRoot(segment.component || segment.name, segment.data, {
- id: segment.id, animate: false, updateUrl: false
- }, done);
- }
- }
- export function setupDeepLinker(app, serializer, location, moduleLoader, cfr) {
- const deepLinker = new DeepLinker(app, serializer, location, moduleLoader, cfr);
- deepLinker.init();
- return deepLinker;
- }
- export function normalizeUrl(browserUrl) {
- browserUrl = browserUrl.trim();
- if (browserUrl.charAt(0) !== '/') {
- // ensure first char is a /
- browserUrl = '/' + browserUrl;
- }
- if (browserUrl.length > 1 && browserUrl.charAt(browserUrl.length - 1) === '/') {
- // ensure last char is not a /
- browserUrl = browserUrl.substr(0, browserUrl.length - 1);
- }
- return browserUrl;
- }
- export function getNavFromTree(nav, id) {
- while (nav) {
- if (nav.id === id || nav.name === id) {
- return nav;
- }
- nav = nav.parent;
- }
- return null;
- }
- //# sourceMappingURL=deep-linker.js.map
|