2021-10-06 21:03:29 +03:00
|
|
|
|
///
|
|
|
|
|
|
/// Copyright © 2016-2021 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 {
|
|
|
|
|
|
AfterViewInit,
|
2021-10-07 10:34:43 +03:00
|
|
|
|
ChangeDetectionStrategy,
|
|
|
|
|
|
ChangeDetectorRef,
|
|
|
|
|
|
Component,
|
|
|
|
|
|
ComponentFactory,
|
|
|
|
|
|
ComponentFactoryResolver,
|
|
|
|
|
|
ComponentRef,
|
2021-10-06 21:03:29 +03:00
|
|
|
|
Directive,
|
|
|
|
|
|
ElementRef,
|
2021-10-07 10:34:43 +03:00
|
|
|
|
EventEmitter,
|
|
|
|
|
|
Injectable,
|
|
|
|
|
|
Injector,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
OnChanges,
|
|
|
|
|
|
OnDestroy,
|
|
|
|
|
|
OnInit,
|
|
|
|
|
|
Optional,
|
|
|
|
|
|
Output,
|
|
|
|
|
|
Renderer2,
|
|
|
|
|
|
SimpleChanges,
|
|
|
|
|
|
TemplateRef,
|
|
|
|
|
|
Type,
|
|
|
|
|
|
ViewChild,
|
|
|
|
|
|
ViewContainerRef,
|
|
|
|
|
|
ViewEncapsulation
|
2021-10-06 21:03:29 +03:00
|
|
|
|
} from '@angular/core';
|
|
|
|
|
|
import { Direction, Directionality } from '@angular/cdk/bidi';
|
|
|
|
|
|
import {
|
|
|
|
|
|
CdkConnectedOverlay,
|
|
|
|
|
|
CdkOverlayOrigin,
|
|
|
|
|
|
ConnectedOverlayPositionChange,
|
|
|
|
|
|
ConnectionPositionPair
|
|
|
|
|
|
} from '@angular/cdk/overlay';
|
|
|
|
|
|
import { Subject, Subscription } from 'rxjs';
|
|
|
|
|
|
import {
|
|
|
|
|
|
DEFAULT_POPOVER_POSITIONS,
|
|
|
|
|
|
getPlacementName,
|
2021-10-07 10:34:43 +03:00
|
|
|
|
popoverMotion,
|
|
|
|
|
|
PopoverPlacement,
|
|
|
|
|
|
PopoverWithTrigger,
|
|
|
|
|
|
POSITION_MAP,
|
|
|
|
|
|
PropertyMapping
|
2021-10-06 21:03:29 +03:00
|
|
|
|
} from '@shared/components/popover.models';
|
|
|
|
|
|
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
|
2021-10-07 10:34:43 +03:00
|
|
|
|
import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
|
2021-10-06 21:03:29 +03:00
|
|
|
|
import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
|
|
|
|
|
|
|
|
|
|
|
|
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
|
|
|
|
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
|
|
selector: '[tb-popover]',
|
|
|
|
|
|
exportAs: 'tbPopover',
|
|
|
|
|
|
host: {
|
|
|
|
|
|
'[class.tb-popover-open]': 'visible'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
|
|
|
|
|
|
|
|
|
|
|
|
// tslint:disable:no-input-rename
|
|
|
|
|
|
@Input('tbPopoverContent') content?: string | TemplateRef<void>;
|
|
|
|
|
|
@Input('tbPopoverTrigger') trigger?: TbPopoverTrigger = 'hover';
|
|
|
|
|
|
@Input('tbPopoverPlacement') placement?: string | string[] = 'top';
|
|
|
|
|
|
@Input('tbPopoverOrigin') origin?: ElementRef<HTMLElement>;
|
|
|
|
|
|
@Input('tbPopoverVisible') visible?: boolean;
|
|
|
|
|
|
@Input('tbPopoverMouseEnterDelay') mouseEnterDelay?: number;
|
|
|
|
|
|
@Input('tbPopoverMouseLeaveDelay') mouseLeaveDelay?: number;
|
|
|
|
|
|
@Input('tbPopoverOverlayClassName') overlayClassName?: string;
|
|
|
|
|
|
@Input('tbPopoverOverlayStyle') overlayStyle?: { [klass: string]: any };
|
|
|
|
|
|
@Input() tbPopoverBackdrop = false;
|
|
|
|
|
|
|
|
|
|
|
|
// tslint:disable-next-line:no-output-rename
|
|
|
|
|
|
@Output('tbPopoverVisibleChange') readonly visibleChange = new EventEmitter<boolean>();
|
|
|
|
|
|
|
|
|
|
|
|
componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent);
|
|
|
|
|
|
component?: TbPopoverComponent;
|
|
|
|
|
|
|
|
|
|
|
|
private readonly destroy$ = new Subject<void>();
|
|
|
|
|
|
private readonly triggerDisposables: Array<() => void> = [];
|
|
|
|
|
|
private delayTimer?;
|
|
|
|
|
|
private internalVisible = false;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
private elementRef: ElementRef,
|
|
|
|
|
|
private hostView: ViewContainerRef,
|
|
|
|
|
|
private resolver: ComponentFactoryResolver,
|
|
|
|
|
|
private renderer: Renderer2
|
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
|
|
ngOnChanges(changes: SimpleChanges): void {
|
|
|
|
|
|
const { trigger } = changes;
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger && !trigger.isFirstChange()) {
|
|
|
|
|
|
this.registerTriggers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.component) {
|
|
|
|
|
|
this.updatePropertiesByChanges(changes);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ngAfterViewInit(): void {
|
|
|
|
|
|
this.registerTriggers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
|
|
|
|
this.destroy$.next();
|
|
|
|
|
|
this.destroy$.complete();
|
|
|
|
|
|
this.clearTogglingTimer();
|
|
|
|
|
|
this.removeTriggerListeners();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
show(): void {
|
|
|
|
|
|
if (!this.component) {
|
|
|
|
|
|
this.createComponent();
|
|
|
|
|
|
}
|
|
|
|
|
|
this.component?.show();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hide(): void {
|
|
|
|
|
|
this.component?.hide();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updatePosition(): void {
|
|
|
|
|
|
if (this.component) {
|
|
|
|
|
|
this.component.updatePosition();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private createComponent(): void {
|
|
|
|
|
|
const componentRef = this.hostView.createComponent(this.componentFactory);
|
|
|
|
|
|
|
|
|
|
|
|
this.component = componentRef.instance;
|
|
|
|
|
|
|
|
|
|
|
|
this.renderer.removeChild(
|
|
|
|
|
|
this.renderer.parentNode(this.elementRef.nativeElement),
|
|
|
|
|
|
componentRef.location.nativeElement
|
|
|
|
|
|
);
|
|
|
|
|
|
this.component.setOverlayOrigin({ elementRef: this.origin || this.elementRef });
|
|
|
|
|
|
|
|
|
|
|
|
this.initProperties();
|
|
|
|
|
|
|
|
|
|
|
|
this.component.tbVisibleChange
|
|
|
|
|
|
.pipe(distinctUntilChanged(), takeUntil(this.destroy$))
|
|
|
|
|
|
.subscribe((visible: boolean) => {
|
|
|
|
|
|
this.internalVisible = visible;
|
|
|
|
|
|
this.visibleChange.emit(visible);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private registerTriggers(): void {
|
|
|
|
|
|
// When the method gets invoked, all properties has been synced to the dynamic component.
|
|
|
|
|
|
// After removing the old API, we can just check the directive's own `nzTrigger`.
|
|
|
|
|
|
const el = this.elementRef.nativeElement;
|
|
|
|
|
|
const trigger = this.trigger;
|
|
|
|
|
|
|
|
|
|
|
|
this.removeTriggerListeners();
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger === 'hover') {
|
|
|
|
|
|
let overlayElement: HTMLElement;
|
|
|
|
|
|
this.triggerDisposables.push(
|
|
|
|
|
|
this.renderer.listen(el, 'mouseenter', () => {
|
|
|
|
|
|
this.delayEnterLeave(true, true, this.mouseEnterDelay);
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
this.triggerDisposables.push(
|
|
|
|
|
|
this.renderer.listen(el, 'mouseleave', () => {
|
|
|
|
|
|
this.delayEnterLeave(true, false, this.mouseLeaveDelay);
|
|
|
|
|
|
if (this.component?.overlay.overlayRef && !overlayElement) {
|
|
|
|
|
|
overlayElement = this.component.overlay.overlayRef.overlayElement;
|
|
|
|
|
|
this.triggerDisposables.push(
|
|
|
|
|
|
this.renderer.listen(overlayElement, 'mouseenter', () => {
|
|
|
|
|
|
this.delayEnterLeave(false, true, this.mouseEnterDelay);
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
this.triggerDisposables.push(
|
|
|
|
|
|
this.renderer.listen(overlayElement, 'mouseleave', () => {
|
|
|
|
|
|
this.delayEnterLeave(false, false, this.mouseLeaveDelay);
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (trigger === 'focus') {
|
|
|
|
|
|
this.triggerDisposables.push(this.renderer.listen(el, 'focusin', () => this.show()));
|
|
|
|
|
|
this.triggerDisposables.push(this.renderer.listen(el, 'focusout', () => this.hide()));
|
|
|
|
|
|
} else if (trigger === 'click') {
|
|
|
|
|
|
this.triggerDisposables.push(
|
|
|
|
|
|
this.renderer.listen(el, 'click', (e: MouseEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (this.component?.visible) {
|
|
|
|
|
|
this.hide();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.show();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Else do nothing because user wants to control the visibility programmatically.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updatePropertiesByChanges(changes: SimpleChanges): void {
|
|
|
|
|
|
this.updatePropertiesByKeys(Object.keys(changes));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updatePropertiesByKeys(keys?: string[]): void {
|
|
|
|
|
|
const mappingProperties: PropertyMapping = {
|
|
|
|
|
|
// common mappings
|
|
|
|
|
|
content: ['tbContent', () => this.content],
|
|
|
|
|
|
trigger: ['tbTrigger', () => this.trigger],
|
|
|
|
|
|
placement: ['tbPlacement', () => this.placement],
|
|
|
|
|
|
visible: ['tbVisible', () => this.visible],
|
|
|
|
|
|
mouseEnterDelay: ['tbMouseEnterDelay', () => this.mouseEnterDelay],
|
|
|
|
|
|
mouseLeaveDelay: ['tbMouseLeaveDelay', () => this.mouseLeaveDelay],
|
|
|
|
|
|
overlayClassName: ['tbOverlayClassName', () => this.overlayClassName],
|
|
|
|
|
|
overlayStyle: ['tbOverlayStyle', () => this.overlayStyle],
|
|
|
|
|
|
tbPopoverBackdrop: ['tbBackdrop', () => this.tbPopoverBackdrop]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
(keys || Object.keys(mappingProperties).filter(key => !key.startsWith('directive'))).forEach(
|
|
|
|
|
|
(property: any) => {
|
|
|
|
|
|
if (mappingProperties[property]) {
|
|
|
|
|
|
const [name, valueFn] = mappingProperties[property];
|
|
|
|
|
|
this.updateComponentValue(name, valueFn());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
this.component?.updateByDirective();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private initProperties(): void {
|
|
|
|
|
|
this.updatePropertiesByKeys();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateComponentValue(key: string, value: any): void {
|
|
|
|
|
|
if (typeof value !== 'undefined') {
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
this.component[key] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private delayEnterLeave(isOrigin: boolean, isEnter: boolean, delay: number = -1): void {
|
|
|
|
|
|
if (this.delayTimer) {
|
|
|
|
|
|
this.clearTogglingTimer();
|
|
|
|
|
|
} else if (delay > 0) {
|
|
|
|
|
|
this.delayTimer = setTimeout(() => {
|
|
|
|
|
|
this.delayTimer = undefined;
|
|
|
|
|
|
isEnter ? this.show() : this.hide();
|
|
|
|
|
|
}, delay * 1000);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// `isOrigin` is used due to the tooltip will not hide immediately
|
|
|
|
|
|
// (may caused by the fade-out animation).
|
|
|
|
|
|
isEnter && isOrigin ? this.show() : this.hide();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private removeTriggerListeners(): void {
|
|
|
|
|
|
this.triggerDisposables.forEach(dispose => dispose());
|
|
|
|
|
|
this.triggerDisposables.length = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private clearTogglingTimer(): void {
|
|
|
|
|
|
if (this.delayTimer) {
|
|
|
|
|
|
clearTimeout(this.delayTimer);
|
|
|
|
|
|
this.delayTimer = undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class TbPopoverService {
|
|
|
|
|
|
|
|
|
|
|
|
private popoverWithTriggers: PopoverWithTrigger[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent);
|
|
|
|
|
|
|
|
|
|
|
|
constructor(private resolver: ComponentFactoryResolver) {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
|
|
|
|
|
|
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
|
2021-10-11 13:23:21 +03:00
|
|
|
|
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent {
|
2021-10-06 21:03:29 +03:00
|
|
|
|
const componentRef = hostView.createComponent(this.componentFactory);
|
|
|
|
|
|
const component = componentRef.instance;
|
|
|
|
|
|
this.popoverWithTriggers.push({
|
|
|
|
|
|
trigger,
|
|
|
|
|
|
popoverComponent: component
|
|
|
|
|
|
});
|
|
|
|
|
|
renderer.removeChild(
|
|
|
|
|
|
renderer.parentNode(trigger),
|
|
|
|
|
|
componentRef.location.nativeElement
|
|
|
|
|
|
);
|
|
|
|
|
|
const originElementRef = new ElementRef(trigger);
|
|
|
|
|
|
component.setOverlayOrigin({ elementRef: originElementRef });
|
|
|
|
|
|
component.tbPlacement = preferredPlacement;
|
|
|
|
|
|
component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType);
|
|
|
|
|
|
component.tbComponentInjector = injector;
|
|
|
|
|
|
component.tbComponentContext = context;
|
2021-10-11 13:23:21 +03:00
|
|
|
|
component.tbOverlayStyle = overlayStyle;
|
2021-10-06 21:03:29 +03:00
|
|
|
|
component.tbPopoverInnerStyle = popoverStyle;
|
|
|
|
|
|
component.tbComponentStyle = style;
|
|
|
|
|
|
component.tbHideOnClickOutside = hideOnClickOutside;
|
|
|
|
|
|
component.tbVisibleChange.subscribe((visible: boolean) => {
|
|
|
|
|
|
if (!visible) {
|
|
|
|
|
|
component.tbAnimationDone.subscribe(() => {
|
|
|
|
|
|
componentRef.destroy();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
component.tbDestroy.subscribe(() => {
|
|
|
|
|
|
this.removePopoverByComponent(component);
|
|
|
|
|
|
});
|
|
|
|
|
|
component.show();
|
|
|
|
|
|
return component;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId: string,
|
|
|
|
|
|
visibleFn: (visible: boolean) => void, readyFn: (ready: boolean) => void) {
|
|
|
|
|
|
if (this.hasPopover(trigger)) {
|
|
|
|
|
|
this.hidePopover(trigger);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
readyFn(false);
|
|
|
|
|
|
const injector = Injector.create({
|
|
|
|
|
|
parent: hostView.injector, providers: []
|
|
|
|
|
|
});
|
|
|
|
|
|
const componentRef = hostView.createComponent(this.componentFactory);
|
|
|
|
|
|
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 = { opacity: '0' };
|
|
|
|
|
|
component.setOverlayOrigin({ elementRef: originElementRef });
|
|
|
|
|
|
component.tbPlacement = 'bottom';
|
|
|
|
|
|
component.tbComponentFactory = this.resolver.resolveComponentFactory(HelpMarkdownComponent);
|
|
|
|
|
|
component.tbComponentInjector = injector;
|
|
|
|
|
|
component.tbComponentContext = {
|
|
|
|
|
|
helpId,
|
|
|
|
|
|
visible: true
|
|
|
|
|
|
};
|
|
|
|
|
|
component.tbHideOnClickOutside = true;
|
|
|
|
|
|
component.tbVisibleChange.subscribe((visible: boolean) => {
|
|
|
|
|
|
if (!visible) {
|
|
|
|
|
|
visibleFn(false);
|
|
|
|
|
|
component.tbAnimationDone.subscribe(() => {
|
|
|
|
|
|
componentRef.destroy();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
component.tbDestroy.subscribe(() => {
|
|
|
|
|
|
this.removePopoverByComponent(component);
|
|
|
|
|
|
});
|
|
|
|
|
|
const showHelpMarkdownComponent = () => {
|
|
|
|
|
|
component.tbOverlayStyle = { opacity: '1' };
|
|
|
|
|
|
component.tbAnimationState = 'active';
|
|
|
|
|
|
component.updatePosition();
|
|
|
|
|
|
readyFn(true);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
component.updatePosition();
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
const setupHelpMarkdownComponent = (helpMarkdownComponent: HelpMarkdownComponent) => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
|
selector: 'tb-popover',
|
|
|
|
|
|
exportAs: 'tbPopoverComponent',
|
|
|
|
|
|
animations: [popoverMotion],
|
|
|
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
|
|
|
encapsulation: ViewEncapsulation.None,
|
|
|
|
|
|
styleUrls: ['./popover.component.scss'],
|
|
|
|
|
|
template: `
|
|
|
|
|
|
<ng-template
|
|
|
|
|
|
#overlay="cdkConnectedOverlay"
|
|
|
|
|
|
cdkConnectedOverlay
|
|
|
|
|
|
[cdkConnectedOverlayHasBackdrop]="hasBackdrop"
|
|
|
|
|
|
[cdkConnectedOverlayOrigin]="origin"
|
|
|
|
|
|
[cdkConnectedOverlayPositions]="positions"
|
|
|
|
|
|
[cdkConnectedOverlayOpen]="visible"
|
|
|
|
|
|
[cdkConnectedOverlayPush]="true"
|
|
|
|
|
|
(overlayOutsideClick)="onClickOutside($event)"
|
|
|
|
|
|
(detach)="hide()"
|
|
|
|
|
|
(positionChange)="onPositionChange($event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div #popoverRoot [@popoverMotion]="tbAnimationState"
|
|
|
|
|
|
(@popoverMotion.done)="animationDone()">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="tb-popover"
|
|
|
|
|
|
[class.tb-popover-rtl]="dir === 'rtl'"
|
|
|
|
|
|
[ngClass]="classMap"
|
|
|
|
|
|
[ngStyle]="tbOverlayStyle"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="tb-popover-content">
|
|
|
|
|
|
<div class="tb-popover-arrow">
|
|
|
|
|
|
<span class="tb-popover-arrow-content"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="tb-popover-inner" [ngStyle]="tbPopoverInnerStyle" role="tooltip">
|
|
|
|
|
|
<div class="tb-popover-close-button" (click)="closeButtonClick($event)">×</div>
|
2021-10-11 13:23:21 +03:00
|
|
|
|
<div style="width: 100%; height: 100%;">
|
2021-10-06 21:03:29 +03:00
|
|
|
|
<div class="tb-popover-inner-content">
|
|
|
|
|
|
<ng-container *ngIf="tbContent">
|
|
|
|
|
|
<ng-container *tbStringTemplateOutlet="tbContent">{{ tbContent }}</ng-container>
|
|
|
|
|
|
</ng-container>
|
|
|
|
|
|
<ng-container *ngIf="tbComponentFactory"
|
|
|
|
|
|
[tbComponentOutlet]="tbComponentFactory"
|
|
|
|
|
|
[tbComponentInjector]="tbComponentInjector"
|
|
|
|
|
|
[tbComponentOutletContext]="tbComponentContext"
|
|
|
|
|
|
(componentChange)="onComponentChange($event)"
|
|
|
|
|
|
[tbComponentStyle]="tbComponentStyle">
|
|
|
|
|
|
</ng-container>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ng-template>
|
|
|
|
|
|
`
|
|
|
|
|
|
})
|
|
|
|
|
|
export class TbPopoverComponent implements OnDestroy, OnInit {
|
|
|
|
|
|
|
|
|
|
|
|
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
|
|
|
|
|
|
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
|
|
|
|
|
|
|
|
|
|
|
|
tbContent: string | TemplateRef<void> | null = null;
|
|
|
|
|
|
tbComponentFactory: ComponentFactory<any> | null = null;
|
|
|
|
|
|
tbComponentRef: ComponentRef<any> | null = null;
|
|
|
|
|
|
tbComponentContext: any;
|
|
|
|
|
|
tbComponentInjector: Injector | null = null;
|
|
|
|
|
|
tbComponentStyle: { [klass: string]: any } = {};
|
|
|
|
|
|
tbOverlayClassName!: string;
|
|
|
|
|
|
tbOverlayStyle: { [klass: string]: any } = {};
|
|
|
|
|
|
tbPopoverInnerStyle: { [klass: string]: any } = {};
|
|
|
|
|
|
tbBackdrop = false;
|
|
|
|
|
|
tbMouseEnterDelay?: number;
|
|
|
|
|
|
tbMouseLeaveDelay?: number;
|
|
|
|
|
|
tbHideOnClickOutside = true;
|
|
|
|
|
|
|
|
|
|
|
|
tbAnimationState = 'active';
|
|
|
|
|
|
|
|
|
|
|
|
tbVisibleChange = new Subject<boolean>();
|
|
|
|
|
|
tbAnimationDone = new Subject<void>();
|
|
|
|
|
|
tbComponentChange = new Subject<ComponentRef<any>>();
|
|
|
|
|
|
tbDestroy = new Subject<void>();
|
|
|
|
|
|
|
|
|
|
|
|
set tbVisible(value: boolean) {
|
|
|
|
|
|
const visible = value;
|
|
|
|
|
|
if (this.visible !== visible) {
|
|
|
|
|
|
this.visible = visible;
|
|
|
|
|
|
this.tbVisibleChange.next(visible);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get tbVisible(): boolean {
|
|
|
|
|
|
return this.visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visible = false;
|
|
|
|
|
|
|
|
|
|
|
|
set tbHidden(value: boolean) {
|
|
|
|
|
|
const hidden = value;
|
|
|
|
|
|
if (this.hidden !== hidden) {
|
|
|
|
|
|
this.hidden = hidden;
|
|
|
|
|
|
if (this.hidden) {
|
|
|
|
|
|
this.renderer.setStyle(this.popoverRoot.nativeElement, 'width', this.popoverRoot.nativeElement.offsetWidth + 'px');
|
|
|
|
|
|
this.renderer.setStyle(this.popoverRoot.nativeElement, 'height', this.popoverRoot.nativeElement.offsetHeight + 'px');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.renderer.removeStyle(this.popoverRoot.nativeElement, 'width');
|
|
|
|
|
|
this.renderer.removeStyle(this.popoverRoot.nativeElement, 'height');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
this.updateStyles();
|
|
|
|
|
|
this.cdr.markForCheck();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get tbHidden(): boolean {
|
|
|
|
|
|
return this.hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hidden = false;
|
|
|
|
|
|
lastIsIntersecting = true;
|
|
|
|
|
|
|
|
|
|
|
|
set tbTrigger(value: TbPopoverTrigger) {
|
|
|
|
|
|
this.trigger = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get tbTrigger(): TbPopoverTrigger {
|
|
|
|
|
|
return this.trigger;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected trigger: TbPopoverTrigger = 'hover';
|
|
|
|
|
|
|
|
|
|
|
|
set tbPlacement(value: PopoverPlacement | PopoverPlacement[]) {
|
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
|
this.positions = [POSITION_MAP[value], ...DEFAULT_POPOVER_POSITIONS];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const preferredPosition = value.map(placement => POSITION_MAP[placement]);
|
|
|
|
|
|
this.positions = [...preferredPosition, ...DEFAULT_POPOVER_POSITIONS];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get hasBackdrop(): boolean {
|
|
|
|
|
|
return this.tbTrigger === 'click' ? this.tbBackdrop : false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
preferredPlacement: PopoverPlacement = 'top';
|
|
|
|
|
|
origin!: CdkOverlayOrigin;
|
|
|
|
|
|
public dir: Direction = 'ltr';
|
|
|
|
|
|
classMap: { [klass: string]: any } = {};
|
|
|
|
|
|
positions: ConnectionPositionPair[] = [...DEFAULT_POPOVER_POSITIONS];
|
|
|
|
|
|
private parentScrollSubscription: Subscription = null;
|
|
|
|
|
|
private intersectionObserver = new IntersectionObserver((entries) => {
|
|
|
|
|
|
if (this.lastIsIntersecting !== entries[0].isIntersecting) {
|
|
|
|
|
|
this.lastIsIntersecting = entries[0].isIntersecting;
|
|
|
|
|
|
this.updateStyles();
|
|
|
|
|
|
this.cdr.markForCheck();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, {threshold: [0.5]});
|
|
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
public cdr: ChangeDetectorRef,
|
|
|
|
|
|
private renderer: Renderer2,
|
|
|
|
|
|
@Optional() private directionality: Directionality
|
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
|
|
|
this.directionality.change?.pipe(takeUntil(this.tbDestroy)).subscribe((direction: Direction) => {
|
|
|
|
|
|
this.dir = direction;
|
|
|
|
|
|
this.cdr.detectChanges();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.dir = this.directionality.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
|
|
|
|
if (this.parentScrollSubscription) {
|
|
|
|
|
|
this.parentScrollSubscription.unsubscribe();
|
|
|
|
|
|
this.parentScrollSubscription = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.origin) {
|
|
|
|
|
|
const el = this.origin.elementRef.nativeElement;
|
|
|
|
|
|
this.intersectionObserver.unobserve(el);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.intersectionObserver.disconnect();
|
|
|
|
|
|
this.intersectionObserver = null;
|
|
|
|
|
|
this.tbVisibleChange.complete();
|
|
|
|
|
|
this.tbAnimationDone.complete();
|
|
|
|
|
|
this.tbDestroy.next();
|
|
|
|
|
|
this.tbDestroy.complete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
closeButtonClick($event: Event) {
|
|
|
|
|
|
if ($event) {
|
|
|
|
|
|
$event.preventDefault();
|
|
|
|
|
|
$event.stopPropagation();
|
|
|
|
|
|
}
|
|
|
|
|
|
this.hide();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
show(): void {
|
|
|
|
|
|
if (this.tbVisible) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.isEmpty()) {
|
|
|
|
|
|
this.tbVisible = true;
|
|
|
|
|
|
this.tbVisibleChange.next(true);
|
|
|
|
|
|
this.cdr.detectChanges();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.origin && this.overlay && this.overlay.overlayRef) {
|
|
|
|
|
|
if (this.overlay.overlayRef.getDirection() === 'rtl') {
|
|
|
|
|
|
this.overlay.overlayRef.setDirection('ltr');
|
|
|
|
|
|
}
|
|
|
|
|
|
const el = this.origin.elementRef.nativeElement;
|
|
|
|
|
|
this.parentScrollSubscription = onParentScrollOrWindowResize(el).subscribe(() => {
|
|
|
|
|
|
this.overlay.overlayRef.updatePosition();
|
|
|
|
|
|
});
|
|
|
|
|
|
this.intersectionObserver.observe(el);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hide(): void {
|
|
|
|
|
|
if (!this.tbVisible) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.parentScrollSubscription) {
|
|
|
|
|
|
this.parentScrollSubscription.unsubscribe();
|
|
|
|
|
|
this.parentScrollSubscription = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.origin) {
|
|
|
|
|
|
const el = this.origin.elementRef.nativeElement;
|
|
|
|
|
|
this.intersectionObserver.unobserve(el);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.tbVisible = false;
|
|
|
|
|
|
this.tbVisibleChange.next(false);
|
|
|
|
|
|
this.cdr.detectChanges();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateByDirective(): void {
|
|
|
|
|
|
this.updateStyles();
|
|
|
|
|
|
this.cdr.detectChanges();
|
|
|
|
|
|
|
|
|
|
|
|
Promise.resolve().then(() => {
|
|
|
|
|
|
this.updatePosition();
|
|
|
|
|
|
this.updateVisibilityByContent();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updatePosition(): void {
|
|
|
|
|
|
if (this.origin && this.overlay && this.overlay.overlayRef) {
|
|
|
|
|
|
this.overlay.overlayRef.updatePosition();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onPositionChange(position: ConnectedOverlayPositionChange): void {
|
|
|
|
|
|
this.preferredPlacement = getPlacementName(position);
|
|
|
|
|
|
this.updateStyles();
|
|
|
|
|
|
this.cdr.detectChanges();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateStyles(): void {
|
|
|
|
|
|
this.classMap = {
|
|
|
|
|
|
[this.tbOverlayClassName]: true,
|
|
|
|
|
|
[`tb-popover-placement-${this.preferredPlacement}`]: true,
|
|
|
|
|
|
['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setOverlayOrigin(origin: CdkOverlayOrigin): void {
|
|
|
|
|
|
this.origin = origin;
|
|
|
|
|
|
this.cdr.markForCheck();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onClickOutside(event: MouseEvent): void {
|
|
|
|
|
|
if (this.tbHideOnClickOutside && !this.origin.elementRef.nativeElement.contains(event.target) && this.tbTrigger !== null) {
|
|
|
|
|
|
this.hide();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onComponentChange(component: ComponentRef<any>) {
|
|
|
|
|
|
this.tbComponentRef = component;
|
|
|
|
|
|
this.tbComponentChange.next(component);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
animationDone() {
|
|
|
|
|
|
this.tbAnimationDone.next();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateVisibilityByContent(): void {
|
|
|
|
|
|
if (this.isEmpty()) {
|
|
|
|
|
|
this.hide();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private isEmpty(): boolean {
|
|
|
|
|
|
return (this.tbComponentFactory instanceof ComponentFactory || this.tbContent instanceof TemplateRef)
|
|
|
|
|
|
? false : !isNotEmptyStr(this.tbContent);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|