diff --git a/components/lib/animateonscroll/AnimateOnScroll.d.ts b/components/lib/animateonscroll/AnimateOnScroll.d.ts index e472b2312..c49a20a47 100644 --- a/components/lib/animateonscroll/AnimateOnScroll.d.ts +++ b/components/lib/animateonscroll/AnimateOnScroll.d.ts @@ -23,6 +23,25 @@ export interface AnimateOnScrollOptions { * AnimateOnScroll scroll to add when item begins to get hidden. */ leaveClass?: string | undefined; + /** + * Specifies the `root` option of the IntersectionObserver API + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/IntersectionObserver/root) + */ + root?: Element | Document | null; + /** + * Specifies the `rootMargin` option of the IntersectionObserver API + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/IntersectionObserver/rootMargin) + */ + rootMargin?: string; + /** + * Specifies the `threshold` option of the IntersectionObserver API + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/IntersectionObserver/thresholds) + */ + threshold?: ReadonlyArray; + /** + * Whether the `enterClass` animation will run if the target is in the viewport when the page is loaded. + */ + animateOnLoad?: boolean; /** * Used to pass attributes to DOM elements inside the component. * @type {AnimateOnScrollDirectivePassThroughOptions} diff --git a/components/lib/animateonscroll/AnimateOnScroll.js b/components/lib/animateonscroll/AnimateOnScroll.js index 513c9216d..ace8894df 100644 --- a/components/lib/animateonscroll/AnimateOnScroll.js +++ b/components/lib/animateonscroll/AnimateOnScroll.js @@ -2,57 +2,102 @@ import { DomHandler } from 'primevue/utils'; import BaseAnimateOnScroll from './BaseAnimateOnScroll'; const AnimateOnScroll = BaseAnimateOnScroll.extend('animateonscroll', { - mounted(el, binding) { - el.setAttribute('data-pd-animateonscroll', true); - !this.isUnstyled() && DomHandler.addClass(el, 'p-animate'); + created() { + this.$value = this.$value || {}; + this.$el.style.opacity = this.$value.enterClass ? '0' : ''; + }, + mounted() { + this.$el.setAttribute('data-pd-animateonscroll', true); - this.bindIntersectionObserver(el, binding); + this.bindIntersectionObserver(); }, - unmounted(el) { - this.unbindIntersectionObserver(el); - clearTimeout(this.timeout); + unmounted() { + this.unbindAnimationEvents(); + this.unbindIntersectionObserver(); }, - timeout: null, - observer: null, + observer: undefined, + resetObserver: undefined, + isObserverActive: false, + animationState: undefined, + animationEndListener: undefined, methods: { - bindIntersectionObserver(el, binding) { - const options = { - root: null, - rootMargin: '0px', - threshold: 1.0 - }; + bindAnimationEvents() { + if (!this.animationEndListener) { + this.animationEndListener = () => { + DomHandler.removeMultipleClasses(this.$el, [this.$value.enterClass, this.$value.leaveClass]); + !this.$modifiers.once && this.resetObserver.observe(this.$el); + this.unbindAnimationEvents(); + }; - this.observer = new IntersectionObserver((element) => this.isVisible(element, el, binding), options); - this.observer.observe(el); - }, - isVisible(target, el, binding) { - const [intersectionObserverEntry] = target; - - intersectionObserverEntry.isIntersecting ? this.enter(el, binding) : this.leave(el, binding); - }, - enter(el, binding) { - el.style.visibility = 'visible'; - DomHandler.addMultipleClasses(el, binding.value.enterClass); - - binding.modifiers.once && this.unbindIntersectionObserver(el); - }, - leave(el, binding) { - DomHandler.removeClass(el, binding.value.enterClass); - - if (binding.value.leaveClass) { - DomHandler.addMultipleClasses(el, binding.value.leaveClass); + this.$el.addEventListener('animationend', this.animationEndListener); } - - const animationDuration = el.style.animationDuration || 500; - - this.timeout = setTimeout(() => { - el.style.visibility = 'hidden'; - }, animationDuration); }, - unbindIntersectionObserver(el) { - if (this.observer) { - this.observer.unobserve(el); + bindIntersectionObserver() { + const { root, rootMargin, threshold = 0.5 } = this.$value; + const options = { root, rootMargin, threshold }; + + // States + this.observer = new IntersectionObserver(([entry]) => { + if (this.isObserverActive) { + if (entry.boundingClientRect.top > 0) { + entry.isIntersecting ? this.enter() : this.leave(); + } + } else if (entry.isIntersecting) { + this.$value.animateOnLoad ? this.enter() : (this.$el.style.opacity = ''); + } + + this.isObserverActive = true; + }, options); + + setTimeout(() => this.observer.observe(this.$el), 0); + + // Reset + this.resetObserver = new IntersectionObserver( + ([entry]) => { + if (entry.boundingClientRect.top > 0 && !entry.isIntersecting) { + this.$el.style.opacity = this.$value.enterClass ? '0' : ''; + DomHandler.removeMultipleClasses(this.$el, [this.$value.enterClass, this.$value.leaveClass]); + + this.resetObserver.unobserve(this.$el); + } + + this.animationState = undefined; + }, + { ...options, threshold: 0 } + ); + }, + enter() { + if (this.animationState !== 'enter' && this.$value.enterClass) { + this.$el.style.opacity = ''; + DomHandler.removeMultipleClasses(this.$el, this.$value.leaveClass); + DomHandler.addMultipleClasses(this.$el, this.$value.enterClass); + + this.$modifiers.once && this.unbindIntersectionObserver(this.$el); + + this.bindAnimationEvents(); + this.animationState = 'enter'; } + }, + leave() { + if (this.animationState !== 'leave' && this.$value.leaveClass) { + this.$el.style.opacity = this.$value.enterClass ? '0' : ''; + DomHandler.removeMultipleClasses(this.$el, this.$value.enterClass); + DomHandler.addMultipleClasses(this.$el, this.$value.leaveClass); + + this.bindAnimationEvents(); + this.animationState = 'leave'; + } + }, + unbindAnimationEvents() { + if (this.animationEndListener) { + this.$el.removeEventListener('animationend', this.animationEndListener); + this.animationEndListener = undefined; + } + }, + unbindIntersectionObserver() { + this.observer?.unobserve(this.$el); + this.resetObserver?.unobserve(this.$el); + this.isObserverActive = false; } } }); diff --git a/components/lib/basedirective/BaseDirective.js b/components/lib/basedirective/BaseDirective.js index fb51257f3..4e209259c 100644 --- a/components/lib/basedirective/BaseDirective.js +++ b/components/lib/basedirective/BaseDirective.js @@ -94,7 +94,9 @@ const BaseDirective = { $name: name, $host: el, $binding: binding, - $el: $prevInstance['$el'] || undefined, + $modifiers: binding?.modifiers, + $value: binding?.value, + $el: $prevInstance['$el'] || el || undefined, $style: { classes: undefined, inlineStyles: undefined, loadStyle: () => {}, ...options?.style }, $config: config, /* computed instance variables */ diff --git a/components/lib/utils/DomHandler.js b/components/lib/utils/DomHandler.js index d1f5a9b6b..f1dc0e175 100755 --- a/components/lib/utils/DomHandler.js +++ b/components/lib/utils/DomHandler.js @@ -126,9 +126,21 @@ export default { return -1; }, - addMultipleClasses(element, className) { - if (element && className) { - className.split(' ').forEach((style) => this.addClass(element, style)); + addMultipleClasses(element, classNames) { + if (element && classNames) { + [classNames] + .flat() + .filter(Boolean) + .forEach((cNames) => cNames.split(' ').forEach((className) => this.addClass(element, className))); + } + }, + + removeMultipleClasses(element, classNames) { + if (element && classNames) { + [classNames] + .flat() + .filter(Boolean) + .forEach((cNames) => cNames.split(' ').forEach((className) => this.removeClass(element, className))); } }, diff --git a/components/lib/utils/Utils.d.ts b/components/lib/utils/Utils.d.ts index e9b83bffd..01c9feb8b 100644 --- a/components/lib/utils/Utils.d.ts +++ b/components/lib/utils/Utils.d.ts @@ -16,7 +16,8 @@ export declare class DomHandler { static getViewport(): { width: number; height: number }; static getOffset(el: HTMLElement): { top: any; left: any }; static index(el: HTMLElement): number; - static addMultipleClasses(el: HTMLElement, className: string): void; + static addMultipleClasses(el: HTMLElement, classNames: string | string[]): void; + static addRemoveClasses(el: HTMLElement, classNames: string | string[]): void; static addClass(el: HTMLElement, className: string): void; static removeClass(el: HTMLElement, className: string): void; static hasClass(el: HTMLElement, className: string): boolean;