Skip to content

Instantly share code, notes, and snippets.

@Hagith
Last active January 24, 2020 16:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hagith/eab5f5c547b6fa852c789011b8cf08db to your computer and use it in GitHub Desktop.
Save Hagith/eab5f5c547b6fa852c789011b8cf08db to your computer and use it in GitHub Desktop.
ngx-bootstrap-dropdown-popper - https://stackblitz.com/edit/angular-msq1rb
$dropdown-bg: #fff;
$dropdown-arrow-size: 6px;
.dropdown-menu {
// reset basic dropdown position for Popper.js
&[x-placement^='top'],
&[x-placement^='right'],
&[x-placement^='bottom'],
&[x-placement^='left'] {
top: auto;
right: auto;
bottom: auto;
left: auto;
}
.dropdown-menu-arrow {
position: absolute;
width: $dropdown-arrow-size * 2;
height: $dropdown-arrow-size;
&:before,
&:after {
position: absolute;
border-style: solid;
content: '';
}
&:before {
top: 0;
left: 0;
border-width: $dropdown-arrow-size + 1;
border-color: rgba(0, 0, 0, 0.2);
}
&:after {
top: 1px;
left: 1px;
border-width: $dropdown-arrow-size;
border-color: $dropdown-bg;
}
}
&.with-arrow {
&[x-placement^='top'] {
margin-bottom: $dropdown-arrow-size;
.dropdown-menu-arrow {
bottom: -$dropdown-arrow-size - 1;
&:before,
&:after {
border-bottom-width: 0;
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
}
&:after {
top: -1px;
}
}
}
&[x-placement^='bottom'] {
margin-top: $dropdown-arrow-size;
.dropdown-menu-arrow {
top: -$dropdown-arrow-size - 1;
&:before,
&:after {
border-top-width: 0;
border-top-color: transparent;
border-right-color: transparent;
border-left-color: transparent;
}
}
}
&[x-placement^='left'] {
margin-right: $dropdown-arrow-size;
.dropdown-menu-arrow {
width: $dropdown-arrow-size;
height: $dropdown-arrow-size * 2;
right: -$dropdown-arrow-size - 1;
&:before,
&:after {
border-right-width: 0;
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
}
&:before {
border-left-color: rgba(0, 0, 0, 0.12);
}
&:after {
left: -1px;
}
}
}
&[x-placement^='right'] {
margin-left: $dropdown-arrow-size;
.dropdown-menu-arrow {
width: $dropdown-arrow-size;
height: $dropdown-arrow-size * 2;
left: -$dropdown-arrow-size - 1;
&:before,
&:after {
border-left-width: 0;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
}
&:before {
border-right-color: rgba(0, 0, 0, 0.12);
}
&:after {
right: -1px;
}
}
}
}
}
import {
ApplicationRef,
Directive,
EmbeddedViewRef,
Renderer2,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
@Directive({
selector: '[sysDropdownMenu]',
exportAs: 'sysDropdownMenu',
})
export class SysDropdownMenuDirective {
element: HTMLElement;
arrow: HTMLElement;
private _viewRef: EmbeddedViewRef<any>;
constructor(
private _viewContainer: ViewContainerRef,
private _templateRef: TemplateRef<any>,
private _renderer: Renderer2,
private _applicationRef: ApplicationRef
) {}
create(arrow = true, container: string = null): HTMLElement {
const element = container ? this._attachTo(container) : this._attachInline();
this._renderer.addClass(element, 'dropdown-menu');
this._renderer.addClass(element, 'show');
if (arrow) {
this._renderer.addClass(element, 'with-arrow');
this.arrow = this._renderer.createElement('div');
this._renderer.addClass(this.arrow, 'dropdown-menu-arrow');
this._renderer.appendChild(element, this.arrow);
}
return element;
}
destroy() {
if (this._viewRef) {
this._viewRef.destroy();
}
}
private _attachInline(): HTMLElement {
this._viewRef = this._viewContainer.createEmbeddedView(this._templateRef);
this.element = this._viewRef.rootNodes[0];
return this.element;
}
private _attachTo(container: string): HTMLElement {
this._viewRef = this._templateRef.createEmbeddedView({});
this._applicationRef.attachView(this._viewRef);
this.element = this._viewRef.rootNodes[0];
this._renderer.setAttribute(this.element, 'tabindex', '-1');
const containerElement = document.querySelector(container) || document.querySelector('body');
this._renderer.appendChild(containerElement, this.element);
this.element.focus({ preventScroll: true });
return this.element;
}
}
import { Directive, ElementRef, EventEmitter, HostListener, Input } from '@angular/core';
@Directive({
selector: '[sysDropdownToggle]',
exportAs: 'sysDropdownToggle',
host: {
'[class.dropdown-toggle]': 'caret',
'[attr.aria-haspopup]': 'true',
'[attr.aria-expanded]': 'isOpen',
},
})
export class SysDropdownToggleDirective {
@Input() caret = true;
isOpen = false;
toggleClick = new EventEmitter<boolean>();
constructor(public host: ElementRef<HTMLElement>) {}
get element() {
return this.host.nativeElement;
}
@HostListener('click', ['$event']) onClick(event: MouseEvent): void {
event.stopPropagation();
this.isOpen = !this.isOpen;
this.toggleClick.emit(true);
}
}
import {
AfterContentInit,
ChangeDetectorRef,
ContentChild,
Directive,
EventEmitter,
Input,
OnDestroy,
Output,
Renderer2,
} from '@angular/core';
import Popper, { Placement, PopperOptions } from 'popper.js';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { SysDropdownMenuDirective } from './dropdown-menu.directive';
import { SysDropdownToggleDirective } from './dropdown-toggle.directive';
@Directive({
selector: '[sysDropdown]',
exportAs: 'sysDropdown',
host: {
class: 'dropdown',
'[class.show]': 'open',
'[class.open]': 'open',
},
})
export class SysDropdownDirective implements AfterContentInit, OnDestroy {
@Input() placement: Placement = 'bottom-end';
@Input() container: string;
@Input() withArrow = false;
@Input() insideClick = false;
@Output() shown = new EventEmitter<void>();
@Output() hidden = new EventEmitter<void>();
open = false;
@ContentChild(SysDropdownToggleDirective, { static: false })
private _toggle: SysDropdownToggleDirective;
@ContentChild(SysDropdownMenuDirective, { static: false })
private _menu: SysDropdownMenuDirective;
private _popper: Popper;
private _listeners: (() => void)[] = [];
private _destroy: Subject<boolean> = new Subject<boolean>();
constructor(private _renderer: Renderer2, private _changeDetectorRef: ChangeDetectorRef) {}
ngAfterContentInit(): void {
this._toggle.toggleClick.pipe(takeUntil(this._destroy)).subscribe(() => this.toggle());
}
ngOnDestroy(): void {
this._destroy.next(true);
this._destroy.complete();
}
show() {
const menu = this._menu.create(this.withArrow, this.container);
const popperOptions: PopperOptions = {
placement: this.placement,
modifiers: {},
};
if (this.withArrow) {
popperOptions.modifiers.arrow = {
element: this._menu.arrow,
};
}
this._changeDetectorRef.detectChanges(); // prevents positioning issue
this._popper = new Popper(this._toggle.element, menu, popperOptions);
this._listen();
this.shown.emit();
}
hide() {
this._popper.destroy();
this._menu.destroy();
this._dispose();
this.hidden.emit();
}
toggle() {
this.open = !this.open;
this.open ? this.show() : this.hide();
}
private _listen() {
const documentListener = this._renderer.listen('document', 'click', (event: MouseEvent) => {
if (!this._toggle.element.contains(event.target as Element)) {
this.toggle();
this._changeDetectorRef.markForCheck();
}
});
this._listeners.push(documentListener);
const escListener = this._renderer.listen(this._menu.element, 'keyup.esc', () => {
this.toggle();
this._changeDetectorRef.markForCheck();
});
this._listeners.push(escListener);
if (this.insideClick) {
const insideListener = this._renderer.listen(this._menu.element, 'click', (e: MouseEvent) =>
e.stopPropagation()
);
this._listeners.push(insideListener);
}
}
private _dispose() {
this._listeners.forEach(dispose => dispose());
}
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SysDropdownMenuDirective } from './dropdown-menu.directive';
import { SysDropdownToggleDirective } from './dropdown-toggle.directive';
import { SysDropdownDirective } from './dropdown.directive';
@NgModule({
imports: [CommonModule],
exports: [SysDropdownDirective, SysDropdownToggleDirective, SysDropdownMenuDirective],
declarations: [SysDropdownDirective, SysDropdownToggleDirective, SysDropdownMenuDirective],
})
export class SysDropdownModule {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment