-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
tooltip.ts
282 lines (249 loc) · 8.98 KB
/
tooltip.ts
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import {
Component,
Directive,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
Inject,
Injector,
Renderer2,
ComponentRef,
ElementRef,
TemplateRef,
ViewContainerRef,
ComponentFactoryResolver,
NgZone,
ViewEncapsulation,
ChangeDetectorRef,
ApplicationRef,
OnChanges,
SimpleChanges
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {listenToTriggers} from '../util/triggers';
import {ngbAutoClose} from '../util/autoclose';
import {positionElements, PlacementArray} from '../util/positioning';
import {PopupService} from '../util/popup';
import {NgbTooltipConfig} from './tooltip-config';
let nextId = 0;
@Component({
selector: 'ngb-tooltip-window',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {'[class]': '"tooltip show" + (tooltipClass ? " " + tooltipClass : "")', 'role': 'tooltip', '[id]': 'id'},
template: `<div class="arrow"></div><div class="tooltip-inner"><ng-content></ng-content></div>`,
styleUrls: ['./tooltip.scss']
})
export class NgbTooltipWindow implements OnChanges {
@Input() id: string;
@Input() tooltipClass: string;
constructor(private _changeDetector: ChangeDetectorRef) {}
ngOnChanges(changes: SimpleChanges): void {
const tooltipClassChange = changes['tooltipClass'];
if (tooltipClassChange && !tooltipClassChange.firstChange) {
this._changeDetector.markForCheck();
}
}
}
/**
* A lightweight and extensible directive for fancy tooltip creation.
*/
@Directive({selector: '[ngbTooltip]', exportAs: 'ngbTooltip'})
export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
/**
* Indicates whether the tooltip should be closed on `Escape` key and inside/outside clicks:
*
* * `true` - closes on both outside and inside clicks as well as `Escape` presses
* * `false` - disables the autoClose feature (NB: triggers still apply)
* * `"inside"` - closes on inside clicks as well as Escape presses
* * `"outside"` - closes on outside clicks (sometimes also achievable through triggers)
* as well as `Escape` presses
*
* @since 3.0.0
*/
@Input() autoClose: boolean | 'inside' | 'outside';
/**
* The preferred placement of the tooltip.
*
* Possible values are `"top"`, `"top-left"`, `"top-right"`, `"bottom"`, `"bottom-left"`,
* `"bottom-right"`, `"left"`, `"left-top"`, `"left-bottom"`, `"right"`, `"right-top"`,
* `"right-bottom"`
*
* Accepts an array of strings or a string with space separated possible values.
*
* The default order of preference is `"auto"` (same as the sequence above).
*
* Please see the [positioning overview](#/positioning) for more details.
*/
@Input() placement: PlacementArray;
/**
* Specifies events that should trigger the tooltip.
*
* Supports a space separated list of event names.
* For more details see the [triggers demo](#/components/tooltip/examples#triggers).
*/
@Input() triggers: string;
/**
* A selector specifying the element the tooltip should be appended to.
*
* Currently only supports `"body"`.
*/
@Input() container: string;
/**
* If `true`, tooltip is disabled and won't be displayed.
*
* @since 1.1.0
*/
@Input() disableTooltip: boolean;
/**
* An optional class applied to the tooltip window element.
*
* @since 3.2.0
*/
@Input() tooltipClass: string;
/**
* The opening delay in ms. Works only for "non-manual" opening triggers defined by the `triggers` input.
*
* @since 4.1.0
*/
@Input() openDelay: number;
/**
* The closing delay in ms. Works only for "non-manual" opening triggers defined by the `triggers` input.
*
* @since 4.1.0
*/
@Input() closeDelay: number;
/**
* An event emitted when the tooltip is shown. Contains no payload.
*/
@Output() shown = new EventEmitter();
/**
* An event emitted when the popover is hidden. Contains no payload.
*/
@Output() hidden = new EventEmitter();
private _ngbTooltip: string | TemplateRef<any>;
private _ngbTooltipWindowId = `ngb-tooltip-${nextId++}`;
private _popupService: PopupService<NgbTooltipWindow>;
private _windowRef: ComponentRef<NgbTooltipWindow>;
private _unregisterListenersFn;
private _zoneSubscription: any;
constructor(
private _elementRef: ElementRef<HTMLElement>, private _renderer: Renderer2, injector: Injector,
componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: NgbTooltipConfig,
private _ngZone: NgZone, @Inject(DOCUMENT) private _document: any, private _changeDetector: ChangeDetectorRef,
private _applicationRef: ApplicationRef) {
this.autoClose = config.autoClose;
this.placement = config.placement;
this.triggers = config.triggers;
this.container = config.container;
this.disableTooltip = config.disableTooltip;
this.tooltipClass = config.tooltipClass;
this.openDelay = config.openDelay;
this.closeDelay = config.closeDelay;
this._popupService = new PopupService<NgbTooltipWindow>(
NgbTooltipWindow, injector, viewContainerRef, _renderer, componentFactoryResolver, _applicationRef);
this._zoneSubscription = _ngZone.onStable.subscribe(() => {
if (this._windowRef) {
positionElements(
this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement,
this.container === 'body', 'bs-tooltip');
}
});
}
/**
* The string content or a `TemplateRef` for the content to be displayed in the tooltip.
*
* If the content if falsy, the tooltip won't open.
*/
@Input()
set ngbTooltip(value: string | TemplateRef<any>) {
this._ngbTooltip = value;
if (!value && this._windowRef) {
this.close();
}
}
get ngbTooltip() { return this._ngbTooltip; }
/**
* Opens the tooltip.
*
* This is considered to be a "manual" triggering.
* The `context` is an optional value to be injected into the tooltip template when it is created.
*/
open(context?: any) {
if (!this._windowRef && this._ngbTooltip && !this.disableTooltip) {
this._windowRef = this._popupService.open(this._ngbTooltip, context);
this._windowRef.instance.tooltipClass = this.tooltipClass;
this._windowRef.instance.id = this._ngbTooltipWindowId;
this._renderer.setAttribute(this._elementRef.nativeElement, 'aria-describedby', this._ngbTooltipWindowId);
if (this.container === 'body') {
this._document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement);
}
// We need to detect changes, because we don't know where .open() might be called from.
// Ex. opening tooltip from one of lifecycle hooks that run after the CD
// (say from ngAfterViewInit) will result in 'ExpressionHasChanged' exception
this._windowRef.changeDetectorRef.detectChanges();
// We need to mark for check, because tooltip won't work inside the OnPush component.
// Ex. when we use expression like `{{ tooltip.isOpen() : 'opened' : 'closed' }}`
// inside the template of an OnPush component and we change the tooltip from
// open -> closed, the expression in question won't be updated unless we explicitly
// mark the parent component to be checked.
this._windowRef.changeDetectorRef.markForCheck();
ngbAutoClose(
this._ngZone, this._document, this.autoClose, () => this.close(), this.hidden,
[this._windowRef.location.nativeElement]);
this.shown.emit();
}
}
/**
* Closes the tooltip.
*
* This is considered to be a "manual" triggering of the tooltip.
*/
close(): void {
if (this._windowRef != null) {
this._renderer.removeAttribute(this._elementRef.nativeElement, 'aria-describedby');
this._popupService.close();
this._windowRef = null;
this.hidden.emit();
this._changeDetector.markForCheck();
}
}
/**
* Toggles the tooltip.
*
* This is considered to be a "manual" triggering of the tooltip.
*/
toggle(): void {
if (this._windowRef) {
this.close();
} else {
this.open();
}
}
/**
* Returns `true`, if the popover is currently shown.
*/
isOpen(): boolean { return this._windowRef != null; }
ngOnInit() {
this._unregisterListenersFn = listenToTriggers(
this._renderer, this._elementRef.nativeElement, this.triggers, this.isOpen.bind(this), this.open.bind(this),
this.close.bind(this), +this.openDelay, +this.closeDelay);
}
ngOnChanges({tooltipClass}: SimpleChanges) {
if (tooltipClass && !tooltipClass.firstChange && this._windowRef) {
this._windowRef.instance.tooltipClass = tooltipClass.currentValue;
}
}
ngOnDestroy() {
this.close();
// This check is needed as it might happen that ngOnDestroy is called before ngOnInit
// under certain conditions, see: https://github.com/ng-bootstrap/ng-bootstrap/issues/2199
if (this._unregisterListenersFn) {
this._unregisterListenersFn();
}
this._zoneSubscription.unsubscribe();
}
}