Skip to content

Commit

Permalink
Fixed #13970 - Add pAnimateOnScroll directive
Browse files Browse the repository at this point in the history
  • Loading branch information
cetincakiroglu committed Oct 30, 2023
1 parent ec01830 commit b2e3448
Show file tree
Hide file tree
Showing 12 changed files with 512 additions and 3 deletions.
168 changes: 168 additions & 0 deletions src/app/components/animateonscroll/animateonscroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { CommonModule } from '@angular/common';
import { AfterViewInit, Directive, ElementRef, Input, NgModule, Renderer2, OnInit } from '@angular/core';
import { DomHandler } from 'primeng/dom';

interface AnimateOnScrollOptions {
root?: HTMLElement;
rootMargin?: string;
threshold?: number;
}

/**
* AnimateOnScroll is used to apply animations to elements when entering or leaving the viewport during scrolling.
* @group Components
*/
@Directive({
selector: '[pAnimateOnScroll]',
host: {
'[class.p-animateonscroll]': 'true'
}
})
export class AnimateOnScroll implements OnInit, AfterViewInit {
/**
* Selector to define the CSS class for enter animation.
* @group Props
*/
@Input() enterClass: string | undefined;
/**
* Selector to define the CSS class for leave animation.
* @group Props
*/
@Input() leaveClass: string | undefined;
/**
* Specifies the root option of the IntersectionObserver API.
* @group Props
*/
@Input() root: HTMLElement | undefined | null;
/**
* Specifies the rootMargin option of the IntersectionObserver API.
* @group Props
*/
@Input() rootMargin: string | undefined;
/**
* Specifies the threshold option of the IntersectionObserver API
* @group Props
*/
@Input() threshold: number | undefined;
/**
* Whether the scroll event listener should be removed after initial run.
* @group Props
*/
@Input() once: boolean = true;

observer: IntersectionObserver | undefined;

resetObserver: any;

isObserverActive: boolean = false;

animationState: any;

animationEndListener: VoidFunction | undefined;

constructor(private host: ElementRef, public el: ElementRef, public renderer: Renderer2) {}

ngOnInit() {
this.renderer.setStyle(this.host.nativeElement, 'opacity', this.enterClass ? '0' : '');
}

ngAfterViewInit() {
this.bindIntersectionObserver();
}

get options(): AnimateOnScrollOptions {
return {
root: this.root,
rootMargin: this.rootMargin,
threshold: this.threshold
}
}

bindIntersectionObserver() {
this.observer = new IntersectionObserver(([entry]) => {
if(this.isObserverActive) {
if(entry.boundingClientRect.top > 0) {
entry.isIntersecting ? this.enter() : this.leave();
}
} else if(entry.isIntersecting) {
this.enter();
}

this.isObserverActive = true;
}, this.options);

setTimeout(() => this.observer.observe(this.host.nativeElement), 0);

// Reset

this.resetObserver = new IntersectionObserver(([entry]) => {
if (entry.boundingClientRect.top > 0 && !entry.isIntersecting) {
this.host.nativeElement.style.opacity = this.enterClass ? '0' : '';
DomHandler.removeMultipleClasses(this.host.nativeElement, [this.enterClass, this.leaveClass]);

this.resetObserver.unobserve(this.host.nativeElement);
}

this.animationState = undefined;
}, {...this.options, threshold: 0})
}

enter() {
if (this.animationState !== 'enter' && this.enterClass) {
this.host.nativeElement.style.opacity = '';
DomHandler.removeMultipleClasses(this.host.nativeElement, this.leaveClass);
DomHandler.addMultipleClasses(this.host.nativeElement, this.enterClass);

this.once && this.unbindIntersectionObserver();

this.bindAnimationEvents();
this.animationState = 'enter';
}
}

leave() {
if (this.animationState !== 'leave' && this.leaveClass) {
this.host.nativeElement.style.opacity = this.enterClass ? '0' : '';
DomHandler.removeMultipleClasses(this.host.nativeElement, this.enterClass);
DomHandler.addMultipleClasses(this.host.nativeElement, this.leaveClass);

this.bindAnimationEvents();
this.animationState = 'leave';
}
}

bindAnimationEvents() {
if (!this.animationEndListener) {
this.animationEndListener = this.renderer.listen(this.host.nativeElement, 'animationend', () => {
DomHandler.removeMultipleClasses(this.host.nativeElement, [this.enterClass, this.leaveClass]);
!this.once && this.resetObserver.observe(this.host.nativeElement);
this.unbindAnimationEvents();
})
}
}

unbindAnimationEvents() {
if (this.animationEndListener) {
this.animationEndListener();
this.animationEndListener = null;
}
}

unbindIntersectionObserver() {
this.observer?.unobserve(this.host.nativeElement);
this.resetObserver?.unobserve(this.host.nativeElement);
this.isObserverActive = false;
}

ngOnDestroy() {
this.unbindAnimationEvents();
this.unbindIntersectionObserver();
}
}

@NgModule({
imports: [CommonModule],
exports: [AnimateOnScroll],
declarations: [AnimateOnScroll]
})
export class AnimateOnScrollModule {}
6 changes: 6 additions & 0 deletions src/app/components/animateonscroll/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public_api.ts"
}
}
1 change: 1 addition & 0 deletions src/app/components/animateonscroll/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './animateonscroll';
198 changes: 198 additions & 0 deletions src/app/showcase/doc/animateonscroll/basicdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Component, Input } from '@angular/core';
import { Code } from '../../domain/code';

@Component({
selector: 'basic-doc',
template: ` <section class="py-4">
<app-docsectiontext [title]="title" [id]="id">
<p>Animation classes are defined with the <i>enterClass</i> and <i>leaveClass</i> properties. This example utilizes PrimeFlex animations however any valid CSS animation is supported.</p>
</app-docsectiontext>
<div class="card flex flex-column align-items-center overflow-hidden">
<div class="flex flex-column align-items-center gap-2">
<span class="text-xl font-medium">Scroll Down</span>
<span class="slidedown-icon h-2rem w-2rem bg-primary border-circle inline-flex align-items-center justify-content-center">
<i class="pi pi-arrow-down"></i>
</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadein" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000">
<span class="text-3xl font-bold">fade-in</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadeinleft" leaveClass="fadeoutleft" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">fade-left</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadeinright" leaveClass="fadeoutright" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">fade-right</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="zoomin" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000">
<span class="text-3xl font-bold">zoom</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="flipleft" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">flip-left</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="flipup" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">flip-y</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="scalein" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">scalein</span>
</div>
</div>
<app-code [code]="code" selector="animate-on-scroll-basic-demo"></app-code>
</section>`,
styles: [
`
:host {
@keyframes slidedown-icon {
0% {
transform: translateY(0);
}
50% {
transform: translateY(20px);
}
100% {
transform: translateY(0);
}
}
.slidedown-icon {
animation: slidedown-icon;
animation-duration: 3s;
animation-iteration-count: infinite;
}
.box {
background-image: radial-gradient(var(--primary-300), var(--primary-600));
border-radius: 50% !important;
color: var(--primary-color-text);
}
}
`
]
})
export class BasicDoc {
@Input() id: string;

@Input() title: string;

code: Code = {
basic: `
<div class="flex flex-column align-items-center gap-2">
<span class="text-xl font-medium">Scroll Down</span>
<span class="slidedown-icon h-2rem w-2rem bg-primary border-circle inline-flex align-items-center justify-content-center">
<i class="pi pi-arrow-down"></i>
</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadein" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000">
<span class="text-3xl font-bold">fade-in</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadeinleft" leaveClass="fadeoutleft" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">fade-left</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadeinright" leaveClass="fadeoutright" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">fade-right</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="zoomin" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000">
<span class="text-3xl font-bold">zoom</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="flipleft" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">flip-left</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="flipup" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">flip-y</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="scalein" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">scalein</span>
</div>`,
html: `
<div class="card flex flex-column align-items-center overflow-hidden">
<div class="flex flex-column align-items-center gap-2">
<span class="text-xl font-medium">Scroll Down</span>
<span class="slidedown-icon h-2rem w-2rem bg-primary border-circle inline-flex align-items-center justify-content-center">
<i class="pi pi-arrow-down"></i>
</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadein" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000">
<span class="text-3xl font-bold">fade-in</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadeinleft" leaveClass="fadeoutleft" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">fade-left</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="fadeinright" leaveClass="fadeoutright" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">fade-right</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="zoomin" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000">
<span class="text-3xl font-bold">zoom</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="flipleft" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">flip-left</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="flipup" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">flip-y</span>
</div>
<div class="h-30rem"></div>
<div pAnimateOnScroll enterClass="scalein" leaveClass="fadeout" class="flex box shadow-4 justify-content-center align-items-center h-10rem w-10rem sm:h-15rem sm:w-15rem border-round animation-duration-1000 animation-ease-in-out">
<span class="text-3xl font-bold">scalein</span>
</div>
</div>`,
typescript: `
import { Component } from '@angular/core';
@Component({
selector: 'animate-on-scroll-basic-demo',
templateUrl: './animate-on-scroll-basic-demo.html',
styles: [
\`
:host {
@keyframes slidedown-icon {
0% {
transform: translateY(0);
}
50% {
transform: translateY(20px);
}
100% {
transform: translateY(0);
}
}
.slidedown-icon {
animation: slidedown-icon;
animation-duration: 3s;
animation-iteration-count: infinite;
}
.box {
background-image: radial-gradient(var(--primary-300), var(--primary-600));
border-radius: 50% !important;
color: var(--primary-color-text);
}
}
\`
]
})
export class AnimateOnScrollBasicDemo {}`
};
}
19 changes: 19 additions & 0 deletions src/app/showcase/doc/animateonscroll/importdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { Code } from '../../domain/code';

@Component({
selector: 'import-doc',
template: ` <section class="py-4">
<app-docsectiontext [title]="title" [id]="id"> </app-docsectiontext>
<app-code [code]="code" [hideToggleCode]="true"></app-code>
</section>`
})
export class ImportDoc {
@Input() id: string;

@Input() title: string;

code: Code = {
typescript: `import { AnimateOnScrollModule } from 'primeng/animateonscroll';`
};
}
Loading

0 comments on commit b2e3448

Please sign in to comment.