Skip to content

Commit

Permalink
feat(ngrid/overlay-panel): plugin that helps poping up overlay panels
Browse files Browse the repository at this point in the history
  • Loading branch information
shlomiassaf committed Jun 26, 2019
1 parent 0cfe0d3 commit cedd949
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 0 deletions.
5 changes: 5 additions & 0 deletions libs/ngrid/overlay-panel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { PblNgridOverlayPanelComponentExtension } from './lib/component-registry-extension';
export { PblNgridOverlayPanelRef } from './lib/overlay-panel-ref';
export { PblNgridOverlayPanelFactory, PblNgridOverlayPanel, PblNgridOverlayPanelConfig } from './lib/overlay-panel.service';
export { PblNgridOverlayPanelDef, PblNgridOverlayPanelContext } from './lib/overlay-panel-def';
export * from './lib/overlay-panel.module';
26 changes: 26 additions & 0 deletions libs/ngrid/overlay-panel/lib/component-registry-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

import { ComponentRef, Type, ComponentFactoryResolver, ComponentFactory, Injector } from '@angular/core';
import { PblNgridMultiComponentRegistry } from '@pebula/ngrid';

export class PblNgridOverlayPanelComponentExtension<T> extends PblNgridMultiComponentRegistry<T, 'overlayPanels'> {
readonly name: string;
readonly kind: 'overlayPanels' = 'overlayPanels';
readonly projectContent = false;

constructor(name: string,
public component: Type<T>,
public cfr?: ComponentFactoryResolver,
public injector?: Injector,) {
super();
this.name = name;
}

getFactory(context: any): ComponentFactory<T> {
return this.cfr.resolveComponentFactory(this.component);
}

onCreated(context: any, cmpRef: ComponentRef<T>): void {
cmpRef.changeDetectorRef.markForCheck();
cmpRef.changeDetectorRef.detectChanges();
}
}
17 changes: 17 additions & 0 deletions libs/ngrid/overlay-panel/lib/overlay-panel-def.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Directive, TemplateRef, Input } from '@angular/core';
import { PblNgridComponent, PblNgridMultiTemplateRegistry, PblNgridRegistryService } from '@pebula/ngrid';
import { PblNgridOverlayPanelRef } from './overlay-panel-ref';

export interface PblNgridOverlayPanelContext<T = any> {
grid: PblNgridComponent<T>;
ref: PblNgridOverlayPanelRef;
}

@Directive({ selector: '[pblNgridOverlayPanelDef]' })
export class PblNgridOverlayPanelDef extends PblNgridMultiTemplateRegistry<PblNgridComponent, 'overlayPanels'> {

readonly kind: 'overlayPanels' = 'overlayPanels';
@Input('pblNgridOverlayPanelDef') name: string;

constructor(tRef: TemplateRef<PblNgridComponent>, registry: PblNgridRegistryService) { super(tRef, registry); }
}
36 changes: 36 additions & 0 deletions libs/ngrid/overlay-panel/lib/overlay-panel-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { merge, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { OverlayRef } from '@angular/cdk/overlay';

export class PblNgridOverlayPanelRef<T = any> {

closed: Observable<void>;
private _closed$ = new Subject<void>();

constructor(private overlayRef: OverlayRef, public readonly data?: T) {
this.closed = this._closed$.asObservable();
this._closingActions(this, overlayRef)
.pipe(
takeUntil(this.closed),
)
.subscribe(() => this.close());
}

close(): void {
if (this._closed$) {
const closed$ = this._closed$;
this._closed$ = undefined;
closed$.next();
closed$.complete();
this.overlayRef.detach();
this.overlayRef.dispose();
}
}

private _closingActions(overlayPanelRef: PblNgridOverlayPanelRef, overlayRef: OverlayRef) {
const backdrop = overlayRef!.backdropClick();
const detachments = overlayRef!.detachments();

return merge(backdrop, detachments, overlayPanelRef.closed);
}
}
28 changes: 28 additions & 0 deletions libs/ngrid/overlay-panel/lib/overlay-panel.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BidiModule } from '@angular/cdk/bidi';
import { OverlayModule } from '@angular/cdk/overlay';

import { PblNgridOverlayPanelFactory } from './overlay-panel.service';
import { PblNgridOverlayPanelDef } from './overlay-panel-def';

