/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {animate, style, transition, trigger} from '@angular/animations'; import {Platform} from '@angular/cdk/platform'; import {DOCUMENT} from '@angular/common'; import { Component, computed, inject, OnDestroy, OnInit, signal, WritableSignal, } from '@angular/core'; import {Events, MessageBus} from 'protocol'; import {interval} from 'rxjs'; import {FrameManager} from './frame_manager'; import {ThemeService} from './theme-service'; import {MatTooltip, MatTooltipModule} from '@angular/material/tooltip'; import {DevToolsTabsComponent} from './devtools-tabs/devtools-tabs.component'; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {Frame} from './application-environment'; const DETECT_ANGULAR_ATTEMPTS = 10; enum AngularStatus { /** * This page may have Angular but we don't know yet. We're still trying to detect it. */ UNKNOWN, /** * We've given up on trying to detect Angular. We tried ${DETECT_ANGULAR_ATTEMPTS} times and * failed. */ DOES_NOT_EXIST, /** * Angular was detected somewhere on the page. */ EXISTS, } const LAST_SUPPORTED_VERSION = 9; @Component({ selector: 'ng-devtools', templateUrl: './devtools.component.html', styleUrls: ['./devtools.component.scss'], animations: [ trigger('enterAnimation', [ transition(':enter', [style({opacity: 0}), animate('200ms', style({opacity: 1}))]), transition(':leave', [style({opacity: 1}), animate('200ms', style({opacity: 0}))]), ]), ], standalone: true, imports: [DevToolsTabsComponent, MatTooltip, MatProgressSpinnerModule, MatTooltipModule], }) export class DevToolsComponent implements OnInit, OnDestroy { AngularStatus = AngularStatus; angularStatus: AngularStatus = AngularStatus.UNKNOWN; angularVersion: WritableSignal = signal(undefined); angularIsInDevMode = true; hydration: boolean = false; ivy: WritableSignal = signal(undefined); supportedVersion = computed(() => { const version = this.angularVersion(); if (!version) { return false; } const majorVersion = parseInt(version.toString().split('.')[0], 10); // Check that major version is either greater or equal to the last supported version // or that the major version is 0 for the (0.0.0-PLACEHOLDER) dev build case. return (majorVersion >= LAST_SUPPORTED_VERSION || majorVersion === 0) && this.ivy(); }); private readonly _firefoxStyleName = 'firefox_styles.css'; private readonly _chromeStyleName = 'chrome_styles.css'; private readonly _messageBus = inject>(MessageBus); private readonly _themeService = inject(ThemeService); private readonly _platform = inject(Platform); private readonly _document = inject(DOCUMENT); private readonly _frameManager = inject(FrameManager); private _interval$ = interval(500).subscribe((attempt) => { if (attempt === DETECT_ANGULAR_ATTEMPTS) { this.angularStatus = AngularStatus.DOES_NOT_EXIST; } this._messageBus.emit('queryNgAvailability'); }); inspectFrame(frame: Frame) { this._frameManager.inspectFrame(frame); } ngOnInit(): void { this._themeService.initializeThemeWatcher(); this._messageBus.once('ngAvailability', ({version, devMode, ivy, hydration}) => { this.angularStatus = version ? AngularStatus.EXISTS : AngularStatus.DOES_NOT_EXIST; this.angularVersion.set(version); this.angularIsInDevMode = devMode; this.ivy.set(ivy); this._interval$.unsubscribe(); this.hydration = hydration; }); const browserStyleName = this._platform.FIREFOX ? this._firefoxStyleName : this._chromeStyleName; this._loadStyle(browserStyleName); } /** Add a style file in header based on fileName */ private _loadStyle(styleName: string) { const head = this._document.getElementsByTagName('head')[0]; const style = this._document.createElement('link'); style.rel = 'stylesheet'; style.href = `./styles/${styleName}`; head.appendChild(style); } ngOnDestroy(): void { this._interval$.unsubscribe(); } }