Skip to content

Commit a606a7f

Browse files
authored
feat(popover): added outsideClick (#2441)
* feat(popover): added outsideClick fixes #1477 * fix(test): fix tests, change listenOpts.target type
1 parent 7bb7eec commit a606a7f

File tree

6 files changed

+111
-24
lines changed

6 files changed

+111
-24
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<button type="button" class="btn btn-primary"
2-
popover="Vivamus sagittis lacus vel augue laoreet rutrum faucibus.">
2+
popover="Vivamus sagittis lacus vel augue laoreet rutrum faucibus."
3+
[outsideClick]="true">
34
Live demo
45
</button>
56

src/component-loader/component-loader.class.ts

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,9 @@ import {
1818
ViewContainerRef
1919
} from '@angular/core';
2020
import { PositioningOptions, PositioningService } from '../positioning';
21-
import { listenToTriggers } from '../utils/triggers';
21+
import { listenToTriggersV2, registerOutsideClick } from '../utils/triggers';
2222
import { ContentRef } from './content-ref.class';
23-
24-
export interface ListenOptions {
25-
target?: ElementRef;
26-
triggers?: string;
27-
show?: Function;
28-
hide?: Function;
29-
toggle?: Function;
30-
}
23+
import { ListenOptions } from './listen-options.model';
3124

3225
export class ComponentLoader<T> {
3326
public onBeforeShow: EventEmitter<any> = new EventEmitter();
@@ -209,19 +202,45 @@ export class ComponentLoader<T> {
209202
this.triggers = listenOpts.triggers || this.triggers;
210203

211204
listenOpts.target = listenOpts.target || this._elementRef;
212-
listenOpts.show = listenOpts.show || (() => this.show());
213-
listenOpts.hide = listenOpts.hide || (() => this.hide());
214-
listenOpts.toggle = listenOpts.toggle || (() => this.isShown
215-
? listenOpts.hide()
216-
: listenOpts.show());
217-
218-
this._unregisterListenersFn = listenToTriggers(
219-
this._renderer,
220-
listenOpts.target.nativeElement,
221-
this.triggers,
222-
listenOpts.show,
223-
listenOpts.hide,
224-
listenOpts.toggle);
205+
let _removeGlobalListener = Function.prototype;
206+
207+
const hide = () => {
208+
if (listenOpts.hide) {
209+
listenOpts.hide();
210+
} else {
211+
this.hide();
212+
}
213+
214+
_removeGlobalListener();
215+
};
216+
217+
const show = (registerHide: Function) => {
218+
listenOpts.show ? listenOpts.show() : this.show();
219+
// register hide listeners
220+
// register outsideClick
221+
registerHide();
222+
if (this._componentRef && this._componentRef.location) {
223+
// why: should run after first event bubble
224+
const target = this._componentRef.location;
225+
setTimeout(() => {
226+
_removeGlobalListener = registerOutsideClick(this._renderer, {
227+
target,
228+
outsideClick: listenOpts.outsideClick,
229+
hide
230+
});
231+
});
232+
}
233+
};
234+
235+
const toggle = (registerHide: Function) => {
236+
this.isShown ? hide() : show(registerHide);
237+
};
238+
239+
this._unregisterListenersFn = listenToTriggersV2(this._renderer, {
240+
target: listenOpts.target,
241+
triggers: listenOpts.triggers,
242+
show, hide, toggle
243+
});
225244

226245
return this;
227246
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ElementRef } from '@angular/core';
2+
3+
export interface ListenOptions {
4+
target?: ElementRef | HTMLElement;
5+
triggers?: string;
6+
outsideClick?: boolean;
7+
show?: Function;
8+
hide?: Function;
9+
toggle?: Function;
10+
}

src/popover/popover.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export class PopoverConfig {
1717
* event names.
1818
*/
1919
public triggers: string = 'click';
20+
21+
outsideClick = false;
2022
/**
2123
* A selector specifying the element the popover should be appended to.
2224
* Currently only supports "body".

src/popover/popover.directive.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export class PopoverDirective implements OnInit, OnDestroy {
2323
* Placement of a popover. Accepts: "top", "bottom", "left", "right"
2424
*/
2525
@Input() public placement: 'top' | 'bottom' | 'left' | 'right';
26+
27+
@Input() outsideClick = false;
2628
/**
2729
* Specifies events that should trigger. Supports a space separated list of
2830
* event names.
@@ -138,6 +140,7 @@ export class PopoverDirective implements OnInit, OnDestroy {
138140

139141
this._popover.listen({
140142
triggers: this.triggers,
143+
outsideClick: this.outsideClick,
141144
show: () => this.show()
142145
});
143146
}

src/utils/triggers.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
* @copyright Valor Software
33
* @copyright Angular ng-bootstrap team
44
*/
5-
import { Renderer } from '@angular/core';
5+
import { ElementRef, Renderer } from '@angular/core';
66
import { Trigger } from './trigger.class';
7+
import { ListenOptions } from '../component-loader/listen-options.model';
78

89
const DEFAULT_ALIASES = {
910
hover: ['mouseover', 'mouseout'],
@@ -60,3 +61,54 @@ export function listenToTriggers(renderer: Renderer, target: any, triggers: stri
6061

6162
return () => { listeners.forEach((unsubscribeFn: Function) => unsubscribeFn()); };
6263
}
64+
65+
export function listenToTriggersV2(renderer: Renderer, options: ListenOptions): Function {
66+
const parsedTriggers = parseTriggers(options.triggers);
67+
const target = options.target.hasOwnProperty('nativeElement') ? (options.target as ElementRef).nativeElement : options.target;
68+
// do nothing
69+
if (parsedTriggers.length === 1 && parsedTriggers[0].isManual()) {
70+
return Function.prototype;
71+
}
72+
73+
// all listeners
74+
const listeners: any[] = [];
75+
76+
// lazy listeners registration
77+
const _registerHide: Function[] = [];
78+
const registerHide = () => {
79+
// add hide listeners to unregister array
80+
_registerHide.forEach((fn) => listeners.push(fn()));
81+
// register hide events only once
82+
_registerHide.length = 0;
83+
};
84+
85+
// register open\close\toggle listeners
86+
parsedTriggers.forEach((trigger: Trigger) => {
87+
const useToggle = trigger.open === trigger.close;
88+
const showFn = useToggle ? options.toggle : options.show;
89+
90+
if (!useToggle) {
91+
_registerHide.push(() => renderer.listen(target, trigger.close, options.hide));
92+
}
93+
94+
listeners.push(renderer.listen(target, trigger.open, () => showFn(registerHide)));
95+
});
96+
97+
// register outside click
98+
// _registerHide.push(() => registerOutsideClick(renderer, options));
99+
return () => {
100+
listeners.forEach((unsubscribeFn: Function) => unsubscribeFn());
101+
};
102+
}
103+
104+
export function registerOutsideClick(renderer: Renderer, options: ListenOptions) {
105+
if (!options.outsideClick) {
106+
return Function.prototype;
107+
}
108+
const target = options.target.hasOwnProperty('nativeElement') ? (options.target as ElementRef).nativeElement : options.target;
109+
return renderer.listenGlobal('document', 'click', (event: any) => {
110+
if (!target.contains(event.target)) {
111+
options.hide();
112+
}
113+
});
114+
}

0 commit comments

Comments
 (0)