diff --git a/src/core/directives/on-appear.ts b/src/core/directives/on-appear.ts index c894878dc..d3e9bd2da 100644 --- a/src/core/directives/on-appear.ts +++ b/src/core/directives/on-appear.ts @@ -37,7 +37,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy { * @inheritdoc */ async ngOnInit(): Promise { - this.visiblePromise = CoreDomUtils.waitToBeVisible(this.element); + this.visiblePromise = CoreDomUtils.waitToBeInViewport(this.element); await this.visiblePromise; diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 65f00c7d3..2df0fcfc1 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -141,6 +141,7 @@ export class CoreDomUtilsProvider { let interval: number | undefined; + // Mutations did not observe for visibility properties. return new CoreCancellablePromise( async (resolve) => { await domPromise; @@ -165,6 +166,60 @@ export class CoreDomUtilsProvider { ); } + /** + * Wait an element to be in dom and visible. + * + * @param element Element to wait. + * @param intersectionRatio Intersection ratio (From 0 to 1). + * @return Cancellable promise. + */ + waitToBeInViewport(element: HTMLElement, intersectionRatio = 1): CoreCancellablePromise { + const visiblePromise = CoreDomUtils.waitToBeVisible(element); + + let intersectionObserver: IntersectionObserver; + let interval: number | undefined; + + return new CoreCancellablePromise( + async (resolve) => { + await visiblePromise; + + if (CoreDomUtils.isElementInViewport(element, intersectionRatio)) { + + return resolve(); + } + + if ('IntersectionObserver' in window) { + intersectionObserver = new IntersectionObserver((observerEntries) => { + const isIntersecting = observerEntries + .some((entry) => entry.isIntersecting && entry.intersectionRatio >= intersectionRatio); + if (!isIntersecting) { + return; + } + + resolve(); + intersectionObserver?.disconnect(); + }); + + intersectionObserver.observe(element); + } else { + interval = window.setInterval(() => { + if (!CoreDomUtils.isElementInViewport(element, intersectionRatio)) { + return; + } + + resolve(); + window.clearInterval(interval); + }, 50); + } + }, + () => { + visiblePromise.cancel(); + intersectionObserver?.disconnect(); + window.clearInterval(interval); + }, + ); + } + /** * Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much. * This function helps setting up the debounce feature and remove listener easily. @@ -870,10 +925,21 @@ export class CoreDomUtilsProvider { return elementPoint > window.innerHeight || elementPoint < scrollTopPos; } + /** + * Check whether an element has been added to the DOM. + * + * @param element Element. + * @return True if element has been added to the DOM, false otherwise. + */ + isElementInDom(element: HTMLElement): boolean { + return element.getRootNode({ composed: true }) === document; + } + /** * Check whether an element is visible or not. * * @param element Element. + * @return True if element is visible inside the DOM. */ isElementVisible(element: HTMLElement): boolean { if (element.clientWidth === 0 || element.clientHeight === 0) { @@ -885,7 +951,35 @@ export class CoreDomUtilsProvider { return false; } - return element.offsetParent !== null; + return CoreDomUtils.isElementInDom(element); + } + + /** + * Check whether an element is intersecting the intersectionRatio in viewport. + * + * @param element + * @param intersectionRatio Intersection ratio (From 0 to 1). + * @return True if in viewport. + */ + isElementInViewport(element: HTMLElement, intersectionRatio = 1): boolean { + const elementRectangle = element.getBoundingClientRect(); + + const elementArea = elementRectangle.width * elementRectangle.height; + if (elementArea == 0) { + return false; + } + + const intersectionRectangle = { + top: Math.max(0, elementRectangle.top), + left: Math.max(0, elementRectangle.left), + bottom: Math.min(window.innerHeight, elementRectangle.bottom), + right: Math.min(window.innerWidth, elementRectangle.right), + }; + + const intersectionArea = (intersectionRectangle.right - intersectionRectangle.left) * + (intersectionRectangle.bottom - intersectionRectangle.top); + + return intersectionArea / elementArea >= intersectionRatio; } /**