From 99501ec9be4ae4553a42bf98997554eb5cfeb69a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Dec 2025 13:17:25 +0200 Subject: [PATCH] Introduce Dynamic MatDialog service to display dialogs over specified components. --- .../dialog/dynamic/dynamic-dialog.module.ts | 47 ++++++++++ .../dialog/dynamic/dynamic-dialog.ts | 85 +++++++++++++++++++ .../dynamic/dynamic-overlay-container.ts | 27 ++++++ .../dialog/dynamic/dynamic-overlay.ts | 62 ++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 7 +- 5 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts create mode 100644 ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts create mode 100644 ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts create mode 100644 ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts new file mode 100644 index 0000000000..7a69ef14c4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts @@ -0,0 +1,47 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { OverlayModule } from '@angular/cdk/overlay'; +import { NgModule } from '@angular/core'; +import { DEFAULT_DIALOG_CONFIG, DialogConfig, DialogModule } from '@angular/cdk/dialog'; +import { MatDialogModule } from '@angular/material/dialog'; +import { DynamicDialog, DynamicMatDialog } from './dynamic-dialog'; +import { DynamicOverlay } from './dynamic-overlay'; +import { DynamicOverlayContainer } from './dynamic-overlay-container'; + +export const DYNAMIC_MAT_DIALOG_PROVIDERS = [ + DynamicOverlayContainer, + DynamicOverlay, + DynamicDialog, + DynamicMatDialog, + { + provide: DEFAULT_DIALOG_CONFIG, + useValue: { + ...new DialogConfig() + } + } +]; + +@NgModule( { + imports: [ + OverlayModule, + DialogModule, + MatDialogModule + ], + providers: DYNAMIC_MAT_DIALOG_PROVIDERS +} ) +export class DynamicMatDialogModule { +} diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts new file mode 100644 index 0000000000..7e47e1884c --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts @@ -0,0 +1,85 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Location } from "@angular/common"; +import { Inject, Injectable, Injector, Optional, SkipSelf, TemplateRef } from '@angular/core'; +import { + MAT_DIALOG_DEFAULT_OPTIONS, + MAT_DIALOG_SCROLL_STRATEGY, + MatDialog, + MatDialogConfig, MatDialogRef +} from '@angular/material/dialog'; +import { DynamicOverlay } from "./dynamic-overlay"; +import { DynamicOverlayContainer } from '@shared/components/dialog/dynamic/dynamic-overlay-container'; +import { ComponentType, ScrollStrategy } from '@angular/cdk/overlay'; +import { DEFAULT_DIALOG_CONFIG, Dialog, DialogConfig } from '@angular/cdk/dialog'; + +export interface DynamicMatDialogConfig extends MatDialogConfig { + containerElement?: HTMLElement; +} + +@Injectable() +export class DynamicMatDialog extends MatDialog { + + private _customOverlay: DynamicOverlay; + + constructor( _overlay: DynamicOverlay, + _injector: Injector, + @Optional() location: Location, + @Inject( MAT_DIALOG_DEFAULT_OPTIONS ) _defaultOptions: MatDialogConfig, + @Inject( MAT_DIALOG_SCROLL_STRATEGY ) _scrollStrategy: ScrollStrategy, + @Optional() @SkipSelf() _parentDialog:DynamicMatDialog, + _overlayContainer: DynamicOverlayContainer) { + + super( _overlay, _injector, location, _defaultOptions, _scrollStrategy, _parentDialog, _overlayContainer ); + this._dialog = _injector.get(DynamicDialog); + this._customOverlay = _overlay; + } + + public open(component: ComponentType | TemplateRef, config?: DynamicMatDialogConfig): MatDialogRef { + if (config?.containerElement) { + config.containerElement.style.transform = 'translateZ(0)'; + this._customOverlay.setContainerElement( config.containerElement ); + } + const ref = super.open(component, config); + if (config?.containerElement) { + ref.afterClosed().subscribe( + { + next: () => { + this._customOverlay.setContainerElement(null); + }, + error: () => { + this._customOverlay.setContainerElement(null); + } + } + ); + } + return ref; + } +} + +@Injectable() +export class DynamicDialog extends Dialog { + constructor( _overlay: DynamicOverlay, + _injector: Injector, + @Inject( DEFAULT_DIALOG_CONFIG ) _defaultOptions: DialogConfig, + @Inject( MAT_DIALOG_SCROLL_STRATEGY ) _scrollStrategy: ScrollStrategy, + @Optional() @SkipSelf() _parentDialog: DynamicDialog, + _overlayContainer: DynamicOverlayContainer) { + + super( _overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, _scrollStrategy ); + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts new file mode 100644 index 0000000000..be1eb1e3d3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { OverlayContainer } from "@angular/cdk/overlay"; +import { Injectable } from "@angular/core"; + +@Injectable() +export class DynamicOverlayContainer extends OverlayContainer { + + public setContainerElement( containerElement:HTMLElement ):void { + + this._containerElement = containerElement; + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts new file mode 100644 index 0000000000..37745b7722 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Overlay, + ScrollStrategyOptions, + OverlayKeyboardDispatcher, OverlayOutsideClickDispatcher, OverlayPositionBuilder +} from '@angular/cdk/overlay'; +import { ComponentFactoryResolver, Inject, Injectable, Injector, NgZone } from '@angular/core'; +import { DynamicOverlayContainer } from './dynamic-overlay-container'; +import { DOCUMENT, Location } from '@angular/common'; +import { Directionality } from '@angular/cdk/bidi'; + +@Injectable() +export class DynamicOverlay extends Overlay { + + private _dynamicOverlayContainer: DynamicOverlayContainer; + + constructor( scrollStrategies: ScrollStrategyOptions, + _overlayContainer: DynamicOverlayContainer, + _componentFactoryResolver: ComponentFactoryResolver, + _positionBuilder: OverlayPositionBuilder, + _keyboardDispatcher: OverlayKeyboardDispatcher, + _injector: Injector, + _ngZone: NgZone, + @Inject(DOCUMENT) document: Document, + _directionality: Directionality, + _location: Location, + _outsideClickDispatcher: OverlayOutsideClickDispatcher) { + + super( scrollStrategies, + _overlayContainer, + _componentFactoryResolver, + _positionBuilder, + _keyboardDispatcher, + _injector, + _ngZone, + document, + _directionality, + _location, + _outsideClickDispatcher); + + this._dynamicOverlayContainer = _overlayContainer; + } + + public setContainerElement(containerElement:HTMLElement ): void { + this._dynamicOverlayContainer.setContainerElement( containerElement ); + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index e4c1bff266..7b73e2e6a3 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -237,6 +237,7 @@ import { StringPatternAutocompleteComponent } from '@shared/components/string-pa import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component'; import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe'; import { EntityLimitExceededDialogComponent } from '@shared/components/dialog/entity-limit-exceeded-dialog.component'; +import { DynamicMatDialogModule } from '@shared/components/dialog/dynamic/dynamic-dialog.module'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -520,7 +521,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) useFactory: MarkedOptionsFactory, deps: [MarkedOptionsService] } - }) + }), + DynamicMatDialogModule ], exports: [ FooterComponent, @@ -726,7 +728,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MqttVersionSelectComponent, PasswordRequirementsTooltipComponent, TimeUnitInputComponent, - StringPatternAutocompleteComponent + StringPatternAutocompleteComponent, + DynamicMatDialogModule ] }) export class SharedModule { }