From 2f59ab97071640ea1a70404255295431711803ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Tue, 25 Jul 2023 16:17:54 +0200 Subject: [PATCH] fix: parallax replacer might fail to find shadow root on its first try --- .../util/ion-content-parallax.directive.ts | 53 +++++++++++-------- .../app/src/app/util/ion-icon/replace-util.ts | 19 ++----- .../src/app/util/ion-icon/shadow-attacher.ts | 23 ++++++++ .../app/util/section/section-link-card.html | 0 .../section/section-tail-prompt-card.html | 0 5 files changed, 59 insertions(+), 36 deletions(-) create mode 100644 frontend/app/src/app/util/ion-icon/shadow-attacher.ts delete mode 100644 frontend/app/src/app/util/section/section-link-card.html delete mode 100644 frontend/app/src/app/util/section/section-tail-prompt-card.html diff --git a/frontend/app/src/app/util/ion-content-parallax.directive.ts b/frontend/app/src/app/util/ion-content-parallax.directive.ts index 1182e386..2eca9fed 100644 --- a/frontend/app/src/app/util/ion-content-parallax.directive.ts +++ b/frontend/app/src/app/util/ion-content-parallax.directive.ts @@ -13,6 +13,7 @@ * this program. If not, see . */ import {Directive, ElementRef, HostBinding, Input, OnDestroy, OnInit} from '@angular/core'; +import {waitForElement} from './ion-icon/shadow-attacher'; type IonicColor = | 'danger' @@ -86,31 +87,39 @@ export class IonContentParallaxDirective implements OnInit, OnDestroy { constructor(private element: ElementRef) {} - ngOnInit() { - this.mutationObserver = new MutationObserver(() => { - const inner = this.element.nativeElement.shadowRoot.querySelector('.inner-scroll') as - | HTMLDivElement - | undefined; - if (!inner) return; - - // eslint-disable-next-line unicorn/no-array-for-each - inner.childNodes.forEach(node => node.remove()); - - inner.part.add('parallax-scroll'); - const parallaxParentElement = document.createElement('div'); - parallaxParentElement.part.add('parallax-parent'); - const parallaxElement = document.createElement('div'); - parallaxElement.part.add('parallax'); - - inner.append(parallaxParentElement); - parallaxParentElement.append(parallaxElement, document.createElement('slot')); - }); - this.mutationObserver.observe(this.element.nativeElement.shadowRoot, { - childList: true, - }); + async ngOnInit() { + this.mutationObserver = new MutationObserver(this.replace.bind(this)); + const element = this.element.nativeElement; + if (element.shadowRoot) { + this.mutationObserver.observe(element.shadowRoot, {childList: true}); + } else { + console.warn("Shadow root didn't exist for parallax, retrying..."); + await waitForElement(() => this.element.nativeElement.shadowRoot); + this.mutationObserver.observe(element.shadowRoot, {childList: true}); + this.replace(); + } } ngOnDestroy() { this.mutationObserver.disconnect(); } + + replace() { + const inner = this.element.nativeElement.shadowRoot.querySelector('.inner-scroll') as + | HTMLDivElement + | undefined; + if (!inner) return; + + // eslint-disable-next-line unicorn/no-array-for-each + inner.childNodes.forEach(node => node.remove()); + + inner.part.add('parallax-scroll'); + const parallaxParentElement = document.createElement('div'); + parallaxParentElement.part.add('parallax-parent'); + const parallaxElement = document.createElement('div'); + parallaxElement.part.add('parallax'); + + inner.append(parallaxParentElement); + parallaxParentElement.append(parallaxElement, document.createElement('slot')); + } } diff --git a/frontend/app/src/app/util/ion-icon/replace-util.ts b/frontend/app/src/app/util/ion-icon/replace-util.ts index bda28d00..cdadf44a 100644 --- a/frontend/app/src/app/util/ion-icon/replace-util.ts +++ b/frontend/app/src/app/util/ion-icon/replace-util.ts @@ -16,6 +16,7 @@ import {ComponentRef, Directive, ElementRef, OnDestroy, OnInit, ViewContainerRef} from '@angular/core'; import {IonIcon} from '@ionic/angular'; import {IonIconDirective} from './ion-icon.directive'; +import {waitForElement} from './shadow-attacher'; export type IconData = Omit< Partial, @@ -79,25 +80,15 @@ export abstract class IconReplacer implements OnInit, OnDestroy { // noop } - ngOnInit() { + async ngOnInit() { this.init(); if (this.host) { this.attachObserver(); } else { - let tries = 0; - console.warn('IconReplacer: host not found, trying again'); - const interval = setInterval(() => { - if (tries > this.maxAttempts) { - clearInterval(interval); - throw new Error('IconReplacer: host not found'); - } - if (this.host) { - clearInterval(interval); - this.replace(); - } - tries++; - }, this.retryAfterMs); + console.warn("Shadow root didn't exist for ion icon replacer, retrying..."); + await waitForElement(() => this.host); + this.replace(); } } diff --git a/frontend/app/src/app/util/ion-icon/shadow-attacher.ts b/frontend/app/src/app/util/ion-icon/shadow-attacher.ts new file mode 100644 index 00000000..6f1157a0 --- /dev/null +++ b/frontend/app/src/app/util/ion-icon/shadow-attacher.ts @@ -0,0 +1,23 @@ +/** + * Waits for an element to appear + * + * In normal circumstances *please* use the mutation observer for these kinds of tasks. + * However, shadowDom attachment is **not** covered by the mutation observer, + * so a solution like this is required. + */ +export async function waitForElement(query: () => HTMLElement, maxAttempts = 10, retryAfterMs = 10) { + let tries = 0; + return new Promise(resolve => { + const interval = setInterval(() => { + if (tries > maxAttempts) { + clearInterval(interval); + throw new Error('Element not found'); + } + if (query()) { + clearInterval(interval); + resolve(); + } + tries++; + }, retryAfterMs); + }); +} diff --git a/frontend/app/src/app/util/section/section-link-card.html b/frontend/app/src/app/util/section/section-link-card.html deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/app/src/app/util/section/section-tail-prompt-card.html b/frontend/app/src/app/util/section/section-tail-prompt-card.html deleted file mode 100644 index e69de29b..00000000