/* * Copyright (C) 2022 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import { ComponentRef, Directive, ElementRef, OnDestroy, OnInit, ViewContainerRef, } from '@angular/core'; import {IonIcon} from '@ionic/angular'; import {IonIconDirective} from './ion-icon.directive'; export type IconData = Omit< Partial, | 'ngOnChanges' | 'ngOnInit' | 'viewContainerRef' | 'ngOnDestroy' | 'element' | 'ionIcon' | 'disableProperty' >; /** * A utility class to replace ion-icons in other ionic components. */ @Directive() export abstract class IconReplacer implements OnInit, OnDestroy { private mutationObserver: MutationObserver; protected slotName = 'sc-icon'; protected maxAttempts = 10; protected retryAfterMs = 10; /** * The host element * * This will be either element.nativeElement.shadowRoot or element.nativeElement * depending on the iconDomLocation */ protected get host() { return this.iconDomLocation === 'shadow' ? this.element.nativeElement.shadowRoot : this.element.nativeElement; } /** * @param element The host element * @param viewContainerRef The view container ref * @param iconDomLocation If the icon is placed inside the shadow dom or not * @protected */ protected constructor( private readonly element: ElementRef, private readonly viewContainerRef: ViewContainerRef, private readonly iconDomLocation: 'shadow' | 'light', ) {} /** * Replace the icons here */ abstract replace(): void; /** * If any additional work needs to be done, this * is called during ngOnInit */ init() { // noop } /** * If you need to do cleanup, this method is called during ngOnDestroy */ destroy() { // noop } ngOnInit() { this.init(); if (!this.host) { 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); } else { this.attachObserver(); } } private attachObserver() { this.mutationObserver = new MutationObserver(() => this.replace()); this.mutationObserver.observe(this.host, { childList: true, }); } replaceIcon(parent: HTMLElement | null, iconData: IconData, slotName = '') { if (!parent) return; const icon = parent.querySelector('ion-icon'); if (!icon) return; const scIcon = this.createIcon(iconData); // @ts-expect-error can be spread scIcon.location.nativeElement.classList.add(...icon.classList); if (this.iconDomLocation === 'shadow') { // shadow dom needs to utilize slotting, to put it outside // the shadow dom, otherwise it won't receive any css data const slot = document.createElement('slot'); slot.name = this.slotName + slotName; icon.replaceWith(slot); scIcon.location.nativeElement.slot = this.slotName + slotName; this.element.nativeElement.append(scIcon.location.nativeElement); } else { icon.replaceWith(scIcon.location.nativeElement); } } private createIcon(iconData: IconData): ComponentRef { const ionIcon = this.viewContainerRef.createComponent(IonIcon, {}); const iconDirective = new IonIconDirective( ionIcon.location, this.viewContainerRef, ionIcon.instance, ); for (const key in iconData) { // @ts-expect-error type mismatch iconDirective[key] = iconData[key]; } iconDirective.ngOnInit(); return ionIcon; } ngOnDestroy() { this.mutationObserver?.disconnect(); this.destroy(); } }