Skip to content

Instantly share code, notes, and snippets.

@amcdnl
Created March 31, 2018 15:28
Show Gist options
  • Save amcdnl/c2d986dbd7f7836a383c378599e721bc to your computer and use it in GitHub Desktop.
Save amcdnl/c2d986dbd7f7836a383c378599e721bc to your computer and use it in GitHub Desktop.
import { Directive, ViewContainerRef, ElementRef, Input, TemplateRef, OnInit, OnDestroy, NgZone } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Overlay, OverlayRef, OriginConnectionPosition, OverlayConnectionPosition } from '@angular/cdk/overlay';
import { fromEvent } from 'rxjs/observable/fromEvent';
import { takeUntil } from 'rxjs/operators';
import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
import { PopoverComponent } from './popover.component';
import { Subscription } from 'rxjs/Subscription';
@Directive({
selector: '[dfPopover]'
})
export class PopoverTriggerDirective implements OnInit, OnDestroy {
@Input('popoverTrigger') trigger: 'hover' = 'hover';
@Input('popoverDelay') delay = 100;
@Input('popoverContent') content: string;
@Input('popoverTemplate') template: TemplateRef<any>;
@Input('popoverTemplateContext') context: any;
@Input('popoverMargin') margin: number;
@Input('popoverPosition') position: 'left' | 'right' | 'above' | 'below' | 'before' | 'after' = 'above';
private _mouseLeaveSubscription: Subscription;
private _contextSubscription: Subscription;
private _destroy$: Subject<void>;
private _overlayRef: OverlayRef;
private _timeout: any;
constructor(
private _overlay: Overlay,
private _elementRef: ElementRef,
private _ngZone: NgZone
) {}
ngOnInit() {
this.hookupTriggers();
}
ngOnDestroy() {
this.hide(0);
if (this._destroy$) {
this._destroy$.next();
this._destroy$.complete();
}
}
hookupTriggers() {
this._destroy$ = new Subject();
if (this.trigger === 'hover') {
fromEvent(this._elementRef.nativeElement, 'mouseenter')
.pipe(takeUntil(this._destroy$))
.subscribe(() => this.show());
}
}
show() {
if (!this.content && !this.template) {
throw new Error('Template or content is not defined.');
}
if (this.trigger === 'hover') {
this._mouseLeaveSubscription =
fromEvent(this._elementRef.nativeElement, 'mouseleave')
.subscribe(() => this.hide());
}
// if a user does a right click, hide the popover
this._contextSubscription =
fromEvent(this._elementRef.nativeElement, 'contextmenu')
.subscribe(() => this.hide(0));
clearTimeout(this._timeout);
this._timeout = setTimeout(() => {
// if the overlay is already opened, don't reopen it
if (this._overlayRef) return;
const originPosition = this.getOriginPosition();
const overlayPosition = this.getOverlayPosition();
const positionStrategy = this._overlay
.position()
.flexibleConnectedTo(this._elementRef)
.withFlexibleWidth(false)
.withFlexibleHeight(false)
.withViewportMargin(this.margin)
.withPositions([{ ...originPosition, ...overlayPosition }]);
this._overlayRef = this._overlay.create({ positionStrategy });
const componentPortal = new ComponentPortal(PopoverComponent);
const componentRef = this._overlayRef.attach(componentPortal);
// virtual scroll causing popovers to mess up sometimes
this._ngZone.run(() => {
componentRef.instance.content = this.content;
componentRef.instance.template = this.template;
componentRef.instance.context = this.context;
});
}, this.delay);
}
getOriginPosition(): OriginConnectionPosition {
if (this.position === 'above' || this.position === 'below') {
return {
originX: 'center',
originY: this.position === 'above' ? 'top' : 'bottom'
};
} else if (this.position === 'before' || this.position === 'left') {
return { originX: 'start', originY: 'center' };
} else if (this.position === 'after' || this.position === 'right') {
return { originX: 'end', originY: 'center' };
}
}
getOverlayPosition(): OverlayConnectionPosition {
if (this.position === 'above') {
return { overlayX: 'center', overlayY: 'bottom' };
} else if (this.position === 'below') {
return { overlayX: 'center', overlayY: 'top' };
} else if (this.position === 'before' || this.position === 'left') {
return { overlayX: 'end', overlayY: 'center' };
} else if (this.position === 'after' || this.position === 'right') {
return { overlayX: 'start', overlayY: 'center' };
}
}
hide(delay = this.delay) {
clearTimeout(this._timeout);
this._timeout = setTimeout(() => {
if (this._overlayRef) {
this._overlayRef.detach();
this._overlayRef.dispose();
this._overlayRef = undefined;
if (this._mouseLeaveSubscription) {
this._mouseLeaveSubscription.unsubscribe();
}
if (this._contextSubscription) {
this._contextSubscription.unsubscribe();
}
}
}, delay);
}
}
import { Component, ChangeDetectionStrategy, Input, TemplateRef, ElementRef } from '@angular/core';
@Component({
selector: 'df-popover',
template: `
<div class="popover">
<span *ngIf="content" [innerHTML]="content"></span>
<ng-template
*ngIf="template"
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ context: context }">
</ng-template>
</div>
`,
host: {
'class': 'mat-elevation-z2'
},
styleUrls: ['./popover.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PopoverComponent {
@Input() content: string;
@Input() template: TemplateRef<any>;
@Input() context: any;
constructor(public elementRef: ElementRef) {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment