UNPKG

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
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