diff --git a/src/lazyload-image.directive.ts b/src/lazyload-image.directive.ts index 6bb51d5..a4452a9 100644 --- a/src/lazyload-image.directive.ts +++ b/src/lazyload-image.directive.ts @@ -18,13 +18,13 @@ import { import { getScrollListener } from './scroll-listener'; import { lazyLoadImage } from './lazyload-image'; -const target = typeof window !== 'undefined' ? window : undefined; +const windowTarget = typeof window !== 'undefined' ? window : undefined; interface LazyLoadImageDirectiveProps { lazyImage: string; defaultImage: string; errorImage: string; - scrollTarget: Object; + scrollTarget: HTMLElement; scrollObservable: Observable; offset: number; useSrcset: boolean; @@ -34,13 +34,13 @@ interface LazyLoadImageDirectiveProps { selector: '[lazyLoad]' }) export class LazyLoadImageDirective implements OnChanges, AfterContentInit, OnDestroy { - @Input('lazyLoad') lazyImage; // The image to be lazy loaded - @Input() defaultImage: string; // The image to be displayed before lazyImage is loaded - @Input() errorImage: string; // The image to be displayed if lazyImage load fails - @Input() scrollTarget = target; // Change the node we should listen for scroll events on, default is window - @Input() scrollObservable; // Pass your own scroll emitter - @Input() offset: number; // The number of px a image should be loaded before it is in view port - @Input() useSrcset: boolean; // Whether srcset attribute should be used instead of src + @Input('lazyLoad') lazyImage; // The image to be lazy loaded + @Input() defaultImage: string; // The image to be displayed before lazyImage is loaded + @Input() errorImage: string; // The image to be displayed if lazyImage load fails + @Input() scrollTarget: HTMLElement; // Scroll container that contains the image and emits scoll events + @Input() scrollObservable; // Pass your own scroll emitter + @Input() offset: number; // The number of px a image should be loaded before it is in view port + @Input() useSrcset: boolean; // Whether srcset attribute should be used instead of src @Output() onLoad: EventEmitter = new EventEmitter(); // Callback when an image is loaded private propertyChanges$: ReplaySubject; private elementRef: ElementRef; @@ -81,7 +81,7 @@ export class LazyLoadImageDirective implements OnChanges, AfterContentInit, OnDe if (this.scrollObservable) { scrollObservable = this.scrollObservable.startWith(''); } else { - scrollObservable = getScrollListener(this.scrollTarget); + scrollObservable = getScrollListener(this.scrollTarget || windowTarget); } this.scrollSubscription = this.propertyChanges$ .debounceTime(10) @@ -92,7 +92,8 @@ export class LazyLoadImageDirective implements OnChanges, AfterContentInit, OnDe props.defaultImage, props.errorImage, props.offset, - props.useSrcset + props.useSrcset, + props.scrollTarget ) )) .subscribe(success => this.onLoad.emit(success)); diff --git a/src/lazyload-image.ts b/src/lazyload-image.ts index 63d1827..ad08180 100644 --- a/src/lazyload-image.ts +++ b/src/lazyload-image.ts @@ -9,12 +9,18 @@ import { Observable } from 'rxjs/Observable'; import { getScrollListener } from './scroll-listener'; import { Rect } from './rect'; -export function isVisible(element: HTMLElement, threshold = 0, _window = window) { +export function isVisible(element: HTMLElement, threshold = 0, _window: Window, scrollContainer?: HTMLElement) { const elementBounds = Rect.fromElement(element); const windowBounds = Rect.fromWindow(_window); elementBounds.inflate(threshold); - - return elementBounds.intersectsWith(windowBounds); + + if (scrollContainer) { + const scrollContainerBounds = Rect.fromElement(scrollContainer); + const intersection = scrollContainerBounds.getIntersectionWith(windowBounds); + return elementBounds.intersectsWith(intersection); + } else { + return elementBounds.intersectsWith(windowBounds); + } } export function isChildOfPicture(element: HTMLImageElement | HTMLDivElement): boolean { @@ -110,7 +116,7 @@ function setLoadedStyle(element: HTMLImageElement | HTMLDivElement) { return element; } -export function lazyLoadImage(element: HTMLImageElement | HTMLDivElement, imagePath: string, defaultImagePath: string, errorImgPath: string, offset: number, useSrcset: boolean = false) { +export function lazyLoadImage(element: HTMLImageElement | HTMLDivElement, imagePath: string, defaultImagePath: string, errorImgPath: string, offset: number, useSrcset: boolean = false, scrollContainer?: HTMLElement) { setImageAndSourcesToDefault(element, defaultImagePath, useSrcset); if (element.className && element.className.includes('ng-lazyloaded')) { element.className = element.className.replace('ng-lazyloaded', ''); @@ -118,7 +124,7 @@ export function lazyLoadImage(element: HTMLImageElement | HTMLDivElement, imageP return (scrollObservable: Observable) => { return scrollObservable - .filter(() => isVisible(element, offset)) + .filter(() => isVisible(element, offset, window, scrollContainer)) .take(1) .mergeMap(() => loadImage(element, imagePath, useSrcset)) .do(() => setImageAndSourcesToLazy(element, imagePath, useSrcset)) diff --git a/src/rect.ts b/src/rect.ts index b360b9c..107751b 100644 --- a/src/rect.ts +++ b/src/rect.ts @@ -1,4 +1,6 @@ export class Rect { + static empty: Rect = new Rect(0, 0, 0, 0); + left: number; top: number; right: number; @@ -33,4 +35,17 @@ export class Rect { (rect.top < this.bottom) && (this.top < rect.bottom); } + + getIntersectionWith(rect: Rect): Rect { + const left = Math.max(this.left, rect.left); + const top = Math.max(this.top, rect.top); + const right = Math.min(this.right, rect.right); + const bottom = Math.min(this.bottom, rect.bottom); + + if (right >= left && bottom >= top) { + return new Rect(left, top, right, bottom); + } else { + return Rect.empty; + } + } } diff --git a/test/lazyload-image.test.ts b/test/lazyload-image.test.ts index fd75a6b..4871b86 100644 --- a/test/lazyload-image.test.ts +++ b/test/lazyload-image.test.ts @@ -210,6 +210,38 @@ describe('Lazy load image', () => { is(result, true); }); + + it('Should not be visible when image is horizontally in window\'s view, but not in scroll-container\'s', () => { + const element = generateElement(800, 0, 1200, 1200); + const scrollContainer = generateElement(0, 0, 700, 1200); + const result = isVisible(element, 0, _window, scrollContainer); + + is(result, false); + }); + + it('Should not be visible when image is vertically in window\'s view, but not in scroll-container\'s', () => { + const element = generateElement(0, 800, 1200, 1200); + const scrollContainer = generateElement(0, 0, 1200, 700); + const result = isVisible(element, 0, _window, scrollContainer); + + is(result, false); + }); + + it('Should not be visible when image is not in window\'s view, but is in scroll-container\'s', () => { + const element = generateElement(1400, 0, 1200, 1200); + const scrollContainer = generateElement(1300, 0, 1200, 1200); + const result = isVisible(element, 0, _window, scrollContainer); + + is(result, false); + }); + + it('Should be visible when image is in window\'s and scroll-container\'s view', () => { + const element = generateElement(100, 0, 1200, 1200); + const scrollContainer = generateElement(0, 0, 700, 1200); + const result = isVisible(element, 0, _window, scrollContainer); + + is(result, true); + }); }); }); diff --git a/test/rect.test.ts b/test/rect.test.ts index e5b334c..5e05bcd 100644 --- a/test/rect.test.ts +++ b/test/rect.test.ts @@ -296,4 +296,66 @@ describe('Rect', () => { is(result, true); }); }); + + describe('getIntersectionWith', () => { + it('Should return a correctly sized Rect if two Rect\'s intersect horizontally', () => { + // Arrange + const rectA = new Rect(0, 0, 20, 20); + const rectB = new Rect(0, 10, 20, 30); + + // Act + const result = rectA.getIntersectionWith(rectB); + + // Assert + is(result.top, 10); + is(result.right, 20); + is(result.bottom, 20); + is(result.left, 0); + }); + + it('Should return a correctly sized Rect if two Rect\'s intersect vertically', () => { + // Arrange + const rectA = new Rect(0, 0, 20, 20); + const rectB = new Rect(10, 0, 30, 20); + + // Act + const result = rectA.getIntersectionWith(rectB); + + // Assert + is(result.top, 0); + is(result.right, 20); + is(result.bottom, 20); + is(result.left, 10); + }); + + it('Should return a correctly sized Rect if two Rect\'s intersect corners', () => { + // Arrange + const rectA = new Rect(0, 0, 20, 20); + const rectB = new Rect(10, 10, 30, 30); + + // Act + const result = rectA.getIntersectionWith(rectB); + + // Assert + is(result.top, 10); + is(result.right, 20); + is(result.bottom, 20); + is(result.left, 10); + }); + + it('Should return an empty Rect if two Rect\'s don\'t intersect', () => { + // Arrange + const rectA = new Rect(0, 0, 20, 20); + const rectB = new Rect(30, 30, 50, 50); + + // Act + const result = rectA.getIntersectionWith(rectB); + + // Assert + is(result.top, 0); + is(result.right, 0); + is(result.bottom, 0); + is(result.left, 0); + }); + }); });