src/lib/ScrollTracker.ts
import sengEvent from 'seng-event';
import ScrollTrackerPoint from './ScrollTrackerPoint';
import Axis from './enum/Axis';
import ScrollTrackerEvent from './event/ScrollTrackerEvent';
import Side from './enum/Side';
const throttle = require('lodash/throttle');
const size = require('element-size');
/**
* Class that keeps track of the vertical scroll position of an element.
*/
export default class ScrollTracker extends sengEvent {
private static _DEFAULT_THROTTLE_SCROLL: number = 1000 / 60;
private static _DEFAULT_THROTTLE_RESIZE: number = 200;
public trackingPoints: Array<ScrollTrackerPoint> = [];
public viewSize: number = 0;
public scrollSize: number = 0;
public viewStart: number = 0;
public viewEnd: number = 0;
protected lastScrollPosition: number = 0;
constructor(private element: HTMLElement | Window = window, private targetAxis: Axis = Axis.Y) {
super();
setTimeout(
() => {
if (this.isDisposed()) {
return;
}
this.updateSize();
this.initEvents();
},
0,
);
}
/**
* Returns which axis this ScrollTracker instance is tracking.
*/
public get axis(): Axis {
return this.targetAxis;
}
/**
* Returns the target element this ScrollTracker instance is tracking.
*/
public get targetElement(): HTMLElement | Window {
return this.element;
}
/**
* Updates the size of the viewport of the target element.
*/
public updateSize(): void {
const isX = this.axis === Axis.X;
const dimensions = size(this.targetElement);
this.viewSize = isX ? dimensions[0] : dimensions[1];
if (this.targetElement === window) {
const dimensions = size(document.body);
this.scrollSize = isX ? dimensions[0] : dimensions[1];
} else {
const target = <HTMLElement>this.targetElement;
this.scrollSize = isX ? target.scrollWidth : target.scrollHeight;
}
this.updateScrollPosition();
}
/**
* Adds a new point of which we will detect when it enters and leaves the view.
* @param position The position of this points in pixels. This is the distance from the start
* or end of the target element depending on the 'side' parameter, measured horizontally or
* vertically depending on the axis of this ScrollTracker instance.
* @param side The side from which the 'position' parameter is defined. Side.START measures the
* position from the top or left edge and Side.END will measure the position from the bottom
* or right edge.
* @returns {ScrollTrackerPoint} A reference to a ScrollTrackerPoint instance that can be
* used to bind events, remove or update the point added.
*/
public addPoint(position: number, height: number = 1, side: Side = Side.START): ScrollTrackerPoint {
const point = new ScrollTrackerPoint(position, height, side, this);
this.trackingPoints.push(point);
point.addEventListener(ScrollTrackerEvent.types.ENTER_VIEW, this.pointEventHandler);
point.addEventListener(ScrollTrackerEvent.types.LEAVE_VIEW, this.pointEventHandler);
return point;
}
/**
* Removes an existing point from this ScrollTracker. This point will be destructed and will
* no longer throw events.
* @param point The ScrollTrackerPoint instance to remove.
* @returns {boolean} Boolean indicating if the point was found and removed successfully.
*/
public removePoint(point: ScrollTrackerPoint): boolean {
const index = this.trackingPoints.indexOf(point);
if (index >= 0) {
this.trackingPoints[index].dispose();
this.trackingPoints.splice(index, 1);
return true;
}
return false;
}
/**
* Removes all points from this ScrollTracker instance. They will be destructed and will
* no longer throw events.
*/
public removeAllPoints(): void {
for (let i = 0; i < this.trackingPoints.length; i += 1) {
this.trackingPoints[i].dispose();
}
this.trackingPoints.length = 0;
}
/**
* Initialize scroll and resize events using jQuery. Resize events will only be used when
* the target of ScrollTracker is 'window'. If the target is not window, updateSize() has
* to be called manually to update the view size.
*/
protected initEvents(): void {
if (this.targetElement === window) {
window.addEventListener(
'resize',
throttle(this.windowResizeHandler, ScrollTracker._DEFAULT_THROTTLE_RESIZE),
);
this.windowResizeHandler();
} else {
this.updateSize();
}
this.targetElement.addEventListener(
'scroll',
throttle(this.scrollHandler, ScrollTracker._DEFAULT_THROTTLE_SCROLL),
);
}
/**
* Handles events thrown by ScrollTrackerPoint instances and bubbles them up to this
* ScrollTracker instance.
* @param event The event thrown.
*/
private pointEventHandler = (event: ScrollTrackerEvent) => {
this.dispatchEvent(event);
}
protected updateScrollPosition() {
const isX = this.axis === Axis.X;
if (this.targetElement === window) {
this.viewStart = isX ? window.pageXOffset : window.pageYOffset;
} else {
const target = <HTMLElement>this.targetElement;
this.viewStart = isX ? target.scrollLeft : target.scrollTop;
}
this.viewEnd = this.viewStart + this.viewSize;
this.lastScrollPosition = this.viewStart;
}
/**
* Event handler called when the target element is scrolled. Will detect the new scroll
* position and call checkInView() on all tracking points.
*/
protected scrollHandler = () => {
this.updateScrollPosition();
const scrollingBack = this.viewStart < this.lastScrollPosition;
for (let i = 0; i < this.trackingPoints.length; i += 1) {
this.trackingPoints[i].checkInView(scrollingBack);
}
}
/**
* Event handler called when the window resizes. Only used when the target of this ScrollTracker
* instance is the window object.
*/
protected windowResizeHandler = () => {
this.updateSize();
}
/**
* Disposes this ScrollTracker and all points created on it. Removes all event handlers.
*/
public dispose(): void {
window.removeEventListener('resize', this.windowResizeHandler);
this.targetElement.removeEventListener('scroll', this.scrollHandler);
this.removeAllPoints();
super.dispose();
}
}