forked from ng-bootstrap/ng-bootstrap
/
popover.ts
296 lines (264 loc) · 9.52 KB
/
popover.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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import {
Component,
Directive,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
OnChanges,
Inject,
Injector,
Renderer2,
ComponentRef,
ElementRef,
TemplateRef,
ViewContainerRef,
ComponentFactoryResolver,
NgZone,
SimpleChanges,
ViewEncapsulation,
ChangeDetectorRef,
ApplicationRef
} 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 {NgbPopoverConfig} from './popover-config';
let nextId = 0;
@Component({
selector: 'ngb-popover-window',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {'[class]': '"popover" + (popoverClass ? " " + popoverClass : "")', 'role': 'tooltip', '[id]': 'id'},
template: `
<div class="arrow"></div>
<h3 class="popover-header" *ngIf="title != null">
<ng-template #simpleTitle>{{title}}</ng-template>
<ng-template [ngTemplateOutlet]="isTitleTemplate() ? title : simpleTitle" [ngTemplateOutletContext]="context"></ng-template>
</h3>
<div class="popover-body"><ng-content></ng-content></div>`,
styleUrls: ['./popover.scss']
})
export class NgbPopoverWindow {
@Input() title: undefined | string | TemplateRef<any>;
@Input() id: string;
@Input() popoverClass: string;
@Input() context: any;
isTitleTemplate() { return this.title instanceof TemplateRef; }
}
/**
* A lightweight and extensible directive for fancy popover creation.
*/
@Directive({selector: '[ngbPopover]', exportAs: 'ngbPopover'})
export class NgbPopover implements OnInit, OnDestroy, OnChanges {
/**
* Indicates whether the popover 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 string content or a `TemplateRef` for the content to be displayed in the popover.
*
* If the title and the content are empty, the popover won't open.
*/
@Input() ngbPopover: string | TemplateRef<any>;
/**
* The title of the popover.
*
* If the title and the content are empty, the popover won't open.
*/
@Input() popoverTitle: string | TemplateRef<any>;
/**
* The preferred placement of the popover.
*
* 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/popover/examples#triggers).
*/
@Input() triggers: string;
/**
* A selector specifying the element the popover should be appended to.
*
* Currently only supports `body`.
*/
@Input() container: string;
/**
* If `true`, popover is disabled and won't be displayed.
*
* @since 1.1.0
*/
@Input() disablePopover: boolean;
/**
* An optional class applied to the popover window element.
*
* @since 2.2.0
*/
@Input() popoverClass: 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 popover is shown. Contains no payload.
*/
@Output() shown = new EventEmitter<void>();
/**
* An event emitted when the popover is hidden. Contains no payload.
*/
@Output() hidden = new EventEmitter<void>();
private _ngbPopoverWindowId = `ngb-popover-${nextId++}`;
private _popupService: PopupService<NgbPopoverWindow>;
private _windowRef: ComponentRef<NgbPopoverWindow>;
private _unregisterListenersFn;
private _zoneSubscription: any;
private _isDisabled(): boolean {
if (this.disablePopover) {
return true;
}
if (!this.ngbPopover && !this.popoverTitle) {
return true;
}
return false;
}
constructor(
private _elementRef: ElementRef<HTMLElement>, private _renderer: Renderer2, injector: Injector,
componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: NgbPopoverConfig,
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.disablePopover = config.disablePopover;
this.popoverClass = config.popoverClass;
this.openDelay = config.openDelay;
this.closeDelay = config.closeDelay;
this._popupService = new PopupService<NgbPopoverWindow>(
NgbPopoverWindow, 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-popover');
}
});
}
/**
* Opens the popover.
*
* This is considered to be a "manual" triggering.
* The `context` is an optional value to be injected into the popover template when it is created.
*/
open(context?: any) {
if (!this._windowRef && !this._isDisabled()) {
this._windowRef = this._popupService.open(this.ngbPopover, context);
this._windowRef.instance.title = this.popoverTitle;
this._windowRef.instance.context = context;
this._windowRef.instance.popoverClass = this.popoverClass;
this._windowRef.instance.id = this._ngbPopoverWindowId;
this._renderer.setAttribute(this._elementRef.nativeElement, 'aria-describedby', this._ngbPopoverWindowId);
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 popover 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 popover won't work inside the OnPush component.
// Ex. when we use expression like `{{ popover.isOpen() : 'opened' : 'closed' }}`
// inside the template of an OnPush component and we change the popover 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 popover.
*
* This is considered to be a "manual" triggering of the popover.
*/
close(): void {
if (this._windowRef) {
this._renderer.removeAttribute(this._elementRef.nativeElement, 'aria-describedby');
this._popupService.close();
this._windowRef = null;
this.hidden.emit();
this._changeDetector.markForCheck();
}
}
/**
* Toggles the popover.
*
* This is considered to be a "manual" triggering of the popover.
*/
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({ngbPopover, popoverTitle, disablePopover, popoverClass}: SimpleChanges) {
if (popoverClass && this.isOpen()) {
this._windowRef.instance.popoverClass = popoverClass.currentValue;
}
// close popover if title and content become empty, or disablePopover set to true
if ((ngbPopover || popoverTitle || disablePopover) && this._isDisabled()) {
this.close();
}
}
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();
}
}