ng-lazyload-image-angular-universal
Version:
This is an extension from ng-lazyload-image, made available for Angular Universal.
773 lines (754 loc) • 21.3 kB
JavaScript
import { isPlatformServer } from '@angular/common';
import { Directive, ElementRef, EventEmitter, Inject, Input, NgZone, Optional, Output, PLATFORM_ID, NgModule } from '@angular/core';
import { Observable, empty, of, ReplaySubject, Subject } from 'rxjs';
import { sampleTime, share, startWith, catchError, filter, map, mergeMap, take, tap, switchMap, first } from 'rxjs/operators';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const cssClassNames = {
loaded: 'ng-lazyloaded',
loading: 'ng-lazyloading',
failed: 'ng-failed-lazyloaded',
};
/**
* @param {?} element
* @param {?} cssClassName
* @return {?}
*/
function removeCssClassName(element, cssClassName) {
element.className = element.className.replace(cssClassName, '');
}
/**
* @param {?} element
* @param {?} cssClassName
* @return {?}
*/
function addCssClassName(element, cssClassName) {
if (!element.className.includes(cssClassName)) {
element.className += ` ${cssClassName}`;
}
}
/**
* @param {?} element
* @param {?} cssClassName
* @return {?}
*/
function hasCssClassName(element, cssClassName) {
return element.className && element.className.includes(cssClassName);
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @return {?}
*/
function isWindowDefined() {
return typeof window !== 'undefined';
}
/**
* @param {?} element
* @return {?}
*/
function isChildOfPicture(element) {
return Boolean(element.parentElement && element.parentElement.nodeName.toLowerCase() === 'picture');
}
/**
* @param {?} element
* @return {?}
*/
function isImageElement(element) {
return element.nodeName.toLowerCase() === 'img';
}
/**
* @param {?} element
* @param {?} imagePath
* @param {?} useSrcset
* @return {?}
*/
function setImage(element, imagePath, useSrcset) {
if (isImageElement(element)) {
if (useSrcset) {
element.srcset = imagePath;
}
else {
element.src = imagePath;
}
}
else {
element.style.backgroundImage = `url('${imagePath}')`;
}
return element;
}
/**
* @param {?} attrName
* @return {?}
*/
function setSources(attrName) {
return (/**
* @param {?} image
* @return {?}
*/
(image) => {
/** @type {?} */
const sources = image.parentElement.getElementsByTagName('source');
for (let i = 0; i < sources.length; i++) {
/** @type {?} */
const attrValue = sources[i].getAttribute(attrName);
if (attrValue) {
sources[i].srcset = attrValue;
}
}
});
}
/** @type {?} */
const setSourcesToDefault = setSources('defaultImage');
/** @type {?} */
const setSourcesToLazy = setSources('lazyLoad');
/** @type {?} */
const setSourcesToError = setSources('errorImage');
/**
* @param {?} setSourcesFn
* @return {?}
*/
function setImageAndSources(setSourcesFn) {
return (/**
* @param {?} element
* @param {?} imagePath
* @param {?} useSrcset
* @return {?}
*/
(element, imagePath, useSrcset) => {
if (isImageElement(element) && isChildOfPicture(element)) {
setSourcesFn(element);
}
if (imagePath) {
setImage(element, imagePath, useSrcset);
}
});
}
/** @type {?} */
const setImageAndSourcesToDefault = setImageAndSources(setSourcesToDefault);
/** @type {?} */
const setImageAndSourcesToLazy = setImageAndSources(setSourcesToLazy);
/** @type {?} */
const setImageAndSourcesToError = setImageAndSources(setSourcesToError);
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const end = (/**
* @param {?} __0
* @return {?}
*/
({ element }) => addCssClassName(element, cssClassNames.loaded));
/** @type {?} */
const loadImage = (/**
* @param {?} __0
* @return {?}
*/
({ element, useSrcset, imagePath }) => {
/** @type {?} */
let img;
if (isImageElement(element) && isChildOfPicture(element)) {
/** @type {?} */
const parentClone = (/** @type {?} */ (element.parentNode.cloneNode(true)));
img = parentClone.getElementsByTagName('img')[0];
setSourcesToLazy(img);
setImage(img, imagePath, useSrcset);
}
else {
img = new Image();
if (isImageElement(element) && element.sizes) {
img.sizes = element.sizes;
}
if (useSrcset) {
img.srcset = imagePath;
}
else {
img.src = imagePath;
}
}
return Observable.create((/**
* @param {?} observer
* @return {?}
*/
observer => {
img.onload = (/**
* @return {?}
*/
() => {
observer.next(imagePath);
observer.complete();
});
img.onerror = (/**
* @param {?} err
* @return {?}
*/
err => {
observer.error(null);
});
}));
});
/** @type {?} */
const setErrorImage = (/**
* @param {?} __0
* @return {?}
*/
({ element, errorImagePath, useSrcset }) => {
setImageAndSourcesToError(element, errorImagePath, useSrcset);
addCssClassName(element, cssClassNames.failed);
});
/** @type {?} */
const setLoadedImage = (/**
* @param {?} __0
* @return {?}
*/
({ element, imagePath, useSrcset }) => {
setImageAndSourcesToLazy(element, imagePath, useSrcset);
});
/** @type {?} */
const setup = (/**
* @param {?} __0
* @return {?}
*/
({ element, defaultImagePath, useSrcset }) => {
setImageAndSourcesToDefault(element, defaultImagePath, useSrcset);
if (hasCssClassName(element, cssClassNames.loaded)) {
removeCssClassName(element, cssClassNames.loaded);
}
});
/** @type {?} */
const sharedPreset = {
finally: end,
loadImage,
setErrorImage,
setLoadedImage,
setup
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class Rect {
/**
* @param {?} left
* @param {?} top
* @param {?} right
* @param {?} bottom
*/
constructor(left, top, right, bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
/**
* @param {?} element
* @return {?}
*/
static fromElement(element) {
const { left, top, right, bottom } = element.getBoundingClientRect();
if (left === 0 && top === 0 && right === 0 && bottom === 0) {
return Rect.empty;
}
else {
return new Rect(left, top, right, bottom);
}
}
/**
* @param {?} _window
* @return {?}
*/
static fromWindow(_window) {
return new Rect(0, 0, _window.innerWidth, _window.innerHeight);
}
/**
* @param {?} inflateBy
* @return {?}
*/
inflate(inflateBy) {
this.left -= inflateBy;
this.top -= inflateBy;
this.right += inflateBy;
this.bottom += inflateBy;
}
/**
* @param {?} rect
* @return {?}
*/
intersectsWith(rect) {
return rect.left < this.right && this.left < rect.right && rect.top < this.bottom && this.top < rect.bottom;
}
/**
* @param {?} rect
* @return {?}
*/
getIntersectionWith(rect) {
/** @type {?} */
const left = Math.max(this.left, rect.left);
/** @type {?} */
const top = Math.max(this.top, rect.top);
/** @type {?} */
const right = Math.min(this.right, rect.right);
/** @type {?} */
const bottom = Math.min(this.bottom, rect.bottom);
if (right >= left && bottom >= top) {
return new Rect(left, top, right, bottom);
}
else {
return Rect.empty;
}
}
}
Rect.empty = new Rect(0, 0, 0, 0);
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const scrollListeners = new WeakMap();
/**
* @param {?} obs
* @param {?=} scheduler
* @return {?}
*/
function sampleObservable(obs, scheduler) {
return obs.pipe(sampleTime(100, scheduler), share(), startWith(''));
}
// Only create one scroll listener per target and share the observable.
// Typical, there will only be one observable per application
/** @type {?} */
const getScrollListener = (/**
* @param {?} scrollTarget
* @return {?}
*/
(scrollTarget) => {
if (!scrollTarget || typeof scrollTarget.addEventListener !== 'function') {
if (isWindowDefined()) {
console.warn('`addEventListener` on ' + scrollTarget + ' (scrollTarget) is not a function. Skipping this target');
}
return empty();
}
if (scrollListeners.has(scrollTarget)) {
return scrollListeners.get(scrollTarget);
}
/** @type {?} */
const srollEvent = Observable.create((/**
* @param {?} observer
* @return {?}
*/
observer => {
/** @type {?} */
const eventName = 'scroll';
/** @type {?} */
const handler = (/**
* @param {?} event
* @return {?}
*/
event => observer.next(event));
/** @type {?} */
const options = { passive: true, capture: false };
scrollTarget.addEventListener(eventName, handler, options);
return (/**
* @return {?}
*/
() => scrollTarget.removeEventListener(eventName, handler, options));
}));
/** @type {?} */
const listener = sampleObservable(srollEvent);
scrollListeners.set(scrollTarget, listener);
return listener;
});
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const isVisible = (/**
* @param {?} __0
* @param {?=} getWindow
* @return {?}
*/
({ element, offset, scrollContainer }, getWindow = (/**
* @return {?}
*/
() => window)) => {
/** @type {?} */
const elementBounds = Rect.fromElement(element);
if (elementBounds === Rect.empty) {
return false;
}
/** @type {?} */
const windowBounds = Rect.fromWindow(getWindow());
elementBounds.inflate(offset);
if (scrollContainer) {
/** @type {?} */
const scrollContainerBounds = Rect.fromElement(scrollContainer);
/** @type {?} */
const intersection = scrollContainerBounds.getIntersectionWith(windowBounds);
return elementBounds.intersectsWith(intersection);
}
else {
return elementBounds.intersectsWith(windowBounds);
}
});
/** @type {?} */
const getObservable = (/**
* @param {?} attributes
* @return {?}
*/
(attributes) => {
if (attributes.scrollObservable) {
return attributes.scrollObservable.pipe(startWith(''));
}
if (attributes.scrollContainer) {
return getScrollListener(attributes.scrollContainer);
}
return getScrollListener(isWindowDefined() ? window : undefined);
});
/** @type {?} */
const scrollPreset = Object.assign({}, sharedPreset, {
isVisible,
getObservable
});
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @template E
* @param {?=} options
* @return {?}
*/
function cretateHooks(options) {
if (!options) {
return scrollPreset;
}
/** @type {?} */
const hooks = {};
if (options.preset) {
Object.assign(hooks, options.preset);
}
else {
Object.assign(hooks, scrollPreset);
}
Object.keys(options)
.filter((/**
* @param {?} key
* @return {?}
*/
key => key !== 'preset'))
.forEach((/**
* @param {?} key
* @return {?}
*/
key => {
hooks[key] = options[key];
}));
return (/** @type {?} */ (hooks));
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @template E
* @param {?} hookSet
* @param {?} attributes
* @return {?}
*/
function lazyLoadImage(hookSet, attributes) {
return (/**
* @param {?} scrollObservable
* @return {?}
*/
(scrollObservable) => {
return scrollObservable.pipe(filter((/**
* @param {?} event
* @return {?}
*/
event => hookSet.isVisible({
element: attributes.element,
event: event,
offset: attributes.offset,
scrollContainer: attributes.scrollContainer
}))), take(1), mergeMap((/**
* @return {?}
*/
() => hookSet.loadImage(attributes))), tap((/**
* @param {?} imagePath
* @return {?}
*/
imagePath => hookSet.setLoadedImage({
element: attributes.element,
imagePath,
useSrcset: attributes.useSrcset
}))), map((/**
* @return {?}
*/
() => true)), catchError((/**
* @return {?}
*/
() => {
hookSet.setErrorImage(attributes);
return of(false);
})), tap((/**
* @return {?}
*/
() => hookSet.finally(attributes))));
});
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class LazyLoadImageDirective {
/**
* @param {?} el
* @param {?} ngZone
* @param {?} platformId
* @param {?=} options
*/
constructor(el, ngZone, platformId, options) {
this.platformId = platformId;
// Whether srcset attribute should be used instead of src
this.onLoad = new EventEmitter(); // Callback when an image is loaded
this.elementRef = el;
this.ngZone = ngZone;
this.propertyChanges$ = new ReplaySubject();
this.hooks = cretateHooks(options);
}
/**
* @return {?}
*/
ngOnChanges() {
this.propertyChanges$.next({
element: this.elementRef.nativeElement,
imagePath: this.lazyImage,
defaultImagePath: this.defaultImage,
errorImagePath: this.errorImage,
useSrcset: this.useSrcset,
offset: this.offset | 0,
scrollContainer: this.scrollTarget,
scrollObservable: this.scrollObservable
});
}
/**
* @return {?}
*/
ngAfterContentInit() {
// Disable lazy load image in server side
if (isPlatformServer(this.platformId)) {
this.scrollSubscription = this.propertyChanges$
.pipe(tap((/**
* @param {?} attributes
* @return {?}
*/
attributes => this.hooks.setup(attributes))), first())
.subscribe();
return null;
}
this.ngZone.runOutsideAngular((/**
* @return {?}
*/
() => {
this.scrollSubscription = this.propertyChanges$
.pipe(tap((/**
* @param {?} attributes
* @return {?}
*/
attributes => this.hooks.setup(attributes))), switchMap((/**
* @param {?} attributes
* @return {?}
*/
attributes => this.hooks
.getObservable(attributes)
.pipe(lazyLoadImage(this.hooks, attributes)))))
.subscribe((/**
* @param {?} success
* @return {?}
*/
success => this.onLoad.emit(success)));
}));
}
/**
* @return {?}
*/
ngOnDestroy() {
[this.scrollSubscription]
.filter((/**
* @param {?} subscription
* @return {?}
*/
subscription => subscription && !subscription.isUnsubscribed))
.forEach((/**
* @param {?} subscription
* @return {?}
*/
subscription => subscription.unsubscribe()));
}
}
LazyLoadImageDirective.decorators = [
{ type: Directive, args: [{
selector: '[lazyLoad]'
},] }
];
/** @nocollapse */
LazyLoadImageDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: NgZone },
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] },
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: ['options',] }] }
];
LazyLoadImageDirective.propDecorators = {
lazyImage: [{ type: Input, args: ['lazyLoad',] }],
defaultImage: [{ type: Input }],
errorImage: [{ type: Input }],
scrollTarget: [{ type: Input }],
scrollObservable: [{ type: Input }],
offset: [{ type: Input }],
useSrcset: [{ type: Input }],
onLoad: [{ type: Output }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class LazyLoadImageModule {
/**
* @param {?} options
* @return {?}
*/
static forRoot(options) {
return {
ngModule: LazyLoadImageModule,
providers: [{ provide: 'options', useValue: options }]
};
}
}
LazyLoadImageModule.decorators = [
{ type: NgModule, args: [{
declarations: [LazyLoadImageDirective],
exports: [LazyLoadImageDirective]
},] }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const observers = new WeakMap();
/** @type {?} */
const intersectionSubject = new Subject();
/**
* @param {?} entrys
* @return {?}
*/
function loadingCallback(entrys) {
entrys.forEach((/**
* @param {?} entry
* @return {?}
*/
entry => intersectionSubject.next(entry)));
}
/** @type {?} */
const getIntersectionObserver = (/**
* @param {?} attributes
* @return {?}
*/
(attributes) => {
if (!attributes.scrollContainer && !isWindowDefined()) {
return empty();
}
/** @type {?} */
const options = {
root: attributes.scrollContainer
};
if (attributes.offset) {
options.rootMargin = `${attributes.offset}px`;
}
/** @type {?} */
const scrollContainer = attributes.scrollContainer || window;
/** @type {?} */
let observer = observers.get(scrollContainer);
if (!observer) {
observer = new IntersectionObserver(loadingCallback, options);
observers.set(scrollContainer, observer);
}
observer.observe(attributes.element);
return Observable.create((/**
* @param {?} obs
* @return {?}
*/
obs => {
/** @type {?} */
const subscription = intersectionSubject.pipe(filter((/**
* @param {?} entry
* @return {?}
*/
entry => entry.target === attributes.element))).subscribe(obs);
return (/**
* @return {?}
*/
() => {
subscription.unsubscribe();
observer.unobserve(attributes.element);
});
}));
});
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const isVisible$1 = (/**
* @param {?} __0
* @return {?}
*/
({ event }) => {
return event.isIntersecting;
});
/** @type {?} */
const getObservable$1 = (/**
* @param {?} attributes
* @param {?=} _getInterObserver
* @return {?}
*/
(attributes, _getInterObserver = getIntersectionObserver) => {
if (attributes.scrollObservable) {
return attributes.scrollObservable;
}
return _getInterObserver(attributes);
});
/** @type {?} */
const intersectionObserverPreset = Object.assign({}, sharedPreset, {
isVisible: isVisible$1,
getObservable: getObservable$1
});
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
export default LazyLoadImageModule;
export { LazyLoadImageDirective, LazyLoadImageModule, intersectionObserverPreset, scrollPreset };
//# sourceMappingURL=ng-lazyload-image-angular-universal.js.map