From b2e34489c12bdc458489a3a8829a772da304871f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=87etin?=
<69278826+cetincakiroglu@users.noreply.github.com>
Date: Mon, 30 Oct 2023 17:04:10 +0300
Subject: [PATCH] Fixed #13970 - Add pAnimateOnScroll directive
---
.../animateonscroll/animateonscroll.ts | 168 +++++++++++++++
.../animateonscroll/ng-package.json | 6 +
.../components/animateonscroll/public_api.ts | 1 +
.../showcase/doc/animateonscroll/basicdoc.ts | 198 ++++++++++++++++++
.../showcase/doc/animateonscroll/importdoc.ts | 19 ++
src/app/showcase/doc/apidoc/index.json | 69 ++++++
src/app/showcase/layout/app-routing.module.ts | 2 +-
.../animateonscrolldemo-routing.module.ts | 9 +
.../pages/animate/animateonscrolldemo.html | 1 +
.../animate/animateonscrolldemo.module.ts | 11 +
.../pages/animate/animateonscrolldemo.ts | 27 +++
src/assets/showcase/data/menu.json | 4 +-
12 files changed, 512 insertions(+), 3 deletions(-)
create mode 100644 src/app/components/animateonscroll/animateonscroll.ts
create mode 100644 src/app/components/animateonscroll/ng-package.json
create mode 100644 src/app/components/animateonscroll/public_api.ts
create mode 100644 src/app/showcase/doc/animateonscroll/basicdoc.ts
create mode 100644 src/app/showcase/doc/animateonscroll/importdoc.ts
create mode 100755 src/app/showcase/pages/animate/animateonscrolldemo-routing.module.ts
create mode 100755 src/app/showcase/pages/animate/animateonscrolldemo.html
create mode 100755 src/app/showcase/pages/animate/animateonscrolldemo.module.ts
create mode 100755 src/app/showcase/pages/animate/animateonscrolldemo.ts
diff --git a/src/app/components/animateonscroll/animateonscroll.ts b/src/app/components/animateonscroll/animateonscroll.ts
new file mode 100644
index 00000000000..2df209ae062
--- /dev/null
+++ b/src/app/components/animateonscroll/animateonscroll.ts
@@ -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 {}
diff --git a/src/app/components/animateonscroll/ng-package.json b/src/app/components/animateonscroll/ng-package.json
new file mode 100644
index 00000000000..0e529e387d7
--- /dev/null
+++ b/src/app/components/animateonscroll/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public_api.ts"
+ }
+}
\ No newline at end of file
diff --git a/src/app/components/animateonscroll/public_api.ts b/src/app/components/animateonscroll/public_api.ts
new file mode 100644
index 00000000000..594bf70f981
--- /dev/null
+++ b/src/app/components/animateonscroll/public_api.ts
@@ -0,0 +1 @@
+export * from './animateonscroll';
diff --git a/src/app/showcase/doc/animateonscroll/basicdoc.ts b/src/app/showcase/doc/animateonscroll/basicdoc.ts
new file mode 100644
index 00000000000..0c006041ae9
--- /dev/null
+++ b/src/app/showcase/doc/animateonscroll/basicdoc.ts
@@ -0,0 +1,198 @@
+import { Component, Input } from '@angular/core';
+import { Code } from '../../domain/code';
+
+@Component({
+ selector: 'basic-doc',
+ template: ` Animation classes are defined with the enterClass and leaveClass properties. This example utilizes PrimeFlex animations however any valid CSS animation is supported.