Skip to content

Commit

Permalink
Merge pull request #13980 from primefaces/issue-13970
Browse files Browse the repository at this point in the history
Issue 13970
  • Loading branch information
cetincakiroglu authored Nov 1, 2023
2 parents 14eee0e + ae09220 commit d5ce73f
Show file tree
Hide file tree
Showing 15 changed files with 567 additions and 0 deletions.
172 changes: 172 additions & 0 deletions src/app/components/animateonscroll/animateonscroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
import { AfterViewInit, Directive, ElementRef, Input, NgModule, Renderer2, OnInit, Inject, PLATFORM_ID } 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(@Inject(DOCUMENT) private document: Document, @Inject(PLATFORM_ID) private platformId: any, private host: ElementRef, public el: ElementRef, public renderer: Renderer2) {}

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

ngAfterViewInit() {
if(isPlatformBrowser(this.platformId)){
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';
9 changes: 9 additions & 0 deletions src/app/components/dom/domhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ export class DomHandler {
}
}

public static removeMultipleClasses(element, classNames) {
if (element && classNames) {
[classNames]
.flat()
.filter(Boolean)
.forEach((cNames) => cNames.split(' ').forEach((className) => this.removeClass(element, className)));
}
}

public static hasClass(element: any, className: string): boolean {
if (element && className) {
if (element.classList) return element.classList.contains(className);
Expand Down
24 changes: 24 additions & 0 deletions src/app/showcase/doc/animateonscroll/accessibilitydoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Component, Input } from '@angular/core';

@Component({
selector: 'accessibility-doc',
template: `
<div>
<app-docsectiontext [title]="title" [id]="id">
<h3>Screen Reader</h3>
<p>
AnimateOnScroll does not require any roles and attributes.
</p>
<h3>Keyboard Support</h3>
<p>
Component does not include any interactive elements.
</p>
</app-docsectiontext>
</div>`
})
export class AccessibilityDoc {
@Input() id: string;

@Input() title: string;

}
16 changes: 16 additions & 0 deletions src/app/showcase/doc/animateonscroll/animateonscrolldoc.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppDocModule } from '../../layout/doc/app.doc.module';
import { AppCodeModule } from '../../layout/doc/code/app.code.component';
import { ImportDoc } from './importdoc';
import { BasicDoc } from './basicdoc';
import { AccessibilityDoc } from './accessibilitydoc';
import { AnimateOnScrollModule } from 'primeng/animateonscroll';

@NgModule({
imports: [CommonModule, RouterModule, AppCodeModule, AppDocModule, AnimateOnScrollModule],
declarations: [ImportDoc, BasicDoc, AccessibilityDoc],
exports: [AppDocModule]
})
export class AnimateOnScrollDocModule {}
Loading

1 comment on commit d5ce73f

@vercel
Copy link

@vercel vercel bot commented on d5ce73f Nov 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.