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.
+
+
+
+ Scroll Down
+
+
+
+
+
+
+ fade-in
+
+
+
+ fade-left
+
+
+
+ fade-right
+
+
+
+ zoom
+
+
+
+ flip-left
+
+
+
+ flip-y
+
+
+
+ scalein
+
+
+
+ `,
+ 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: `
+
+ Scroll Down
+
+
+
+
+
+
+ fade-in
+
+
+
+ fade-left
+
+
+
+ fade-right
+
+
+
+ zoom
+
+
+
+ flip-left
+
+
+
+ flip-y
+
+
+
+ scalein
+
`,
+ html: `
+
+
+ Scroll Down
+
+
+
+
+
+
+ fade-in
+
+
+
+ fade-left
+
+
+
+ fade-right
+
+
+
+ zoom
+
+
+
+ flip-left
+
+
+
+ flip-y
+
+
+
+ scalein
+
+
`,
+ 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 {}`
+ };
+}
diff --git a/src/app/showcase/doc/animateonscroll/importdoc.ts b/src/app/showcase/doc/animateonscroll/importdoc.ts
new file mode 100644
index 00000000000..f4264dd08c7
--- /dev/null
+++ b/src/app/showcase/doc/animateonscroll/importdoc.ts
@@ -0,0 +1,19 @@
+import { Component, Input } from '@angular/core';
+import { Code } from '../../domain/code';
+
+@Component({
+ selector: 'import-doc',
+ template: ` `
+})
+export class ImportDoc {
+ @Input() id: string;
+
+ @Input() title: string;
+
+ code: Code = {
+ typescript: `import { AnimateOnScrollModule } from 'primeng/animateonscroll';`
+ };
+}
diff --git a/src/app/showcase/doc/apidoc/index.json b/src/app/showcase/doc/apidoc/index.json
index 940d29058f2..74644a6efe2 100644
--- a/src/app/showcase/doc/apidoc/index.json
+++ b/src/app/showcase/doc/apidoc/index.json
@@ -332,6 +332,61 @@
}
}
},
+ "animateonscroll": {
+ "components": {
+ "AnimateOnScroll": {
+ "description": "Animate manages PrimeFlex CSS classes declaratively to during enter/leave animations on scroll or on page load.",
+ "props": {
+ "description": "Defines the input properties of the component.",
+ "values": [
+ {
+ "name": "enterClass",
+ "optional": false,
+ "readonly": false,
+ "type": "string",
+ "description": "Selector to define the CSS class for enter animation."
+ },
+ {
+ "name": "leaveClass",
+ "optional": false,
+ "readonly": false,
+ "type": "string",
+ "description": "Selector to define the CSS class for leave animation."
+ },
+ {
+ "name": "root",
+ "optional": false,
+ "readonly": false,
+ "type": "HTMLElement",
+ "description": "Specifies the root option of the IntersectionObserver API."
+ },
+ {
+ "name": "rootMargin",
+ "optional": false,
+ "readonly": false,
+ "type": "string",
+ "description": "Specifies the rootMargin option of the IntersectionObserver API."
+ },
+ {
+ "name": "threshold",
+ "optional": false,
+ "readonly": false,
+ "type": "number",
+ "description": "Specifies the threshold option of the IntersectionObserver API"
+ },
+ {
+ "name": "once",
+ "optional": false,
+ "readonly": false,
+ "type": "boolean",
+ "default": "true",
+ "description": "Whether the scroll event listener should be removed after initial run."
+ }
+ ]
+ }
+ }
+ }
+ },
"blockableui": {
"components": {}
},
@@ -2312,6 +2367,13 @@
"readonly": false,
"type": "number",
"description": "Time to wait in milliseconds to hide the tooltip even it is active."
+ },
+ {
+ "name": "id",
+ "optional": true,
+ "readonly": false,
+ "type": "string",
+ "description": "When present, it adds a custom id to the tooltip."
}
]
}
@@ -11753,6 +11815,13 @@
"type": "string",
"description": "Attribute of the image element."
},
+ {
+ "name": "loading",
+ "optional": false,
+ "readonly": false,
+ "type": "\"eager\" | \"lazy\"",
+ "description": "Attribute of the image element."
+ },
{
"name": "appendTo",
"optional": false,
diff --git a/src/app/showcase/layout/app-routing.module.ts b/src/app/showcase/layout/app-routing.module.ts
index a1017a986a9..d7f4225123f 100644
--- a/src/app/showcase/layout/app-routing.module.ts
+++ b/src/app/showcase/layout/app-routing.module.ts
@@ -115,7 +115,7 @@ const routes: Routes = [
{ path: 'uikit', loadChildren: () => import('../pages/uikit/uikit.module').then((m) => m.UIKitModule) },
{ path: 'autofocus', loadChildren: () => import('../pages/autofocus/autofocusdemo.module').then((m) => m.AutoFocusDemoModule) },
{ path: 'overlay', loadChildren: () => import('../pages/overlay/overlaydemo.module').then((m) => m.OverlayDemoModule) },
- { path: 'animate', loadChildren: () => import('../pages/animate/animatedemo.module').then((m) => m.AnimateDemoModule) },
+ { path: 'animateonscroll', loadChildren: () => import('../pages/animate/animateonscrolldemo.module').then((m) => m.AnimateOnScrollDemoModule) },
{ path: 'templates', loadChildren: () => import('../pages/templates/templates.module').then((m) => m.TemplatesModule) }
]
},
diff --git a/src/app/showcase/pages/animate/animateonscrolldemo-routing.module.ts b/src/app/showcase/pages/animate/animateonscrolldemo-routing.module.ts
new file mode 100755
index 00000000000..b4907cf8138
--- /dev/null
+++ b/src/app/showcase/pages/animate/animateonscrolldemo-routing.module.ts
@@ -0,0 +1,9 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { AnimateOnScrollDemo } from './animateonscrolldemo';
+
+@NgModule({
+ imports: [RouterModule.forChild([{ path: '', component: AnimateOnScrollDemo }])],
+ exports: [RouterModule]
+})
+export class AnimateOnScrollDemoRoutingModule {}
diff --git a/src/app/showcase/pages/animate/animateonscrolldemo.html b/src/app/showcase/pages/animate/animateonscrolldemo.html
new file mode 100755
index 00000000000..4724daef07c
--- /dev/null
+++ b/src/app/showcase/pages/animate/animateonscrolldemo.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/app/showcase/pages/animate/animateonscrolldemo.module.ts b/src/app/showcase/pages/animate/animateonscrolldemo.module.ts
new file mode 100755
index 00000000000..6799a65e560
--- /dev/null
+++ b/src/app/showcase/pages/animate/animateonscrolldemo.module.ts
@@ -0,0 +1,11 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { AnimateOnScrollDocModule } from '../../doc/animateonscroll/animateonscrolldoc.module';
+import { AnimateOnScrollDemoRoutingModule } from './animateonscrolldemo-routing.module';
+import { AnimateOnScrollDemo } from './animateonscrolldemo';
+
+@NgModule({
+ imports: [CommonModule, AnimateOnScrollDemoRoutingModule, AnimateOnScrollDocModule],
+ declarations: [AnimateOnScrollDemo]
+})
+export class AnimateOnScrollDemoModule {}
diff --git a/src/app/showcase/pages/animate/animateonscrolldemo.ts b/src/app/showcase/pages/animate/animateonscrolldemo.ts
new file mode 100755
index 00000000000..299d2d4aaa3
--- /dev/null
+++ b/src/app/showcase/pages/animate/animateonscrolldemo.ts
@@ -0,0 +1,27 @@
+import { Component } from '@angular/core';
+import { ImportDoc } from '../../doc/animateonscroll/importdoc';
+import { BasicDoc } from '../../doc/animateonscroll/basicdoc';
+import { AccessibilityDoc } from '../../doc/animateonscroll/accessibilitydoc';
+
+@Component({
+ templateUrl: './animateonscrolldemo.html'
+})
+export class AnimateOnScrollDemo {
+ docs = [
+ {
+ id: 'import',
+ label: 'Import',
+ component: ImportDoc
+ },
+ {
+ id: 'basic',
+ label: 'Basic',
+ component: BasicDoc
+ },
+ {
+ id: 'accessibility',
+ label: 'Accessibility',
+ component: AccessibilityDoc
+ }
+ ];
+}
diff --git a/src/assets/showcase/data/menu.json b/src/assets/showcase/data/menu.json
index 46e4e1c918f..8db1d0711c0 100644
--- a/src/assets/showcase/data/menu.json
+++ b/src/assets/showcase/data/menu.json
@@ -447,8 +447,8 @@
"routerLink": "/autofocus"
},
{
- "name": "Animate",
- "routerLink": "/animate"
+ "name": "AnimateOnScroll",
+ "routerLink": "/animateonscroll"
}
]
},