@pshurygin/ngx-quicklink
Version:
Quicklink for Angular
292 lines (285 loc) • 9.82 kB
JavaScript
import { __decorate, __metadata, __param } from 'tslib';
import { Injectable, InjectionToken, NgZone, Input, Directive, Inject, Optional, ElementRef, NgModule } from '@angular/core';
import { Router, PRIMARY_OUTLET, RouterPreloader, RouterLink, RouterLinkWithHref } from '@angular/router';
import { EMPTY } from 'rxjs';
// Using a global registry so we can keep it populated across lazy-loaded
// modules with different parent injectors which create instance of the registry.
const globalRegistry = [];
let PrefetchRegistry = class PrefetchRegistry {
constructor(router) {
this.router = router;
this.trees = globalRegistry;
}
add(tree) {
this.trees.push(tree);
}
remove(tree) {
this.trees.splice(this.trees.indexOf(tree), 1);
}
shouldPrefetch(url) {
const tree = this.router.parseUrl(url);
return this.trees.some(containsTree.bind(null, tree));
}
};
PrefetchRegistry = __decorate([
Injectable(),
__metadata("design:paramtypes", [Router])
], PrefetchRegistry);
function containsQueryParams(container, containee) {
// TODO: This does not handle array params correctly.
return (Object.keys(containee).length <= Object.keys(container).length &&
Object.keys(containee).every(key => containee[key] === container[key]));
}
function containsTree(containee, container) {
return (containsQueryParams(container.queryParams, containee.queryParams) &&
containsSegmentGroup(container.root, containee.root, containee.root.segments));
}
function containsSegmentGroup(container, containee, containeePaths) {
if (container.segments.length > containeePaths.length) {
const current = container.segments.slice(0, containeePaths.length);
if (!equalPath(current, containeePaths))
return false;
if (containee.hasChildren())
return false;
return true;
}
else if (container.segments.length === containeePaths.length) {
if (!equalPath(container.segments, containeePaths))
return false;
if (!containee.hasChildren())
return true;
for (const c in containee.children) {
if (!container.children[c])
break;
if (containsSegmentGroup(container.children[c], containee.children[c], containee.children[c].segments))
return true;
}
return false;
}
else {
const current = containeePaths.slice(0, container.segments.length);
const next = containeePaths.slice(container.segments.length);
if (!equalPath(container.segments, current))
return false;
if (!container.children[PRIMARY_OUTLET])
return false;
return containsSegmentGroup(container.children[PRIMARY_OUTLET], containee, next);
}
}
function equalPath(as, bs) {
if (as.length !== bs.length)
return false;
return as.every((a, i) => a.path === bs[i].path || a.path.startsWith(':') || bs[i].path.startsWith(':'));
}
const ɵ0 = function (cb) {
const start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
}, ɵ1 = () => { };
const requestIdleCallback = typeof window !== 'undefined'
? window.requestIdleCallback || ɵ0
: ɵ1;
const observerSupported = () => typeof window !== 'undefined' ? !!window.IntersectionObserver : false;
const LinkHandler = new InjectionToken('LinkHandler');
let ObservableLinkHandler = class ObservableLinkHandler {
constructor(loader, queue, ngZone) {
this.loader = loader;
this.queue = queue;
this.ngZone = ngZone;
this.elementLink = new Map();
this.observer = observerSupported()
? new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target;
const routerLink = this.elementLink.get(link);
if (!routerLink || !routerLink.urlTree)
return;
this.queue.add(routerLink.urlTree);
this.observer.unobserve(link);
requestIdleCallback(() => {
this.loader.preload().subscribe(() => void 0);
this.queue.remove(routerLink.urlTree);
});
}
});
})
: null;
}
register(el) {
this.elementLink.set(el.element, el);
this.ngZone.runOutsideAngular(() => {
this.observer.observe(el.element);
});
}
// First call to unregister will not hit this.
unregister(el) {
if (this.elementLink.has(el.element)) {
this.observer.unobserve(el.element);
this.elementLink.delete(el.element);
}
}
supported() {
return observerSupported();
}
};
ObservableLinkHandler = __decorate([
Injectable(),
__metadata("design:paramtypes", [RouterPreloader, PrefetchRegistry, NgZone])
], ObservableLinkHandler);
let PreloadLinkHandler = class PreloadLinkHandler {
constructor(loader, queue) {
this.loader = loader;
this.queue = queue;
}
register(el) {
this.queue.add(el.urlTree);
requestIdleCallback(() => this.loader.preload().subscribe(() => void 0));
}
unregister(_) { }
supported() {
return true;
}
};
PreloadLinkHandler = __decorate([
Injectable(),
__metadata("design:paramtypes", [RouterPreloader, PrefetchRegistry])
], PreloadLinkHandler);
let LinkDirective = class LinkDirective {
constructor(linkHandlers, el, link, linkWithHref) {
this.linkHandlers = linkHandlers;
this.el = el;
this.linkHandler = this.linkHandlers.filter(h => h.supported()).shift();
this.rl = link || linkWithHref;
}
ngOnChanges(c) {
if (c.routerLink) {
this.linkHandler.unregister(this);
this.linkHandler.register(this);
}
}
ngOnDestroy() {
this.linkHandler.unregister(this);
}
get element() {
return this.el.nativeElement;
}
get urlTree() {
return this.rl.urlTree;
}
};
__decorate([
Input(),
__metadata("design:type", Object)
], LinkDirective.prototype, "routerLink", void 0);
LinkDirective = __decorate([
Directive({
selector: '[routerLink]'
}),
__param(0, Inject(LinkHandler)),
__param(2, Optional()),
__param(3, Optional()),
__metadata("design:paramtypes", [Array, ElementRef,
RouterLink,
RouterLinkWithHref])
], LinkDirective);
let QuicklinkStrategy = class QuicklinkStrategy {
constructor(queue, router) {
this.queue = queue;
this.router = router;
this.loading = new Set();
}
preload(route, load) {
if (this.loading.has(route)) {
// Don't preload the same route twice
return EMPTY;
}
const conn = typeof window !== 'undefined' ? navigator.connection : undefined;
if (conn) {
// Don't preload if the user is on 2G. or if Save-Data is enabled..
if ((conn.effectiveType || '').includes('2g') || conn.saveData)
return EMPTY;
}
// Prevent from preloading
if (route.data && route.data.preload === false) {
return EMPTY;
}
const fullPath = findPath(this.router.config, route);
if (this.queue.shouldPrefetch(fullPath)) {
this.loading.add(route);
return load();
}
return EMPTY;
}
};
QuicklinkStrategy = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrefetchRegistry, Router])
], QuicklinkStrategy);
const findPath = (config, route) => {
config = config.slice();
const parent = new Map();
const visited = new Set();
while (config.length) {
const el = config.shift();
visited.add(el);
if (el === route)
break;
let children = el.children || [];
const current = el._loadedConfig;
if (current && current.routes) {
children = children.concat(current.routes);
}
children.forEach((r) => {
if (visited.has(r))
return;
parent.set(r, el);
config.push(r);
});
}
let path = '';
let current = route;
while (current) {
if (isPrimaryRoute(current)) {
path = `/${current.path}${path}`;
}
else {
path = `/(${current.outlet}:${current.path}${path})`;
}
current = parent.get(current);
}
return path;
};
function isPrimaryRoute(route) {
return route.outlet === PRIMARY_OUTLET || !route.outlet;
}
let QuicklinkModule = class QuicklinkModule {
};
QuicklinkModule = __decorate([
NgModule({
declarations: [LinkDirective],
providers: [
{
provide: LinkHandler,
useClass: ObservableLinkHandler,
multi: true
},
{
provide: LinkHandler,
useClass: PreloadLinkHandler,
multi: true
},
PrefetchRegistry,
QuicklinkStrategy
],
exports: [LinkDirective]
})
], QuicklinkModule);
export { QuicklinkModule, QuicklinkStrategy, LinkHandler as ɵa, ObservableLinkHandler as ɵb, PreloadLinkHandler as ɵc, PrefetchRegistry as ɵe, LinkDirective as ɵɵLinkDirective };
//# sourceMappingURL=pshurygin-ngx-quicklink.js.map