rfx-parallax
Version:
RfxParallax - parallax made easy
567 lines (556 loc) • 26.1 kB
JavaScript
import { isPlatformBrowser } from '@angular/common';
import * as i0 from '@angular/core';
import { PLATFORM_ID, Injectable, Inject, Directive, Input, Component, NgModule } from '@angular/core';
import { Subject, BehaviorSubject } from 'rxjs';
import { trigger, state, style } from '@angular/animations';
class ResizeEventService {
constructor(platformId) {
this.platformId = platformId;
this.subjectResize = new Subject();
this.resizeEvent = this.onResizeEvent.bind(this);
this.isBrowser = isPlatformBrowser(platformId);
}
ngOnDestroy() {
this.destroyListener();
}
/**
* Create window resize listener.
*/
createListener() {
if (this.isBrowser) {
window.addEventListener('resize', this.resizeEvent, { passive: true });
}
}
/**
* Destroy window resize listener.
*/
destroyListener() {
if (this.isBrowser) {
window.removeEventListener('resize', this.resizeEvent);
}
}
/**
* Trigger window resize event.
*/
onResizeEvent() {
this.subjectResize.next(undefined);
}
/**
* Get window resize event.
* @return {Observable<undefined>} - Window resize event.
*/
getResize() {
return this.subjectResize.asObservable();
}
}
ResizeEventService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ResizeEventService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable });
ResizeEventService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ResizeEventService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ResizeEventService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: function () {
return [{ type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }];
} });
class ScrollEventService {
constructor() {
this.subjectScroll = new BehaviorSubject(0);
this.mouseScrollEvent = this.onMouseScroll.bind(this);
}
ngOnDestroy() {
this.destroyListener();
}
/**
* Create mouse scroll listener.
* @param {HTMLElement} element - Element with scroll event.
*/
createListener(element) {
this.element = element;
this.element.addEventListener('scroll', this.mouseScrollEvent, { passive: true });
}
/**
* Destroy mouse scroll listener.
*/
destroyListener() {
var _a;
(_a = this.element) === null || _a === void 0 ? void 0 : _a.removeEventListener('scroll', this.mouseScrollEvent);
this.element = undefined;
}
/**
* Trigger mouse scroll event.
* @param {number} event - Mouse scroll event.
*/
onMouseScroll(event) {
const target = event.target instanceof Document ? event.target.documentElement : event.target;
this.subjectScroll.next(target.scrollTop);
}
/**
* Get body scroll event.
* @return {Observable<number>} - Body scroll event.
*/
getMouseScroll() {
return this.subjectScroll.asObservable();
}
/**
* Get body scroll current value.
* @return {number} - Body scroll value.
*/
getMouseScrollValue() {
return this.subjectScroll.value;
}
}
ScrollEventService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ScrollEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
ScrollEventService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ScrollEventService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ScrollEventService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: function () { return []; } });
class RfxParallaxService {
constructor(resizeEventService, scrollEventService, platformId) {
this.resizeEventService = resizeEventService;
this.scrollEventService = scrollEventService;
this.platformId = platformId;
this.isBrowser = isPlatformBrowser(this.platformId);
}
/**
* Start element scroll event, window resize event and element resize event listeners
* @param element main element with scroll property
*/
initListeners(element) {
if (this.isBrowser) {
this.resizeEventService.createListener();
this.scrollEventService.createListener(element !== null && element !== void 0 ? element : document);
}
}
}
RfxParallaxService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxService, deps: [{ token: ResizeEventService }, { token: ScrollEventService }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable });
RfxParallaxService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: function () {
return [{ type: ResizeEventService }, { type: ScrollEventService }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }];
} });
const visibilityAnimation = [
trigger('visibility', [
state('true', style({
opacity: 1
})),
state('false', style({
opacity: 0,
}))
// transition('true <=> false', [
// animate('{{ transitionDurationMs }}ms {{ transitionDelayMs }}ms {{ transitionTimingFunction }}')
// ], { params: { transitionDurationMs: 0, transitionDelayMs: 0, transitionTimingFunction: 'ease' }})
])
];
class WillChangeDirective {
constructor(htmlElement, renderer, scrollEventService) {
this.htmlElement = htmlElement;
this.renderer = renderer;
this.scrollEventService = scrollEventService;
this.triggerArea = typeof window !== 'undefined' ? window.innerHeight / 4 * 6 : 0;
this.isDisabled = false;
this.willChange = true;
}
ngOnInit() {
if (!this.isDisabled) {
this.willChangeArea = this.getWillChangeArea();
this.createListener();
}
}
ngOnDestroy() {
this.destroyListener();
}
/**
* Create listener for scroll event.
*/
createListener() {
this.scrollEventListener = this.scrollEventService.getMouseScroll().subscribe((scroll) => this.checkWillChange(scroll));
}
/**
* Destroy scroll event listener.
*/
destroyListener() {
var _a;
(_a = this.scrollEventListener) === null || _a === void 0 ? void 0 : _a.unsubscribe();
}
/**
* Refresh will-change area if some properties changed.
* @param {SimpleChanges} changes - Changes.
*/
ngOnChanges(changes) {
if (changes['isDisabled'] && (changes['isDisabled'].currentValue !== undefined) && !changes['isDisabled'].firstChange) {
this.destroyListener();
return;
}
if (changes['triggerArea'] && (changes['triggerArea'].currentValue !== undefined) && !changes['triggerArea'].firstChange && !this.isDisabled) {
const scroll = this.scrollEventService.getMouseScrollValue();
this.willChangeArea = this.getWillChangeArea();
this.checkWillChange(scroll);
}
}
/**
* Check will-change property and
* update if needed.
* @param {number} scroll - Scroll position.
*/
checkWillChange(scroll) {
if (this.willChangeArea) {
const isElementInArea = this.isElementInArea(scroll, this.willChangeArea);
if (isElementInArea !== this.willChange) {
this.setWillChange(isElementInArea);
}
}
}
/**
* Check if element is in will-change area.
* @param {number} scroll - Scroll position.
* @param {SectionAreaModel} willChangeArea - Will-change area.
* @returns {boolean} - Is element in will-change area.
*/
isElementInArea(scroll, willChangeArea) {
return scroll >= willChangeArea.top && scroll <= willChangeArea.bottom;
}
/**
* Calculate area where element is probably going to be animated soon.
* @returns {number} - Will-change area.
*/
getWillChangeArea() {
const elementRect = this.htmlElement.nativeElement.getBoundingClientRect();
const scroll = this.scrollEventService.getMouseScrollValue();
const elementTop = elementRect.top + scroll;
const areaTop = elementTop - this.triggerArea;
const areaBottom = elementTop + this.triggerArea;
return { top: areaTop, bottom: areaBottom };
}
/**
* Set element will-change property.
* @param {boolean} willChange - will-change enabled or disabled.
*/
setWillChange(willChange) {
this.willChange = willChange;
this.renderer.setStyle(this.htmlElement.nativeElement, 'will-change', willChange ? 'transform' : 'auto');
}
}
WillChangeDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: WillChangeDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: ScrollEventService }], target: i0.ɵɵFactoryTarget.Directive });
WillChangeDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "13.0.2", type: WillChangeDirective, selector: "[libWillChange]", inputs: { triggerArea: "triggerArea", isDisabled: "isDisabled" }, usesOnChanges: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: WillChangeDirective, decorators: [{
type: Directive,
args: [{
selector: '[libWillChange]'
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: ScrollEventService }]; }, propDecorators: { triggerArea: [{
type: Input
}], isDisabled: [{
type: Input
}] } });
class RfxParallaxComponent {
constructor(htmlElement, renderer, scrollEventService, resizeEventService, platformId) {
this.htmlElement = htmlElement;
this.renderer = renderer;
this.scrollEventService = scrollEventService;
this.resizeEventService = resizeEventService;
this.platformId = platformId;
this.parallaxPercentage = 40;
this.positionPercentage = 50;
this.imageZIndex = -1;
this.isDisabled = false;
this.visibleOverflow = false;
this.imageLeftPx = 0;
this.isLoaded = false;
this.forceFullWidth = false;
this.isBrowser = isPlatformBrowser(this.platformId);
}
/**
* Set container position and overflow.
* If parallax is not disabled, create listeners.
*/
ngOnInit() {
if (this.isBrowser) {
this.setContainerPosition(this.htmlElement.nativeElement);
this.setContainerOverflow(this.htmlElement.nativeElement, this.visibleOverflow);
if (!this.isDisabled) {
this.createListeners();
}
}
}
/**
* Update parallax values when some properies changed.
* - isDisabled change, enable or disable parallax and refresh image properties.
* - visibleOverflow change, show or hide image overflow on container without refresh.
* - imagerUrl, parallaxPercentage or positionPercentage change, refresh image properties.
* @param {SimpleChanges} changes - SimpleChanges object.
*/
ngOnChanges(changes) {
if (this.isBrowser) {
if (this.hasValueChanged(changes['isDisabled'], true)) {
this.destroyListeners();
}
else if (this.hasValueChanged(changes['isDisabled'], false)) {
this.createListeners();
}
if (this.hasValueChanged(changes['visibleOverflow'])) {
this.setContainerOverflow(this.htmlElement.nativeElement, changes['visibleOverflow'].currentValue);
}
if (this.image && ((this.hasValueChanged(changes['imageUrl']) ||
this.hasValueChanged(changes['parallaxPercentage']) ||
this.hasValueChanged(changes['positionPercentage'])) ||
this.hasValueChanged(changes['isDisabled']) ||
this.hasValueChanged(changes['forceFullWidth']))) {
this.setImageProperties(this.image);
}
}
}
ngOnDestroy() {
this.destroyListeners();
}
/**
* Check if SimpleChange value has changed.
* Eventually check if value corresponds to a new value.
* @param {SimpleChange} change - SimpleChange object.
* @param {any} newValue - New value.
* @returns {boolean} - true if value has changed.
*/
hasValueChanged(change, newValue = change === null || change === void 0 ? void 0 : change.currentValue) {
return (change === null || change === void 0 ? void 0 : change.firstChange) === false && (change === null || change === void 0 ? void 0 : change.currentValue) !== undefined && (change === null || change === void 0 ? void 0 : change.currentValue) === newValue;
}
/**
* Destroy all listeners.
*/
destroyListeners() {
var _a, _b;
(_a = this.scrollEventListener) === null || _a === void 0 ? void 0 : _a.unsubscribe();
(_b = this.resizeEventListener) === null || _b === void 0 ? void 0 : _b.unsubscribe();
}
/**
* Listen to scroll and resize events.
* Destroy alreay existing listeners.
*/
createListeners() {
this.destroyListeners();
this.scrollEventListener = this.scrollEventService.getMouseScroll().subscribe((scroll) => this.onMouseScroll(scroll));
this.resizeEventListener = this.resizeEventService.getResize().subscribe(() => this.onResize());
}
/**
* On mouse scroll event recalculate and change
* parallaxed image position.
* @param {number} scroll - Scroll value.
*/
onMouseScroll(scroll) {
if (this.parallaxBoundaries && this.image) {
const topPx = this.getImageTop(scroll, this.parallaxBoundaries);
this.setImageTransform(this.image, this.imageLeftPx, topPx);
}
}
/**
* On window resize event reload parallax properties.
*/
onResize() {
if (this.image) {
this.setImageProperties(this.image);
}
}
/**
* Set image object and loading status.
* Set image properties after image is fully loaded
* but only if platform is browser.
* @param {Event} event - Image loaded event.
*/
onImageLoaded(event) {
this.image = event.target;
if (this.isBrowser) {
this.setImageProperties(this.image);
}
this.isLoaded = true;
}
/**
* Set image properties needed for parallax effect.
* @param {HTMLImageElement} image - Image to be parallaxed.
*/
setImageProperties(image) {
const scrollTop = this.scrollEventService.getMouseScrollValue();
this.setImageSize(image, this.htmlElement.nativeElement.clientWidth, this.htmlElement.nativeElement.clientHeight, this.parallaxPercentage, this.isDisabled, this.forceFullWidth);
this.parallaxBoundaries = this.getParallaxBoundaries(scrollTop, this.htmlElement.nativeElement, image.height, this.parallaxPercentage);
this.imageLeftPx = this.getImageLeft(this.htmlElement.nativeElement.clientWidth, image.width, this.positionPercentage);
const topPx = this.isDisabled ?
this.getDisabledImageTop(this.parallaxBoundaries) :
this.getImageTop(scrollTop, this.parallaxBoundaries);
this.setImageTransform(image, this.imageLeftPx, topPx);
}
/**
* Set container position to relative
* if image is not already in relative or absolute position.
* @param {HTMLElement} container
*/
setContainerPosition(container) {
const position = getComputedStyle(container).position;
if (position !== 'relative' && position !== 'absolute') {
this.renderer.setStyle(container, 'position', 'relative');
}
}
/**
* Set container overflow.
* @param {HTMLElement} container - Container element.
* @param {boolean} isOverflowVisible - true to show container overflow.
*/
setContainerOverflow(container, isOverflowVisible) {
this.renderer.setStyle(container, 'overflow', isOverflowVisible ? 'visible' : 'hidden');
}
/**
* Set image size based on container size,
* parallax percentage and disabled state.
* @param {HTMLImageElement} image - Image element.
* @param {number} containerWidth - Container width.
* @param {number} containerHeight - Container height.
* @param {number} parallaxPercentage - Parallax percentage.
* @param {boolean} isDisabled - true to disable parallax effect.
* @param {boolean} forceFullWidth - true to force image size to container width.
*/
setImageSize(image, containerWidth, containerHeight, parallaxPercentage, isDisabled, forceFullWidth) {
if (forceFullWidth) {
this.renderer.setAttribute(image, 'width', `${containerWidth}px`);
this.renderer.setAttribute(image, 'height', `auto`);
}
else {
const minimumHeight = (containerHeight * (100 + (isDisabled ? 0 : parallaxPercentage))) / 100;
const ratio = image.naturalHeight / image.naturalWidth;
const minimumRatio = minimumHeight / containerWidth;
if (ratio > minimumRatio) {
this.renderer.setAttribute(image, 'width', `${containerWidth}px`);
this.renderer.setAttribute(image, 'height', `auto`);
}
else {
this.renderer.setAttribute(image, 'width', `auto`);
this.renderer.setAttribute(image, 'height', `${minimumHeight}px`);
}
}
}
/**
* Get parallax boundaries. This data is used
* to calculate parallax movement.
* @param {number} scrollTop - Scroll top.
* @param {HTMLElement} container - Container element.
* @param {number} imageHeight - Image height.
* @param {number} parallaxPercentage - Parallax percentage.
* @return {ParallaxBoundariesModel} - Parallax boundaries.
*/
getParallaxBoundaries(scrollTop, container, imageHeight, parallaxPercentage) {
const elementTop = container.getBoundingClientRect().top + scrollTop;
const usablePixels = container.clientHeight / 100 * parallaxPercentage;
const unusablePixels = imageHeight - container.clientHeight - usablePixels;
const startPoint = elementTop - usablePixels - window.innerHeight;
const endPoint = elementTop + container.clientHeight + usablePixels;
const totalPixels = endPoint - startPoint;
return { startPoint, endPoint, totalPixels, usablePixels, unusablePixels };
}
/**
* Get image left position.
* @param {number} containerWidth - Container width.
* @param {number} imageWidth - Image width.
* @param {number} positionPercentage - Position percentage.
* @return {number} - Image left position.
*/
getImageLeft(containerWidth, imageWidth, positionPercentage) {
return (containerWidth - imageWidth) / 100 * positionPercentage;
}
/**
* Get image top position.
* @param {number} scrollTop - Scroll top.
* @param {ParallaxBoundariesModel} boundaries - Parallax boundaries.
* @return {number} - Image top position.
*/
getImageTop(scrollTop, boundaries) {
const area = Math.max(0, Math.min(scrollTop - boundaries.startPoint, boundaries.totalPixels));
const areaPercentage = 100 / boundaries.totalPixels * area;
return -boundaries.usablePixels * (1 - areaPercentage / 100) - boundaries.unusablePixels / 2;
}
/**
* Get disabled image top position.
* @param {number} boundaries - Parallax boundaries.
* @return {number} - Image top position.
*/
getDisabledImageTop(boundaries) {
return (-boundaries.usablePixels - boundaries.unusablePixels) / 2;
}
/**
* Set image transform properties.
* @param {HTMLImageElement} image - Image element.
* @param {number} leftPx - Image left position.
* @param {number} topPx - Image top position.
*/
setImageTransform(image, leftPx, topPx) {
const newTranslate = `translate(${leftPx.toFixed(1)}px, ${topPx.toFixed(1)}px)`;
if (image.style.transform !== newTranslate) {
this.renderer.setStyle(image, 'transform', newTranslate);
}
}
}
RfxParallaxComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: ScrollEventService }, { token: ResizeEventService }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component });
RfxParallaxComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.0.2", type: RfxParallaxComponent, selector: "[libRfxParallax]", inputs: { imageUrl: "imageUrl", imageAlt: "imageAlt", parallaxPercentage: "parallaxPercentage", positionPercentage: "positionPercentage", imageZIndex: "imageZIndex", visibleOverflow: "visibleOverflow", isDisabled: "isDisabled", forceFullWidth: "forceFullWidth" }, usesOnChanges: true, ngImport: i0, template: "<img\r\n [alt]=\"imageAlt ?? ''\"\r\n [@visibility]=\"isLoaded\"\r\n libWillChange\r\n [isDisabled]=\"isDisabled || !isBrowser\"\r\n class=\"rfx-image\"\r\n [src]=\"imageUrl\"\r\n [style.zIndex]=\"imageZIndex\"\r\n (load)=\"onImageLoaded($event)\" />\r\n\r\n<ng-content></ng-content>\r\n", styles: [".rfx-image{position:absolute;left:0;top:0;will-change:transform;opacity:0}\n"], directives: [{ type: WillChangeDirective, selector: "[libWillChange]", inputs: ["triggerArea", "isDisabled"] }], animations: [
visibilityAnimation
] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxComponent, decorators: [{
type: Component,
args: [{ selector: '[libRfxParallax]', animations: [
visibilityAnimation
], template: "<img\r\n [alt]=\"imageAlt ?? ''\"\r\n [@visibility]=\"isLoaded\"\r\n libWillChange\r\n [isDisabled]=\"isDisabled || !isBrowser\"\r\n class=\"rfx-image\"\r\n [src]=\"imageUrl\"\r\n [style.zIndex]=\"imageZIndex\"\r\n (load)=\"onImageLoaded($event)\" />\r\n\r\n<ng-content></ng-content>\r\n", styles: [".rfx-image{position:absolute;left:0;top:0;will-change:transform;opacity:0}\n"] }]
}], ctorParameters: function () {
return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: ScrollEventService }, { type: ResizeEventService }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }];
}, propDecorators: { imageUrl: [{
type: Input
}], imageAlt: [{
type: Input
}], parallaxPercentage: [{
type: Input
}], positionPercentage: [{
type: Input
}], imageZIndex: [{
type: Input
}], visibleOverflow: [{
type: Input
}], isDisabled: [{
type: Input
}], forceFullWidth: [{
type: Input
}] } });
class RfxParallaxModule {
}
RfxParallaxModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
RfxParallaxModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, declarations: [RfxParallaxComponent,
WillChangeDirective], exports: [RfxParallaxComponent] });
RfxParallaxModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, imports: [[]] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, decorators: [{
type: NgModule,
args: [{
declarations: [
RfxParallaxComponent,
WillChangeDirective
],
imports: [],
exports: [
RfxParallaxComponent
]
}]
}] });
/*
* Public API Surface of rfx-parallax
*/
/**
* Generated bundle index. Do not edit.
*/
export { RfxParallaxComponent, RfxParallaxModule, RfxParallaxService };