import { Component, Directive, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, Injector, Renderer2, ComponentRef, ElementRef, TemplateRef, ViewContainerRef, ComponentFactoryResolver, NgZone, SimpleChanges } from '@angular/core'; import {listenToTriggers} from '../util/triggers'; import {positionElements, Placement, 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, host: { '[class]': '"popover bs-popover-" + placement.split("-")[0]+" bs-popover-" + placement', 'role': 'tooltip', '[id]': 'id' }, template: `

{{title}}

`, styles: [` :host.bs-popover-top .arrow, :host.bs-popover-bottom .arrow { left: 50%; margin-left: -5px; } :host.bs-popover-top-left .arrow, :host.bs-popover-bottom-left .arrow { left: 2em; } :host.bs-popover-top-right .arrow, :host.bs-popover-bottom-right .arrow { left: auto; right: 2em; } :host.bs-popover-left .arrow, :host.bs-popover-right .arrow { top: 50%; margin-top: -5px; } :host.bs-popover-left-top .arrow, :host.bs-popover-right-top .arrow { top: 0.7em; } :host.bs-popover-left-bottom .arrow, :host.bs-popover-right-bottom .arrow { top: auto; bottom: 0.7em; } `] }) export class NgbPopoverWindow { @Input() placement: Placement = 'top'; @Input() title: string; @Input() id: string; constructor(private _element: ElementRef, private _renderer: Renderer2) {} applyPlacement(_placement: Placement) { // remove the current placement classes this._renderer.removeClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString().split('-')[0]); this._renderer.removeClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString()); // set the new placement classes this.placement = _placement; // apply the new placement this._renderer.addClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString().split('-')[0]); this._renderer.addClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString()); } } /** * A lightweight, extensible directive for fancy popover creation. */ @Directive({selector: '[ngbPopover]', exportAs: 'ngbPopover'}) export class NgbPopover implements OnInit, OnDestroy, OnChanges { /** * Content to be displayed as popover. If title and content are empty, the popover won't open. */ @Input() ngbPopover: string | TemplateRef; /** * Title of a popover. If title and content are empty, the popover won't open. */ @Input() popoverTitle: string; /** * Placement of a popover accepts: * "top", "top-left", "top-right", "bottom", "bottom-left", "bottom-right", * "left", "left-top", "left-bottom", "right", "right-top", "right-bottom" * and array of above values. */ @Input() placement: PlacementArray; /** * Specifies events that should trigger. Supports a space separated list of event names. */ @Input() triggers: string; /** * A selector specifying the element the popover should be appended to. * Currently only supports "body". */ @Input() container: string; /** * A flag indicating if a given popover is disabled and should not be displayed. * * @since 1.1.0 */ @Input() disablePopover: boolean; /** * Emits an event when the popover is shown */ @Output() shown = new EventEmitter(); /** * Emits an event when the popover is hidden */ @Output() hidden = new EventEmitter(); private _ngbPopoverWindowId = `ngb-popover-${nextId++}`; private _popupService: PopupService; private _windowRef: ComponentRef; 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, private _renderer: Renderer2, injector: Injector, componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: NgbPopoverConfig, ngZone: NgZone) { this.placement = config.placement; this.triggers = config.triggers; this.container = config.container; this.disablePopover = config.disablePopover; this._popupService = new PopupService( NgbPopoverWindow, injector, viewContainerRef, _renderer, componentFactoryResolver); this._zoneSubscription = ngZone.onStable.subscribe(() => { if (this._windowRef) { this._windowRef.instance.applyPlacement( positionElements( this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement, this.container === 'body')); } }); } /** * Opens an element’s popover. This is considered a “manual” triggering of the popover. * 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.id = this._ngbPopoverWindowId; this._renderer.setAttribute(this._elementRef.nativeElement, 'aria-describedby', this._ngbPopoverWindowId); if (this.container === 'body') { window.document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement); } // apply styling to set basic css-classes on target element, before going for positioning this._windowRef.changeDetectorRef.detectChanges(); this._windowRef.changeDetectorRef.markForCheck(); // position popover along the element this._windowRef.instance.applyPlacement( positionElements( this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement, this.container === 'body')); this.shown.emit(); } } /** * Closes an element’s popover. This is considered 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(); } } /** * Toggles an element’s popover. This is considered a “manual” triggering of the popover. */ toggle(): void { if (this._windowRef) { this.close(); } else { this.open(); } } /** * Returns whether or not the popover is currently being shown */ isOpen(): boolean { return this._windowRef != null; } ngOnInit() { this._unregisterListenersFn = listenToTriggers( this._renderer, this._elementRef.nativeElement, this.triggers, this.open.bind(this), this.close.bind(this), this.toggle.bind(this)); } ngOnChanges(changes: SimpleChanges) { // close popover if title and content become empty, or disablePopover set to true if ((changes['ngbPopover'] || changes['popoverTitle'] || changes['disablePopover']) && this._isDisabled()) { this.close(); } } ngOnDestroy() { this.close(); this._unregisterListenersFn(); this._zoneSubscription.unsubscribe(); } }