| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197 |
1×
1×
1×
1×
1×
1×
1×
1×
1×
3×
1×
3×
1×
3×
3×
2×
2×
12×
1×
12×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
3×
1×
1×
1×
2×
2×
1×
1×
1×
1×
1×
1×
| /*
* @license
* Copyright Hôpitaux Universitaires de Genève. All Rights Reserved.
*
* Use of this source code is governed by an Apache-2.0 license that can be
* found in the LICENSE file at https://github.com/DSI-HUG/dejajs-components/blob/master/LICENSE
*/
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Component, ContentChild, ElementRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { from as observableFrom, fromEvent as observableFromEvent, Observable } from 'rxjs';
import { debounceTime, delay, filter, map, takeUntil, tap } from 'rxjs/operators';
import { Position } from '../../common/core/graphics/position';
import { Rect } from '../../common/core/graphics/rect';
import { DejaConnectionPositionPair } from '../../common/core/overlay/connection-position-pair';
import { DejaTooltipService, ITooltipParams } from './tooltip.service';
/**
* Customizable tooltip component for Angular
*/
@Component({
encapsulation: ViewEncapsulation.None,
selector: 'deja-tooltip',
template: require('./tooltip.component.html'),
styles: [
require('./tooltip.component.scss'),
],
})
export class DejaTooltipComponent implements OnInit {
/**
* This position config ensures that the top "start" corner of the overlay
* is aligned with with the top "start" of the origin by default (overlapping
* the trigger completely). If the panel cannot fit below the trigger, it
* will fall back to a position above the trigger.
*/
private _positions = [
{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top',
},
{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom',
},
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom',
},
{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top',
},
{
originX: 'end',
originY: 'top',
overlayX: 'end',
overlayY: 'bottom',
},
] as DejaConnectionPositionPair[];
/** Tooltip name. Mandatory, and need to be unic */
@Input() public name: string;
/** Event Emmited when hide action is called */
@Output() public hide = new EventEmitter();
/** Template for tooltip content */
@ContentChild('tooltipTemplate')
public tooltipTemplate: any;
/** Parameters of the tooltip */
public params: ITooltipParams;
public overlayVisible = false;
public ownerElement: HTMLElement;
private _model: any;
private _closeOnMoveOver = false;
@Input()
public set closeOnMoveOver(value: boolean) {
this._closeOnMoveOver = coerceBooleanProperty(value);
}
public get closeOnMoveOver() {
return this._closeOnMoveOver;
}
@Input()
public set positions(value: DejaConnectionPositionPair[] | string) {
this._positions = typeof value === 'string' ? DejaConnectionPositionPair.parse(value) : value;
}
public get positions() {
return this._positions;
}
public get model() {
return this._model;
}
/**
* Constructor
* Subscribe to mouseover to know when tooltip must disappear.
*/
constructor(elementRef: ElementRef, private tooltipService: DejaTooltipService) {
const element = elementRef.nativeElement as HTMLElement;
const hide$ = observableFrom(this.hide).pipe(
tap(() => this._model = undefined));
observableFromEvent(element.ownerDocument, 'mousemove').pipe(
takeUntil(hide$),
debounceTime(100),
map((event: MouseEvent) => new Position(event.pageX, event.pageY)),
filter((position) => {
Iif (this._closeOnMoveOver) {
return true;
}
const containerElement = document.elementFromPoint(position.left, position.top);
let parentElement = containerElement;
while (parentElement) {
Iif (parentElement.className === 'cdk-overlay-pane') {
return false;
}
parentElement = parentElement.parentElement;
}
return true;
}),
filter((position) => {
Iif (this._closeOnMoveOver) {
return true;
}
const ownerElement = (this.params.ownerElement as ElementRef).nativeElement || this.params.ownerElement;
const ownerRect = new Rect(ownerElement.getBoundingClientRect());
return !ownerRect.containsPoint(position);
}),
delay(300))
.subscribe(() => {
this.hide.emit();
this.overlayVisible = false;
});
}
/**
* Init tooltip configuration
* Check if ng-template model passed through param is an observable or a promise and resolve it before set.
*/
public ngOnInit() {
Iif (!this.name) {
throw (new Error('Name is required'));
}
this.params = this.tooltipService.params[this.name];
this.ownerElement = (this.params.ownerElement as ElementRef).nativeElement || this.params.ownerElement;
const model$ = this.params.model as Observable<any>;
Iif (!model$) {
this._model = undefined;
this.overlayVisible = true;
} else if (model$.subscribe) {
model$.subscribe((model) => {
this._model = model;
this.overlayVisible = true;
}, () => {
this.hide.emit();
this.overlayVisible = false;
});
} else {
const promise = this.params.model as Promise<any>;
if (promise.then) {
promise
.then((model) => {
this._model = model;
this.overlayVisible = true;
})
.catch(() => {
this.hide.emit();
this.overlayVisible = false;
});
} else {
this._model = this.params.model;
this.overlayVisible = true;
}
}
}
}
|