2021-10-13 20:35:10 +03:00
|
|
|
///
|
2025-02-25 09:39:16 +02:00
|
|
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
2021-10-13 20:35:10 +03:00
|
|
|
///
|
|
|
|
|
/// 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 {
|
2022-10-07 19:33:18 +03:00
|
|
|
ComponentRef,
|
|
|
|
|
ElementRef,
|
|
|
|
|
Inject,
|
|
|
|
|
Injectable,
|
|
|
|
|
Injector,
|
2021-10-13 20:35:10 +03:00
|
|
|
Renderer2,
|
|
|
|
|
Type,
|
|
|
|
|
ViewContainerRef
|
|
|
|
|
} from '@angular/core';
|
2025-03-24 17:15:52 +02:00
|
|
|
import {
|
|
|
|
|
defaultPopoverConfig,
|
|
|
|
|
DisplayPopoverConfig,
|
|
|
|
|
DisplayPopoverWithComponentRefConfig,
|
|
|
|
|
PopoverPreferredPlacement,
|
|
|
|
|
PopoverWithTrigger
|
|
|
|
|
} from '@shared/components/popover.models';
|
2021-10-13 20:35:10 +03:00
|
|
|
import { TbPopoverComponent } from '@shared/components/popover.component';
|
|
|
|
|
import { ComponentType } from '@angular/cdk/portal';
|
|
|
|
|
import { HELP_MARKDOWN_COMPONENT_TOKEN } from '@shared/components/tokens';
|
2023-02-06 13:09:43 +02:00
|
|
|
import { CdkOverlayOrigin } from '@angular/cdk/overlay';
|
2024-11-28 20:05:26 +02:00
|
|
|
import { Observable } from 'rxjs';
|
2025-03-24 17:15:52 +02:00
|
|
|
import { mergeDeep } from '@core/utils';
|
2021-10-13 20:35:10 +03:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class TbPopoverService {
|
|
|
|
|
|
|
|
|
|
private popoverWithTriggers: PopoverWithTrigger[] = [];
|
|
|
|
|
|
2024-10-04 18:30:19 +03:00
|
|
|
constructor(@Inject(HELP_MARKDOWN_COMPONENT_TOKEN) private helpMarkdownComponent: ComponentType<any>) {
|
2021-10-13 20:35:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasPopover(trigger: Element): boolean {
|
|
|
|
|
const res = this.findPopoverByTrigger(trigger);
|
|
|
|
|
return res !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hidePopover(trigger: Element): boolean {
|
|
|
|
|
const component: TbPopoverComponent = this.findPopoverByTrigger(trigger);
|
|
|
|
|
if (component && component.tbVisible) {
|
|
|
|
|
component.hide();
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-07 19:33:18 +03:00
|
|
|
createPopoverRef(hostView: ViewContainerRef): ComponentRef<TbPopoverComponent> {
|
2024-10-04 18:30:19 +03:00
|
|
|
return hostView.createComponent(TbPopoverComponent);
|
2022-10-07 19:33:18 +03:00
|
|
|
}
|
|
|
|
|
|
2025-03-24 17:15:52 +02:00
|
|
|
displayPopover<T>(config: DisplayPopoverConfig<T>): TbPopoverComponent<T>;
|
2021-10-13 20:35:10 +03:00
|
|
|
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
|
2025-03-24 17:15:52 +02:00
|
|
|
componentType: Type<T>, preferredPlacement: PopoverPreferredPlacement,
|
|
|
|
|
hideOnClickOutside: boolean, injector?: Injector, context?: any, overlayStyle?: any,
|
|
|
|
|
popoverStyle?: any, style?: any,
|
|
|
|
|
showCloseButton?: boolean, visibleFn?: (visible: boolean) => void,
|
|
|
|
|
popoverContentStyle?: any): TbPopoverComponent<T>;
|
|
|
|
|
displayPopover<T>(config: Element | DisplayPopoverConfig<T>, renderer?: Renderer2, hostView?: ViewContainerRef,
|
|
|
|
|
componentType?: Type<T>, preferredPlacement?: PopoverPreferredPlacement,
|
|
|
|
|
hideOnClickOutside?: boolean, injector?: Injector, context?: any, overlayStyle?: any,
|
|
|
|
|
popoverStyle?: any, style?: any,
|
|
|
|
|
showCloseButton?: boolean, visibleFn?: (visible: boolean) => void,
|
|
|
|
|
popoverContentStyle?: any): TbPopoverComponent<T> {
|
|
|
|
|
if (!(config instanceof Element) && 'trigger' in config && 'renderer' in config && 'componentType' in config) {
|
|
|
|
|
const componentRef = this.createPopoverRef(config.hostView);
|
|
|
|
|
return this.displayPopoverWithComponentRef<T>({ ...config, componentRef })
|
|
|
|
|
} else if (config instanceof Element) {
|
|
|
|
|
const componentRef = this.createPopoverRef(hostView);
|
|
|
|
|
return this.displayPopoverWithComponentRef<T>(componentRef, config, renderer, componentType, preferredPlacement, hideOnClickOutside,
|
|
|
|
|
injector, context, overlayStyle, popoverStyle, style, showCloseButton, visibleFn, popoverContentStyle);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Invalid configuration provided for displayPopover");
|
|
|
|
|
}
|
2022-10-07 19:33:18 +03:00
|
|
|
}
|
|
|
|
|
|
2025-03-24 17:15:52 +02:00
|
|
|
displayPopoverWithComponentRef<T>(config: DisplayPopoverWithComponentRefConfig<T>): TbPopoverComponent<T>;
|
2022-10-07 19:33:18 +03:00
|
|
|
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
|
2025-03-24 17:15:52 +02:00
|
|
|
componentType: Type<T>, preferredPlacement: PopoverPreferredPlacement,
|
|
|
|
|
hideOnClickOutside: boolean, injector?: Injector, context?: any, overlayStyle?: any,
|
|
|
|
|
popoverStyle?: any, style?: any, showCloseButton?: boolean,
|
|
|
|
|
visibleFn?: (visible: boolean) => void, popoverContentStyle?: any): TbPopoverComponent<T>;
|
|
|
|
|
displayPopoverWithComponentRef<T>(config: ComponentRef<TbPopoverComponent> | DisplayPopoverWithComponentRefConfig<T>,
|
|
|
|
|
trigger?: Element, renderer?: Renderer2, componentType?: Type<T>,
|
|
|
|
|
preferredPlacement?: PopoverPreferredPlacement, hideOnClickOutside?: boolean,
|
|
|
|
|
injector?: Injector, context?: any, overlayStyle?: any,
|
|
|
|
|
popoverStyle?: any, style?: any, showCloseButton?: boolean,
|
|
|
|
|
visibleFn?: (visible: boolean) => void,
|
2024-03-01 14:45:51 +02:00
|
|
|
popoverContentStyle: any = {}): TbPopoverComponent<T> {
|
2025-03-24 17:15:52 +02:00
|
|
|
let popoverConfig: DisplayPopoverWithComponentRefConfig<T>;
|
|
|
|
|
if (!(config instanceof ComponentRef) && 'trigger' in config && 'renderer' in config && 'componentType' in config) {
|
|
|
|
|
popoverConfig = config;
|
|
|
|
|
} else if(config instanceof ComponentRef) {
|
|
|
|
|
popoverConfig = {
|
|
|
|
|
componentRef: config,
|
|
|
|
|
trigger,
|
|
|
|
|
renderer,
|
|
|
|
|
componentType,
|
|
|
|
|
preferredPlacement,
|
|
|
|
|
hideOnClickOutside,
|
|
|
|
|
injector,
|
|
|
|
|
context,
|
|
|
|
|
overlayStyle,
|
|
|
|
|
popoverStyle,
|
|
|
|
|
style,
|
|
|
|
|
showCloseButton,
|
|
|
|
|
visibleFn,
|
|
|
|
|
popoverContentStyle
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Invalid configuration provided for displayPopoverWithComponentRef");
|
|
|
|
|
}
|
|
|
|
|
popoverConfig = mergeDeep({} as any, defaultPopoverConfig, popoverConfig);
|
|
|
|
|
return this._displayPopoverWithComponentRef(popoverConfig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private _displayPopoverWithComponentRef<T>(conf: DisplayPopoverWithComponentRefConfig<T>): TbPopoverComponent<T> {
|
|
|
|
|
const component = conf.componentRef.instance;
|
2021-10-13 20:35:10 +03:00
|
|
|
this.popoverWithTriggers.push({
|
2025-03-24 17:15:52 +02:00
|
|
|
trigger: conf.trigger,
|
2021-10-13 20:35:10 +03:00
|
|
|
popoverComponent: component
|
|
|
|
|
});
|
2025-03-24 17:15:52 +02:00
|
|
|
conf.renderer.removeChild(
|
|
|
|
|
conf.renderer.parentNode(conf.trigger),
|
|
|
|
|
conf.componentRef.location.nativeElement
|
2021-10-13 20:35:10 +03:00
|
|
|
);
|
2025-03-24 17:15:52 +02:00
|
|
|
const originElementRef = new ElementRef(conf.trigger);
|
2023-02-06 13:09:43 +02:00
|
|
|
component.setOverlayOrigin(new CdkOverlayOrigin(originElementRef));
|
2025-03-24 17:15:52 +02:00
|
|
|
component.tbPlacement = conf.preferredPlacement;
|
|
|
|
|
component.tbComponent = conf.componentType;
|
|
|
|
|
component.tbComponentInjector = conf.injector;
|
|
|
|
|
component.tbComponentContext = conf.context;
|
|
|
|
|
component.tbOverlayStyle = conf.overlayStyle;
|
|
|
|
|
component.tbModal = conf.isModal;
|
|
|
|
|
component.tbPopoverInnerStyle = conf.popoverStyle;
|
|
|
|
|
component.tbPopoverInnerContentStyle = conf.popoverContentStyle;
|
|
|
|
|
component.tbComponentStyle = conf.style;
|
|
|
|
|
component.tbHideOnClickOutside = conf.hideOnClickOutside;
|
|
|
|
|
component.tbShowCloseButton = conf.showCloseButton;
|
2021-10-13 20:35:10 +03:00
|
|
|
component.tbVisibleChange.subscribe((visible: boolean) => {
|
|
|
|
|
if (!visible) {
|
2025-03-24 17:15:52 +02:00
|
|
|
conf.componentRef.destroy();
|
2021-10-13 20:35:10 +03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
component.tbDestroy.subscribe(() => {
|
|
|
|
|
this.removePopoverByComponent(component);
|
|
|
|
|
});
|
2023-11-23 17:56:04 +02:00
|
|
|
component.tbHideStart.subscribe(() => {
|
2025-03-24 17:15:52 +02:00
|
|
|
conf.visibleFn(false);
|
2023-11-23 17:56:04 +02:00
|
|
|
});
|
2021-10-13 20:35:10 +03:00
|
|
|
component.show();
|
2025-03-24 17:15:52 +02:00
|
|
|
conf.visibleFn(true);
|
2021-10-13 20:35:10 +03:00
|
|
|
return component;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId = '',
|
|
|
|
|
helpContent = '',
|
2024-11-28 20:05:26 +02:00
|
|
|
helpContentBase64 = '',
|
|
|
|
|
asyncHelpContent: Observable<string> = null,
|
2021-10-13 20:35:10 +03:00
|
|
|
visibleFn: (visible: boolean) => void = () => {},
|
|
|
|
|
readyFn: (ready: boolean) => void = () => {},
|
2024-01-23 20:03:14 +02:00
|
|
|
preferredPlacement: PopoverPreferredPlacement = 'bottom',
|
2021-10-13 20:35:10 +03:00
|
|
|
overlayStyle: any = {}, helpStyle: any = {}) {
|
|
|
|
|
if (this.hasPopover(trigger)) {
|
|
|
|
|
this.hidePopover(trigger);
|
|
|
|
|
} else {
|
|
|
|
|
readyFn(false);
|
|
|
|
|
const injector = Injector.create({
|
|
|
|
|
parent: hostView.injector, providers: []
|
|
|
|
|
});
|
2024-10-04 18:30:19 +03:00
|
|
|
const componentRef = hostView.createComponent(TbPopoverComponent);
|
2021-10-13 20:35:10 +03:00
|
|
|
const component = componentRef.instance;
|
|
|
|
|
this.popoverWithTriggers.push({
|
|
|
|
|
trigger,
|
|
|
|
|
popoverComponent: component
|
|
|
|
|
});
|
|
|
|
|
renderer.removeChild(
|
|
|
|
|
renderer.parentNode(trigger),
|
|
|
|
|
componentRef.location.nativeElement
|
|
|
|
|
);
|
|
|
|
|
const originElementRef = new ElementRef(trigger);
|
|
|
|
|
component.tbAnimationState = 'void';
|
|
|
|
|
component.tbOverlayStyle = {...overlayStyle, opacity: '0' };
|
2023-02-06 13:09:43 +02:00
|
|
|
component.setOverlayOrigin(new CdkOverlayOrigin(originElementRef));
|
2021-10-13 20:35:10 +03:00
|
|
|
component.tbPlacement = preferredPlacement;
|
2024-10-04 18:30:19 +03:00
|
|
|
component.tbComponent = this.helpMarkdownComponent;
|
2021-10-13 20:35:10 +03:00
|
|
|
component.tbComponentInjector = injector;
|
|
|
|
|
component.tbComponentContext = {
|
|
|
|
|
helpId,
|
|
|
|
|
helpContent,
|
2024-11-28 20:05:26 +02:00
|
|
|
helpContentBase64,
|
|
|
|
|
asyncHelpContent,
|
2021-10-13 20:35:10 +03:00
|
|
|
style: helpStyle,
|
|
|
|
|
visible: true
|
|
|
|
|
};
|
|
|
|
|
component.tbHideOnClickOutside = true;
|
|
|
|
|
component.tbVisibleChange.subscribe((visible: boolean) => {
|
|
|
|
|
if (!visible) {
|
|
|
|
|
visibleFn(false);
|
2022-06-01 16:35:20 +03:00
|
|
|
componentRef.destroy();
|
2021-10-13 20:35:10 +03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
component.tbDestroy.subscribe(() => {
|
|
|
|
|
this.removePopoverByComponent(component);
|
|
|
|
|
});
|
|
|
|
|
const showHelpMarkdownComponent = () => {
|
|
|
|
|
component.tbOverlayStyle = {...component.tbOverlayStyle, opacity: '1' };
|
|
|
|
|
component.tbAnimationState = 'active';
|
|
|
|
|
component.updatePosition();
|
|
|
|
|
readyFn(true);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
component.updatePosition();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const setupHelpMarkdownComponent = (helpMarkdownComponent: any) => {
|
|
|
|
|
if (helpMarkdownComponent.isMarkdownReady) {
|
|
|
|
|
showHelpMarkdownComponent();
|
|
|
|
|
} else {
|
|
|
|
|
helpMarkdownComponent.markdownReady.subscribe(() => {
|
|
|
|
|
showHelpMarkdownComponent();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if (component.tbComponentRef) {
|
|
|
|
|
setupHelpMarkdownComponent(component.tbComponentRef.instance);
|
|
|
|
|
} else {
|
|
|
|
|
component.tbComponentChange.subscribe((helpMarkdownComponentRef) => {
|
|
|
|
|
setupHelpMarkdownComponent(helpMarkdownComponentRef.instance);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
component.show();
|
|
|
|
|
visibleFn(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null {
|
|
|
|
|
const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger));
|
|
|
|
|
if (res) {
|
|
|
|
|
return res.popoverComponent;
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private removePopoverByComponent(component: TbPopoverComponent): void {
|
|
|
|
|
const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component);
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
this.popoverWithTriggers.splice(index, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean {
|
|
|
|
|
return element1 === element2 || element1.contains(element2) || element2.contains(element1);
|
|
|
|
|
}
|
|
|
|
|
}
|