diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f3c4f6..fba17a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v 0.8.0 (2018/01/02) +* [FIX] - now triggers only once when in or after target (#200) +* [REFACTOR] - "distance" number has been refined to be the percentage point of the scroll nob. +* [REFACTOR] - added more unit tests. + ## v 0.7.2 (2017/12/07) * [FIX] - fix for ie11 - fix #157 diff --git a/README.md b/README.md index d2739d4c..0e32c3db 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ npm install ngx-infinite-scroll --save ## Supported API Currently supported attributes: -* **infiniteScrollDistance**<_number_> - (optional, default: **2**) - should get a number, the number of viewport lengths from the bottom of the page at which the event will be triggered. +* **infiniteScrollDistance**<_number_> - (optional, default: **2**) - the bottom percentage point of the scroll nob relatively to the infinite-scroll container (i.e, 2 (2 * 10 = 20%) is event is triggered when 80% (100% - 20%) has been scrolled). +if container.height is 900px, when the container is scrolled to or past the 720px, it will fire the scrolled event. * **infiniteScrollUpDistance**<_number_> - (optional, default: **1.5**) - should get a number * **infiniteScrollThrottle**<_number_> - (optional, default: **300**) - should get a number of **milliseconds** for throttle. The event will be triggered this many milliseconds after the user *stops* scrolling. * **infiniteScrollContainer**<_string|HTMLElement_> - (optional, default: null) - should get a html element or css selector for a scrollable element; window or current element will be used if this attribute is empty. diff --git a/package.json b/package.json index e67f48ad..53696df8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngx-infinite-scroll", - "version": "0.7.2", + "version": "0.8.0", "description": "An infinite scroll directive for Angular compatible with AoT compilation and Tree shaking", "main": "./bundles/ngx-infinite-scroll.umd.js", "module": "./modules/ngx-infinite-scroll.es5.js", diff --git a/src/models.ts b/src/models.ts index dd4b414f..3c4cf3f3 100644 --- a/src/models.ts +++ b/src/models.ts @@ -9,7 +9,6 @@ export interface InfiniteScrollEvent { export interface IPositionElements { windowElement: ContainerRef; axis: any; - isWindow: boolean; } export interface IPositionStats { @@ -31,6 +30,13 @@ export interface IScrollStats { shouldScroll: boolean; } +export interface IScrollState { + lastTotalToScroll: number; + totalToScroll: number; + isTriggeredTotal: boolean; + lastScrollPosition: number; +} + export interface IResolver { container: ContainerRef; isWindow: boolean; diff --git a/src/services/event-trigger.ts b/src/services/event-trigger.ts index fbf00b9a..3800bf3d 100644 --- a/src/services/event-trigger.ts +++ b/src/services/event-trigger.ts @@ -20,16 +20,11 @@ export interface IDistanceRange { export interface IScrollConfig { alwaysCallback: boolean; - disable: boolean; - shouldScroll: boolean; + shouldFireScrollEvent: boolean; } -export function shouldTriggerEvents({ - alwaysCallback, - shouldScroll, - disable -}: IScrollConfig) { - return (alwaysCallback || shouldScroll) && !disable; +export function shouldTriggerEvents({ alwaysCallback, shouldFireScrollEvent }: IScrollConfig) { + return (alwaysCallback || shouldFireScrollEvent); } export function triggerEvents( diff --git a/src/services/position-resolver.ts b/src/services/position-resolver.ts index 0bbd00e2..c85d99b1 100644 --- a/src/services/position-resolver.ts +++ b/src/services/position-resolver.ts @@ -4,15 +4,11 @@ import { ContainerRef, IPositionElements, IPositionStats, IResolver } from '../m import { AxisResolver } from './axis-resolver'; export function createResolver({ - isWindow, windowElement, axis }: IPositionElements): IResolver { return createResolverWithContainer( - { - axis, - isWindow - }, + { axis, isWindow: isElementWindow(windowElement) }, windowElement ); } diff --git a/src/services/scroll-register.ts b/src/services/scroll-register.ts index e2234e9f..0352e21f 100644 --- a/src/services/scroll-register.ts +++ b/src/services/scroll-register.ts @@ -8,12 +8,13 @@ import { ElementRef } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; -import { ContainerRef, IPositionStats } from '../models'; +import { ContainerRef, IPositionStats, IScrollState } from '../models'; import { AxisResolver } from './axis-resolver'; import { shouldTriggerEvents, triggerEvents } from './event-trigger'; import { resolveContainerElement } from './ngx-ins-utils'; -import { calculatePoints, createResolver, isElementWindow } from './position-resolver'; -import { getScrollStats, updateScrollPosition } from './scroll-resolver'; +import { calculatePoints, createResolver } from './position-resolver'; +import * as ScrollResolver from './scroll-resolver'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; export interface IScrollRegisterConfig { container: ContainerRef; @@ -39,42 +40,40 @@ export interface IScroller { }; } -export function attachScrollEvent( - options: IScrollRegisterConfig -): Subscription { - return Observable.fromEvent(options.container, 'scroll') - .sampleTime(options.throttleDuration) - .mergeMap((ev: any) => Observable.of(options.mergeMap(ev))) - .subscribe(options.scrollHandler); -} - export function createScroller(config: IScroller): Subscription { - const containerElement = resolveContainerElement( - config.scrollContainer, - config.scrollWindow, - config.element, - config.fromRoot - ); + const { scrollContainer, scrollWindow, element, fromRoot } = config; const resolver = createResolver({ axis: new AxisResolver(!config.horizontal), - isWindow: isElementWindow(containerElement), - windowElement: containerElement + windowElement: resolveContainerElement(scrollContainer, scrollWindow, element, fromRoot) }); - const scrollPosition = { - last: 0 + const stats = calculatePoints(element, resolver); + const scrollState: IScrollState = { + lastScrollPosition: 0, + lastTotalToScroll: 0, + totalToScroll: stats.totalToScroll, + isTriggeredTotal: false }; const options: IScrollRegisterConfig = { container: resolver.container, - mergeMap: () => calculatePoints(config.element, resolver), + mergeMap: () => calculatePoints(element, resolver), scrollHandler: (positionStats: IPositionStats) => - handleOnScroll(scrollPosition, positionStats, config), + handleOnScroll(scrollState, positionStats, config), throttleDuration: config.throttle }; return attachScrollEvent(options); } +export function attachScrollEvent( + options: IScrollRegisterConfig +): Subscription { + return Observable.fromEvent(options.container, 'scroll') + .sampleTime(options.throttleDuration) + .mergeMap((ev: any) => Observable.of(options.mergeMap(ev))) + .subscribe(options.scrollHandler); +} + export function handleOnScroll( - scrollPosition, + scrollState: IScrollState, positionStats: IPositionStats, config: IScroller ) { @@ -82,20 +81,19 @@ export function handleOnScroll( down: config.downDistance, up: config.upDistance }; - const { isScrollingDown, shouldScroll } = getScrollStats( - scrollPosition.last, + const { isScrollingDown, shouldFireScrollEvent } = ScrollResolver.getScrollStats( + scrollState.lastScrollPosition, positionStats, - { - distance - } + { distance } ); const scrollConfig = { alwaysCallback: config.alwaysCallback, - disable: config.disable, - shouldScroll + shouldFireScrollEvent }; - updateScrollPosition(positionStats.scrolledUntilNow, scrollPosition); - if (shouldTriggerEvents(scrollConfig)) { + ScrollResolver.updateScrollState(scrollState, positionStats.scrolledUntilNow, positionStats.totalToScroll); + const shouldTrigger = shouldTriggerEvents(scrollConfig); + if (shouldTrigger && !scrollState.isTriggeredTotal) { + ScrollResolver.updateTriggeredFlag(scrollState, true); triggerEvents( config.events, isScrollingDown, diff --git a/src/services/scroll-resolver.ts b/src/services/scroll-resolver.ts index 3ae9cb82..904956d1 100644 --- a/src/services/scroll-resolver.ts +++ b/src/services/scroll-resolver.ts @@ -1,6 +1,6 @@ -import { IPositionStats, IScrollerConfig } from '../models'; +import { IPositionStats, IScrollerConfig, IScrollState } from '../models'; -export function shouldScroll( +export function shouldFireScrollEvent( container: IPositionStats, config: IScrollerConfig, scrollingDown: boolean @@ -9,14 +9,15 @@ export function shouldScroll( let remaining: number; let containerBreakpoint: number; if (scrollingDown) { - remaining = container.totalToScroll - container.scrolledUntilNow; - containerBreakpoint = container.height * distance.down + 1; + remaining = (container.totalToScroll - container.scrolledUntilNow) / container.totalToScroll; + containerBreakpoint = distance.down / 10; } else { - remaining = container.scrolledUntilNow; - containerBreakpoint = container.height * distance.up + 1; + remaining = container.scrolledUntilNow / container.totalToScroll; + containerBreakpoint = distance.up / 10; } - const shouldScroll: boolean = remaining <= containerBreakpoint; - return shouldScroll; + + const shouldFireEvent: boolean = remaining <= containerBreakpoint; + return shouldFireEvent; } export function isScrollingDownwards( @@ -33,11 +34,33 @@ export function getScrollStats( ) { const isScrollingDown = isScrollingDownwards(lastScrollPosition, container); return { - shouldScroll: shouldScroll(container, config, isScrollingDown), + shouldFireScrollEvent: shouldFireScrollEvent(container, config, isScrollingDown), isScrollingDown }; } -export function updateScrollPosition(position: number, lastPositionState) { - return (lastPositionState.last = position); +export function updateScrollPosition(position: number, scrollState: IScrollState) { + return (scrollState.lastScrollPosition = position); +} + +export function updateTotalToScroll(totalToScroll: number, scrollState: IScrollState) { + scrollState.lastTotalToScroll = scrollState.totalToScroll; + scrollState.totalToScroll = totalToScroll; +} + +export function isSameTotalToScroll(scrollState) { + return scrollState.totalToScroll === scrollState.lastTotalToScroll; +} + +export function updateTriggeredFlag(scrollState, triggered: boolean) { + scrollState.isTriggeredTotal = triggered; +} + +export function updateScrollState(scrollState: IScrollState, scrolledUntilNow: number, totalToScroll: number) { + updateScrollPosition(scrolledUntilNow, scrollState); + updateTotalToScroll(totalToScroll, scrollState); + const isSameTotal = isSameTotalToScroll(scrollState); + if (!isSameTotal) { + updateTriggeredFlag(scrollState, false); + } } diff --git a/tests/services/event-trigger.spec.ts b/tests/services/event-trigger.spec.ts index 39c4c27b..46273cf8 100644 --- a/tests/services/event-trigger.spec.ts +++ b/tests/services/event-trigger.spec.ts @@ -9,49 +9,44 @@ const props = { } as IScrollerProps; describe('EventTrigger', () => { - it('should return true when alwaysCallback, not disabled', () => { + it('should return true when alwaysCallback', () => { const actual = shouldTriggerEvents({ alwaysCallback: true, - shouldScroll: false, - disable: false + shouldFireScrollEvent: false, }); expect(actual).toBeTruthy(); }); - it('should return true when alwaysCallback, shouldScroll and not disabled', () => { + it('should return true when alwaysCallback, shouldFireScrollEvent', () => { const actual = shouldTriggerEvents({ alwaysCallback: true, - shouldScroll: true, - disable: false + shouldFireScrollEvent: true, }); expect(actual).toBeTruthy(); }); - it('should return true when not alwaysCallback, shouldScroll is true and not disabled', () => { + it('should return true when not alwaysCallback, shouldFireScrollEvent is true', () => { const actual = shouldTriggerEvents({ alwaysCallback: false, - shouldScroll: true, - disable: false + shouldFireScrollEvent: true, }); expect(actual).toBeTruthy(); }); - it('should return false when alwaysCallback, shouldScroll is true and disabled', () => { + it('should return false when alwaysCallback, shouldFireScrollEvent is true', () => { const actual = shouldTriggerEvents({ alwaysCallback: true, - shouldScroll: true, - disable: true + shouldFireScrollEvent: true, }); - expect(actual).toBeFalsy(); + expect(actual).toBeTruthy(); }); - it('should return false when not alwaysCallback, shouldScroll is true and disabled', () => { + it('should return false when not alwaysCallback, shouldFireScrollEvent is true', () => { const actual = shouldTriggerEvents({ alwaysCallback: false, - shouldScroll: true, - disable: true + shouldFireScrollEvent: true, }); - expect(actual).toBeFalsy(); + expect(actual).toBeTruthy(); }); describe('triggerEvents', () => { diff --git a/tests/services/position-resolver.spec.ts b/tests/services/position-resolver.spec.ts index 66bedac9..e7762be2 100644 --- a/tests/services/position-resolver.spec.ts +++ b/tests/services/position-resolver.spec.ts @@ -31,7 +31,6 @@ describe('Position Resolver', () => { const actual = createResolver({ axis, windowElement: mockDom.element, - isWindow: true }); expect(actual).toBeDefined(); }); diff --git a/tests/services/scroll-register.spec.ts b/tests/services/scroll-register.spec.ts index 4fd2dac7..3a3b355f 100644 --- a/tests/services/scroll-register.spec.ts +++ b/tests/services/scroll-register.spec.ts @@ -1,16 +1,15 @@ -/* import { Subscription } from 'rxjs/Rx'; import { async, inject } from '@angular/core/testing'; -import { ScrollRegister, IScrollRegisterConfig } from '../../src/services/scroll-register'; +import * as ScrollRegister from '../../src/services/scroll-register'; import { ElementRef } from '@angular/core'; describe('Scroll Regsiter', () => { let mockedElement: ElementRef; let mockedContainer: ElementRef; - let scrollRegister: ScrollRegister; + // let scrollRegister: ScrollRegister; const createMockDom = () => { const container = document.createElement('section'); @@ -24,21 +23,61 @@ describe('Scroll Regsiter', () => { }; beforeEach(() => { - scrollRegister = new ScrollRegister(); + // scrollRegister = new ScrollRegister(); }); it('should create a Subscription of scroll observable', () => { const mockDom = createMockDom(); - const scrollConfig: IScrollRegisterConfig = { + const scrollConfig: ScrollRegister.IScrollRegisterConfig = { container: mockDom.container.nativeElement, mergeMap: (e: any) => e, scrollHandler: (ev: any) => ev, throttleDuration: 300, }; - const scroller$: Subscription = scrollRegister.attachEvent(scrollConfig); + const scroller$: Subscription = ScrollRegister.attachScrollEvent(scrollConfig); const actual = scroller$; expect(actual).toBeDefined(); }); + + // describe('Manage Scroll State', () => { + // it('should backup old Total and update the new one', () => { + // const state = { + // totalToScroll: 10, + // lastTotalToScroll: 0 + // } as any; + // const newTotal = 20; + // ScrollRegister.updateTotalToScroll(newTotal, state); + // const actual = state.totalToScroll; + // const expected = newTotal; + // expect(actual).toEqual(expected); + // }); + + // it('should return false when total != lastTotal', () => { + // const state = { + // totalToScroll: 10, + // lastTotalToScroll: 0 + // } as any; + // const actual = ScrollRegister.isSameTotalToScroll(state); + // expect(actual).toBeFalsy(); + // }); + + // it('should return true total = lastTotal', () => { + // const state = { + // totalToScroll: 10, + // lastTotalToScroll: 10 + // } as any; + // const actual = ScrollRegister.isSameTotalToScroll(state); + // expect(actual).toBeTruthy(); + // }); + + // it('should set the isTriggeredTotal', () => { + // const state = { + // isTriggeredTotal: false + // } as any; + // ScrollRegister.updateTriggeredFlag(state, true); + // const actual = state.isTriggeredTotal; + // expect(actual).toBeTruthy(); + // }); + // }); }); -*/ \ No newline at end of file diff --git a/tests/services/scroll-resolver.spec.ts b/tests/services/scroll-resolver.spec.ts new file mode 100644 index 00000000..19b35cef --- /dev/null +++ b/tests/services/scroll-resolver.spec.ts @@ -0,0 +1,42 @@ +import * as ScrollResolver from '../../src/services/scroll-resolver'; + +describe('Manage Scroll State', () => { + it('should backup old Total and update the new one', () => { + const state = { + totalToScroll: 10, + lastTotalToScroll: 0 + } as any; + const newTotal = 20; + ScrollResolver.updateTotalToScroll(newTotal, state); + const actual = state.totalToScroll; + const expected = newTotal; + expect(actual).toEqual(expected); + }); + + it('should return false when total != lastTotal', () => { + const state = { + totalToScroll: 10, + lastTotalToScroll: 0 + } as any; + const actual = ScrollResolver.isSameTotalToScroll(state); + expect(actual).toBeFalsy(); + }); + + it('should return true total = lastTotal', () => { + const state = { + totalToScroll: 10, + lastTotalToScroll: 10 + } as any; + const actual = ScrollResolver.isSameTotalToScroll(state); + expect(actual).toBeTruthy(); + }); + + it('should set the isTriggeredTotal', () => { + const state = { + isTriggeredTotal: false + } as any; + ScrollResolver.updateTriggeredFlag(state, true); + const actual = state.isTriggeredTotal; + expect(actual).toBeTruthy(); + }); +});