@NgModule({
imports: [
CommonModule,
OverlayModule,
BidiModule,
],
declarations: [
PblNgridOverlayPanelDef,
],
exports: [
PblNgridOverlayPanelDef,
],
providers: [
PblNgridOverlayPanelFactory,
],
entryComponents: [ ],
})
export class PblNgridOverlayPanelModule {

}
245 changes: 245 additions & 0 deletions libs/ngrid/overlay-panel/lib/overlay-panel.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { Injectable, ViewContainerRef, ElementRef, Injector, EmbeddedViewRef, TemplateRef } from '@angular/core';
import { Directionality } from '@angular/cdk/bidi';
import {
FlexibleConnectedPositionStrategy,
HorizontalConnectionPos,
Overlay,
OverlayConfig,
OverlayRef,
VerticalConnectionPos,
ScrollStrategy,
} from '@angular/cdk/overlay';
import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
import { RowContext } from '@angular/cdk/table';
import { PblNgridPluginController, PblNgridComponent, PblNgridMultiTemplateRegistry } from '@pebula/ngrid';

import { PblNgridOverlayPanelComponentExtension } from './component-registry-extension';
import { PblNgridOverlayPanelRef } from './overlay-panel-ref';
import { PblNgridOverlayPanelContext } from './overlay-panel-def';

declare module '@pebula/ngrid/lib/table/services/table-registry.service' {
interface PblNgridMultiRegistryMap {
overlayPanels?:
| PblNgridMultiTemplateRegistry<any, 'overlayPanels'>
| PblNgridOverlayPanelComponentExtension<any>;
}
}

export interface PblNgridOverlayPanelConfig {
hasBackdrop?: boolean;
backdropClass?: string;
xPos?: 'before' | 'center' | 'after';
yPos?: 'above' | 'center' | 'below';
insetPos?: boolean;
}

const DEFAULT_OVERLAY_PANEL_CONFIG: PblNgridOverlayPanelConfig = {
hasBackdrop: false,
xPos: 'center',
yPos: 'center',
insetPos: false,
};

@Injectable()
export class PblNgridOverlayPanelFactory {
constructor(private _overlay: Overlay, private _dir: Directionality) { }

create<T>(grid: PblNgridComponent<T>): PblNgridOverlayPanel<T> {
return new PblNgridOverlayPanel<T>(this._overlay, this._dir, grid);
}
}

export class PblNgridOverlayPanel<T = any> {

private vcRef: ViewContainerRef;
private injector: Injector;
private _scrollStrategy: () => ScrollStrategy;

constructor(private _overlay: Overlay,
private _dir: Directionality,
public readonly grid: PblNgridComponent<T>) {
const controller = PblNgridPluginController.find(grid);
this.injector = controller.injector;
this.vcRef = controller.injector.get(ViewContainerRef);
this._scrollStrategy = () => _overlay.scrollStrategies.reposition();
}


/**
* Opens a panel relative to a cell element using the overlay panel extension registry template/component with the name provided in `extName`.
* The cell element is referenced by the `columnId` and the `rowRenderPosition`.
*
* If the `rowRenderPosition` is "header" or "footer" then the grid's header / footer rows are targeted, otherwise the number provided should reference
* the rendered row index to use to get the cell from.
*
* > Note that this helper method does not allow targeting meta cells.
*/
openGridCell<T = any>(extName: string, columnId: string, rowRenderPosition: number | 'header' | 'footer', config?: PblNgridOverlayPanelConfig, data?: T): PblNgridOverlayPanelRef<T> {
const column = this.grid.columnApi.findColumn(columnId);
if (!column) {
throw new Error('Could not find the column ' + columnId);
}

let section: 'table' | 'header' | 'footer';
let rowRenderIndex = 0;
switch (rowRenderPosition) {
case 'header':
case 'footer':
section = rowRenderPosition;
break;
default:
if (typeof rowRenderPosition === 'number') {
section = 'table';
rowRenderIndex = rowRenderPosition;
}
break;
}

if (!section) {
throw new Error('Invalid "rowRenderPosition" provided, use "header", "footer" or any number >= 0.');
}

const el = column && column.columnDef.queryCellElements(section)[rowRenderIndex];
if (!el) {
throw new Error(`Could not find a cell for the column ${columnId} at render index ${rowRenderIndex}`);
}

return this.open(extName, new ElementRef(el), config, data);
}

open<T = any>(extName: string, source: ElementRef<HTMLElement>, config?: PblNgridOverlayPanelConfig, data?: T): PblNgridOverlayPanelRef<T> {
config = Object.assign({ ...DEFAULT_OVERLAY_PANEL_CONFIG }, config || {});
const match = this.findNamesExtension(extName);

if (!match) {
throw new Error('Could not find the overlay panel with the name ' + extName);
}

const overlayRef = this._createOverlay(source, config);
const overlayPanelRef = new PblNgridOverlayPanelRef(overlayRef, data);
this._setPosition(overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy, config);

if (match instanceof PblNgridMultiTemplateRegistry) {
const tPortal = this._getTemplatePortal(match.tRef, overlayPanelRef);
const viewRef = overlayRef.attach(tPortal);
viewRef.markForCheck();
viewRef.detectChanges();
} else {
const cPortal = this._getComponentPortal(overlayPanelRef, match)
const cmpRef = overlayRef.attach(cPortal);
match.onCreated(null, cmpRef);
}

overlayRef.updatePosition();
return overlayPanelRef;
}

/**
* This method creates the overlay from the provided menu's template and saves its
* OverlayRef so that it can be attached to the DOM when openMenu is called.
*/
private _createOverlay(element: ElementRef<HTMLElement>, config: PblNgridOverlayPanelConfig): OverlayRef {
const overlayConfig = this._getOverlayConfig(element, config);
const overlayRef = this._overlay.create(overlayConfig);
overlayRef.getConfig().hasBackdrop = !!config.hasBackdrop
// Consume the `keydownEvents` in order to prevent them from going to another overlay.
// Ideally we'd also have our keyboard event logic in here, however doing so will
// break anybody that may have implemented the `MatMenuPanel` themselves.
overlayRef.keydownEvents().subscribe();

return overlayRef;
}

/**
* This method builds the configuration object needed to create the overlay, the OverlayState.
* @returns OverlayConfig
*/
private _getOverlayConfig(element: ElementRef<HTMLElement>, config: PblNgridOverlayPanelConfig): OverlayConfig {
return new OverlayConfig({
positionStrategy: this._overlay.position()
.flexibleConnectedTo(element)
.withLockedPosition(),
backdropClass: config.backdropClass || 'cdk-overlay-transparent-backdrop', // TODO: don't use the cdk's class, create it
scrollStrategy: this._scrollStrategy(),
direction: this._dir
});
}

private _getTemplatePortal(tRef: TemplateRef<PblNgridOverlayPanelContext>, overlayPanelRef: PblNgridOverlayPanelRef) {
const context: PblNgridOverlayPanelContext = {
grid: this.grid,
ref: overlayPanelRef,
};
return new TemplatePortal(tRef, this.vcRef, context);
}

private _getComponentPortal(overlayPanelRef: PblNgridOverlayPanelRef,
componentExtension: PblNgridOverlayPanelComponentExtension<any>) {
const portalInjector = Injector.create({
providers: [
{ provide: PblNgridOverlayPanelRef, useValue: overlayPanelRef },
],
parent: componentExtension.injector || this.injector,
});
return new ComponentPortal(componentExtension.component, this.vcRef, portalInjector, componentExtension.cfr || null)
}

private _setPosition(positionStrategy: FlexibleConnectedPositionStrategy, config: PblNgridOverlayPanelConfig) {
let [originX, originFallbackX]: HorizontalConnectionPos[] =
config.xPos === 'center'
? ['center', 'center']
: config.xPos === 'before' ? ['end', 'start'] : ['start', 'end'];

let [overlayY, overlayFallbackY]: VerticalConnectionPos[] =
config.yPos === 'center'
? ['center', 'center']
: config.yPos === 'above' ? ['bottom', 'top'] : ['top', 'bottom'];

let [originY, originFallbackY] = [overlayY, overlayFallbackY];
let [overlayX, overlayFallbackX] = [originX, originFallbackX];
let offsetY = 0;

if (!config.insetPos) {
if (overlayY !== 'center') {
originY = overlayY === 'top' ? 'bottom' : 'top';
}
if (overlayFallbackY !== 'center') {
originFallbackY = overlayFallbackY === 'top' ? 'bottom' : 'top';
}
}

positionStrategy.withPositions([
{originX, originY, overlayX, overlayY, offsetY},
{originX: originFallbackX, originY, overlayX: overlayFallbackX, overlayY, offsetY},
{
originX,
originY: originFallbackY,
overlayX,
overlayY: overlayFallbackY,
offsetY: -offsetY
},
{
originX: originFallbackX,
originY: originFallbackY,
overlayX: overlayFallbackX,
overlayY: overlayFallbackY,
offsetY: -offsetY
}
]);
}

private findNamesExtension(extName: string) {
let match: PblNgridMultiTemplateRegistry<PblNgridOverlayPanelContext, 'overlayPanels'> | PblNgridOverlayPanelComponentExtension<any>;
this.grid.registry.forMulti('overlayPanels', values => {
for (const value of values) {
if (value.name === extName) {
match = value;
return true;
}
}
});
return match;
}
}


0 comments on commit cedd949

Please sign in to comment.