ngx-contextual-action-bar
Version:
ngx-contextual-action-bar is an Angular2+ implementation of the [Material Design Contextual Action Bar](https://material.io/components/app-bars-top#contextual-action-bar). The implementation makes it possible to change the action bar properties (i.e. titl
546 lines (528 loc) • 25.3 kB
JavaScript
import { EventEmitter, ɵɵdefineInjectable, Injectable, Component, ViewEncapsulation, ViewChild, ElementRef, Input, Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ComponentPortal, CdkPortalOutlet, PortalModule } from '@angular/cdk/portal';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
import { BehaviorSubject, fromEvent, combineLatest, of } from 'rxjs';
import { filter, map, mapTo, startWith, pairwise, scan, switchMap, debounceTime } from 'rxjs/operators';
import { v4 } from 'uuid';
import { trigger, transition, style, animate, query, group, state, animateChild } from '@angular/animations';
import xor from 'arr-xor';
var ActionBarLayerModes;
(function (ActionBarLayerModes) {
/** Action bar acts as traditional content, and can be scrolled out of view. */
ActionBarLayerModes["fixed"] = "fixed";
/** Action bar follows the view, and always stays in place. */
ActionBarLayerModes["follow"] = "follow";
/** Action bar can be scrolled out of view, but will return when scrolled back. */
ActionBarLayerModes["mobile"] = "mobile";
})(ActionBarLayerModes || (ActionBarLayerModes = {}));
class ActionBarLayerRegistration {
constructor(service, _layer) {
this.service = service;
this._layer = _layer;
this.service._addLayer(_layer);
// this.service._customElementLoaded.subscribe((s) => {})
// this.onCustomElementReady.subscribe(s => console.log(s))
}
get onCustomElementReady() {
return this.service._customElement.pipe(filter((v) => v !== undefined), filter((ev) => ev[0] === this._layer.id), map(v => v[1]));
}
// public onCustomElementReady: Observable<ComponentType<T>> = this.service._customElementLoaded.pipe(
// // filter(([id]) => id === this._layer.id),
// // take(1),
// tap(e => console.log('meme')),
// map(([,comp]) => {
// this.instance = comp;
// return comp;
// })
// )
get onButtonClick() {
return this.service.buttonEmitter.pipe(filter(id => id === this._layer.id), mapTo(undefined));
}
get onActionClick() {
return this.service.actionEmitter.pipe(filter(([id, action]) => id === this._layer.id), map(([id, action]) => action));
}
get layer() {
return this._layer;
}
/**
*
* @param layer Object containing properties to update
*/
setLayer(layer) {
const value = this.service._layers.value;
const idx = value.findIndex(e => e.id === this.layer.id);
if (idx > -1) {
value[idx] = Object.assign(value[idx], layer);
this.service._layers.next(value);
this._layer = value[idx];
}
}
unregister() {
const id = this._layer.id;
this.service._removeLayer(id);
}
}
class ActionBarLayerToggleRegistration {
constructor(service, _layer, _toggleLayer) {
this.service = service;
this._layer = _layer;
this._toggleLayer = _toggleLayer;
this.toggled = false;
this.service._addLayer(_layer);
}
get onButtonClick() {
return this.service.buttonEmitter.pipe(filter(id => {
return id === this._layer.id || id === this._toggleLayer.id;
}), map(id => {
return id === this._layer.id ? 0 : 1;
}));
}
get onActionClick() {
return this.service.actionEmitter.pipe(filter(([id, action]) => {
return id === this._layer.id || id === this._toggleLayer.id;
}), map(([id, action]) => {
return [id === this._layer.id ? 0 : 1, action];
}));
}
toggleLayer() {
if (!this.toggled)
this.service._addLayer(this._toggleLayer);
else
this.service._removeLayer(this._toggleLayer.id);
this.toggled = !this.toggled;
}
unregister() {
this.service._removeLayer(this._layer.id);
this.service._removeLayer(this._toggleLayer.id);
}
}
const DEFAULT_LAYER_OPTIONS = {
title: '',
group: 'root',
actions: [],
mode: ActionBarLayerModes.fixed,
prominent: false
};
class ContextualActionBarService {
constructor() {
/**
*
* Internal property, use at own risk.
*/
this._layers = new BehaviorSubject([]);
this.buttonEmitter = new EventEmitter();
this.actionEmitter = new EventEmitter();
// public _customElementLoaded: EventEmitter<[string, ComponentType<any>]> = new EventEmitter<[string, ComponentType<any>]>();
this._customElement = new BehaviorSubject(undefined);
}
/**
* Internal function used to retrieve most recent layer.
* @param group - what group to target
*/
latest(group = 'root') {
return this._layers.asObservable().pipe(
//filter by group
map(layers => layers.filter(layer => layer.group === group)),
//get latest layer
map(layers => layers[layers.length - 1]));
}
/**
* Internal function
* @param id id of layer to remove
*/
_removeLayer(id) {
const { value } = this._layers;
this._layers.next(value.filter(layer => layer.id !== id));
}
/**
* Internal function
* @param layer - The layer to add
*/
_addLayer(layer) {
const { value } = this._layers;
this._layers.next([...value, layer]);
}
applyMissingProperties(layer) {
const id = v4();
return Object.assign(Object.assign({}, DEFAULT_LAYER_OPTIONS), Object.assign({ id }, layer));
}
validateLayer(layer) {
return true;
}
/**
* Function to register a new layer. Returns a registration
* @param layerOptions
*/
register(layerOptions) {
const layer = this.applyMissingProperties(layerOptions);
if (this.validateLayer(layer)) {
return new ActionBarLayerRegistration(this, layer);
}
else {
throw new Error('Please validate that you have filled all required properties in your layer');
}
}
/**
* Use at own risk, is set to be deprecated after next major version.
* @param layer
* @param toggleLayer
*/
registerToggle(layer, toggleLayer) {
const _layer = this.applyMissingProperties(layer);
const _toggleLayer = this.applyMissingProperties(toggleLayer);
if (this.validateLayer(_layer) && this.validateLayer(_toggleLayer)) {
return new ActionBarLayerToggleRegistration(this, _layer, _toggleLayer);
}
else {
throw new Error('Please validate that you have filled all required properties in your layers');
}
}
}
ContextualActionBarService.ɵprov = ɵɵdefineInjectable({ factory: function ContextualActionBarService_Factory() { return new ContextualActionBarService(); }, token: ContextualActionBarService, providedIn: "root" });
ContextualActionBarService.decorators = [
{ type: Injectable, args: [{
providedIn: 'root'
},] }
];
ContextualActionBarService.ctorParameters = () => [];
// elements that begin and end at resting position
const standard = 'cubic-bezier(0.4, 0.0, 0.2, 1)';
// incoming elements
const decelerated = 'cubic-bezier(0.0, 0.0, 0.2, 1)';
// outgoing elements
const accelerated = 'cubic-bezier(0.4, 0.0, 1, 1)';
const ɵ0 = (from, to, el) => {
return from == to;
}, ɵ1 = (from, to, element) => {
return ((from != 'void') && to == 'void');
}, ɵ2 = (from, to, element) => {
return ((from == 'void') && to != 'void');
};
const buttonAnimation = trigger('buttonAnimation', [
transition(ɵ0, []),
transition(ɵ1, [
style({ width: '!', overflow: 'hidden', opacity: 1 }),
animate(`90ms ${accelerated}`, style({ opacity: 0 })),
animate(`110ms ${standard}`, style({ width: 0 }))
]),
transition(ɵ2, [
style({ width: 0, overflow: 'hidden', opacity: 0 }),
animate(`90ms ${accelerated}`, style({ width: '*' })),
animate(`110ms ${standard}`, style({ opacity: 1 }))
]),
// transition((from, to) => {
// return from === undefined && to !== undefined
// }, [
// query('.new', [
// style({ opacity: 0 }),
// ])
// ]),
transition(('* <=> *'), [
query('.new', [
style({ opacity: 0, transform: 'rotate(180deg)' })
]),
group([
query('.old', [
style({ display: 'block' }),
animate(`150ms ${accelerated}`, style({ opacity: 0, transform: 'rotate(180deg)' })),
]),
query('.new', [
animate(`150ms ${decelerated}`, style({ opacity: 1, transform: 'rotate(360deg)' }))
]),
])
]),
]);
// based on transition area
const small_enter = '200ms';
const small_leave = small_enter;
const medium_enter = '250ms';
const medium_leave = '200ms';
const large_enter = '300ms';
const large_leave = '250ms';
const navbarAnimation = trigger('navbar', [
state('transparent', style({ position: 'absolute' })),
state('fixed', style({ position: 'relative' })),
state('visible', style({ position: 'sticky' })),
transition('fixed => visible', [
style({ position: 'sticky', transform: 'translateY(-100%)' }),
animate(`${medium_enter} ${decelerated}`, style({ transform: 'translateY(0%)' })),
]),
transition('visible => fixed', [
style({ position: 'sticky', transform: 'translateY(0%)' }),
animate(`${medium_leave} ${accelerated}`, style({ transform: 'translateY(-100%)' })),
style({ position: 'relative' })
]),
transition('* => noanim', [])
]);
// none, prominent, regular
const paddingAnimation = trigger('padding', [
transition('* => prominent', [
style({ paddingTop: 128 }),
animate(`${medium_enter} ${standard}`, style({ paddingTop: 0 }))
]),
transition('* => regular', [
style({ paddingTop: 56 }),
animate(`${medium_enter} ${standard}`, style({ paddingTop: 0 }))
]),
transition('* => none', [
style({ padding: '!' }),
animate(`${medium_enter} ${standard}`, style({ padding: 0 }))
])
]);
const ɵ0$1 = (from, to) => {
return from == 'void';
};
const layerSwitchAnimation = trigger('layerSwitch', [
transition(ɵ0$1, [
query('*', animate(0))
]),
transition(':enter', [
query('*', animate(0))
]),
transition('* => *', [
query('*', animateChild(), { optional: false })
])
]);
class ContextualActionBarComponent {
constructor(service) {
this.service = service;
this.scrollThreshold = 200;
this.group = 'root';
}
handleButtonClick(id) {
this.service.buttonEmitter.emit(id);
}
ngOnInit() {
this.layer$ = this.service.latest(this.group);
this.layer$.pipe(
// distinctUntilChanged((a,b) => a?.id === b?.id)
)
.subscribe(layer => {
var _a;
if ((_a = this.outlet) === null || _a === void 0 ? void 0 : _a.hasAttached()) {
this.outlet.ngOnDestroy();
this.outlet.detach();
}
if (layer === null || layer === void 0 ? void 0 : layer.middleElement) {
const ref = this.outlet.attachComponentPortal(new ComponentPortal(layer.middleElement));
this.service._customElement.next([layer.id, ref.instance]);
}
});
this.layerSwitchTrigger$ = this.layer$.pipe(map(layer => layer === null || layer === void 0 ? void 0 : layer.id));
this.scroll = fromEvent(this.content.nativeElement, 'scroll').pipe(map(event => event.target.scrollTop), startWith(0));
this.netScroll$ = this.scroll.pipe(startWith(0), pairwise(), scan((acc, cur) => {
const [a, b] = cur;
if (b > a) {
if (acc < 0)
return 0;
}
else {
if (acc > 0)
return 0;
}
return acc + (b - a);
}, 0));
this.button$ = this.layer$.pipe(map(layer => layer === null || layer === void 0 ? void 0 : layer.button), startWith(undefined), pairwise());
this.navbarState$ = combineLatest([this.scroll, this.netScroll$, this.layer$]).pipe(scan((prev, [scroll, netscroll, layer]) => {
// spaget 🍝
if ((layer === null || layer === void 0 ? void 0 : layer.background) === 'transparent')
return 'transparent';
if ((layer === null || layer === void 0 ? void 0 : layer.mode) === ActionBarLayerModes.follow) {
if (prev === 'noanim')
return 'fixed';
return scroll > 1 ? 'visible' : 'nonaim';
}
else if ((layer === null || layer === void 0 ? void 0 : layer.mode) === ActionBarLayerModes.fixed)
return 'fixed';
else {
if (prev === 'noanim')
return 'fixed';
else if (prev === 'fixed') {
if (scroll < ((layer === null || layer === void 0 ? void 0 : layer.prominent) ? 128 : 56))
return 'fixed';
}
else {
if (scroll < 1)
return 'noanim';
}
return netscroll < -this.scrollThreshold ? 'visible' : 'fixed';
}
}, 'fixed'));
this.shadow$ = this.layer$.pipe(switchMap(layer => {
if ((layer === null || layer === void 0 ? void 0 : layer.mode) === ActionBarLayerModes.follow) {
return this.scroll.pipe(map(v => v > 1));
}
else if ((layer === null || layer === void 0 ? void 0 : layer.background) === 'transparent') {
return of(false);
}
return this.navbarState$.pipe(map(v => v === 'visible'));
}));
this.contentState$ = this.layer$.pipe(map(layer => {
if (!layer)
return 'none';
if (layer.background !== 'transparent')
return 'none';
return layer.prominent ? 'prominent' : 'regular';
}));
}
ngOnDestroy() {
// this.outlet.ngOnDestroy();
// this.outlet?.dispose();
}
}
ContextualActionBarComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-contextual-action-bar',
template: "<ng-template #navbar let-layer=\"layer\">\r\n <div class=\"button\"\r\n *ngIf=\"(button$ | async); let button\"\r\n >\r\n <button mat-icon-button *ngIf=\"button[1]\" (click)=\"handleButtonClick(layer.id)\" [@buttonAnimation]=\"button\">\r\n <!-- <button mat-icon-button [@buttonAnimation]=\"button\" (click)=\"handleButtonClick(layer.id)\"> -->\r\n <div class=\"wrapper\">\r\n <mat-icon class=\"old\">{{button[0]}}</mat-icon>\r\n <mat-icon class=\"new\">{{button[1]}}</mat-icon>\r\n </div>\r\n </button>\r\n </div>\r\n <div class=\"title\">\r\n <div class=\"custom-element\">\r\n <div cdkPortalOutlet></div> \r\n </div>\r\n <h2 *ngIf=\"layer?.title\">{{layer.title}}</h2>\r\n </div>\r\n\r\n <div class=\"actions\" *ngIf=\"layer?.actions.length > 0\">\r\n <ngx-actions-overflow-menu [group]=\"group\"></ngx-actions-overflow-menu>\r\n </div>\r\n</ng-template>\r\n<div class=\"component\"\r\n [class]=\"(layer$ | async)?.mode\"\r\n [class.prominent]=\"(layer$ | async)?.prominent\"\r\n [@layerSwitch]=\"layerSwitchTrigger$ | async\"\r\n> \r\n <div class=\"content\" #c cdkScrollable\r\n [@padding]=\"contentState$ | async\"\r\n >\r\n \r\n \r\n <nav class=\"navbar\"\r\n [style.color]=\"(layer$ | async)?.color\"\r\n [style.background]=\"(layer$ | async)?.background\"\r\n [@navbar]=\"navbarState$ | async\"\r\n [class.shadow]=\"(shadow$ | async)\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"navbar; context: { layer: layer$ | async }\"></ng-container>\r\n </nav>\r\n <mat-divider></mat-divider>\r\n <ng-content></ng-content>\r\n </div>\r\n</div>",
host: {
'class': 'ngx-contextual-action-bar'
},
animations: [
navbarAnimation,
buttonAnimation,
paddingAnimation,
layerSwitchAnimation
],
encapsulation: ViewEncapsulation.Emulated,
styles: [":host{height:inherit;width:inherit}.component,:host{position:relative}.component{display:flex;flex-direction:column;height:100%;width:100%;z-index:16}.component .content{overflow-x:hidden;overflow-y:auto;position:relative;z-index:0}.navbar{box-sizing:border-box;flex-direction:row;height:56px;left:0;top:0;transition:box-shadow .28s cubic-bezier(.4,0,.2,1);transition-property:height,box-shadow,padding-bottom,color,background;width:100%;z-index:3}.navbar,.navbar .button{display:flex;position:relative}.navbar .button{flex-direction:column;height:100%;padding:8px}.navbar .button button,.navbar .button button .wrapper{align-items:center;display:flex;height:40px;justify-content:center;position:relative}.navbar .button button .wrapper{width:40px}.navbar .button button .mat-icon{position:absolute}.navbar .button button .mat-icon.old{display:none}.navbar .title{align-items:flex-end;display:flex;flex:2 1 auto;padding-bottom:10px;padding-left:16px;padding-right:64px;position:relative}.navbar .title h2{margin:0}.navbar .title .custom-element{bottom:0;height:56px;left:0;position:absolute;width:100%}.navbar .actions{display:flex;flex:0.5 1 50px;flex-direction:column;height:100%;min-width:0;overflow:hidden;position:relative}.navbar .actions .ngx-actions-overflow-menu{flex:0 0 56px}.navbar.shadow{box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.component.prominent>.content>.navbar{height:128px;padding-bottom:8px}.component.transparent .navbar{position:absolute}"]
},] }
];
ContextualActionBarComponent.ctorParameters = () => [
{ type: ContextualActionBarService }
];
ContextualActionBarComponent.propDecorators = {
content: [{ type: ViewChild, args: ['c', { read: ElementRef, static: true },] }],
outlet: [{ type: ViewChild, args: [CdkPortalOutlet,] }],
scrollThreshold: [{ type: Input }],
group: [{ type: Input }]
};
class ScrollhandlerDirective {
constructor(el) {
this.el = el;
this.scrollHeight = fromEvent(this.el.nativeElement, 'scroll').pipe(debounceTime(10), map(e => {
return this.el.nativeElement.scrollTop;
}));
this.scrolled$ = this.scrollHeight.pipe(pairwise(), scan((acc, cur) => {
const [a, b] = cur;
if (b > a) {
if (acc < 50)
return 0;
}
else {
if (acc > 50)
return 0;
}
return acc + (b - a);
}, 0));
}
ngOnDestroy() {
}
}
ScrollhandlerDirective.decorators = [
{ type: Directive, args: [{
selector: '[ngxScrollhandler]',
exportAs: 'scroll'
},] }
];
ScrollhandlerDirective.ctorParameters = () => [
{ type: ElementRef }
];
const ActionAnimation = trigger('action', [
transition(':enter', [
style({ opacity: 0 }),
animate('300ms ease', style({ opacity: 1 }))
]),
]);
const ICON_WIDTH = 40;
class DmlActionsOverflowMenuComponent {
constructor(service) {
this.service = service;
this.group = 'root';
this._resizeEvent$ = fromEvent(window, 'resize').pipe(startWith(undefined), debounceTime(100));
}
handleActionClick(id, action) {
this.service.actionEmitter.emit([id, action.icon]);
}
ngOnInit() {
this.layer$ = this.service.latest(this.group).pipe(map(layer => {
if (!layer)
return undefined;
return { id: layer.id, actions: layer.actions };
}));
this.width = this._resizeEvent$.pipe(map(_ => this.c.nativeElement.getBoundingClientRect().width));
this.visible$ = combineLatest([this.width, this.layer$]).pipe(map(([width, layer]) => {
if (!(layer === null || layer === void 0 ? void 0 : layer.actions))
return [];
let max_visible = Math.floor(width / ICON_WIDTH) - 1;
let visible = max_visible > layer.actions.length ? layer.actions.length : max_visible;
if (visible + 1 == layer.actions.length)
return layer.actions;
return layer.actions.slice(0, visible);
}));
this.menu$ = combineLatest([this.visible$, this.layer$]).pipe(map(([visible, layer]) => {
return xor(visible, (layer === null || layer === void 0 ? void 0 : layer.actions) || [], (a, b) => {
return a.icon === b.icon;
});
}));
}
}
DmlActionsOverflowMenuComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-actions-overflow-menu',
template: "<div #c class=\"component\" >\r\n <ng-container *ngIf=\"layer$ | async as layer\">\r\n <!-- visible -->\r\n <button @action mat-icon-button *ngFor=\"let action of visible$ | async\" (click)=\"handleActionClick(layer.id, action)\">\r\n <mat-icon>{{action.icon}}</mat-icon>\r\n </button>\r\n <!-- menu -->\r\n <ng-container *ngIf=\"menu$ | async as menu\">\r\n <mat-menu #matMenu>\r\n <button mat-menu-item *ngFor=\"let action of menu\" (click)=\"handleActionClick(layer.id, action)\">\r\n <mat-icon>{{action.icon}}</mat-icon>\r\n <span>{{action.displayName}}</span>\r\n </button>\r\n </mat-menu>\r\n <button @action class=\"menu-trigger\" [matMenuTriggerFor]=\"matMenu\" mat-icon-button *ngIf=\"menu.length > 0\">\r\n <mat-icon>more_vert</mat-icon>\r\n </button>\r\n </ng-container>\r\n </ng-container>\r\n</div>",
host: {
class: 'ngx-actions-overflow-menu'
},
animations: [
ActionAnimation,
],
styles: [":host{height:inherit;width:inherit}.component,:host{overflow:hidden;position:relative}.component{align-items:center;box-sizing:border-box;display:flex;height:100%;justify-content:flex-end;min-width:0;width:100%}.component button{min-width:0;overflow:hidden}"]
},] }
];
DmlActionsOverflowMenuComponent.ctorParameters = () => [
{ type: ContextualActionBarService }
];
DmlActionsOverflowMenuComponent.propDecorators = {
group: [{ type: Input }],
c: [{ type: ViewChild, args: ['c', { read: ElementRef, static: true },] }]
};
const MATERIAL_MODULES = [
MatIconModule,
MatButtonModule,
MatMenuModule,
MatDividerModule
];
class ContextualActionBarModule {
}
ContextualActionBarModule.decorators = [
{ type: NgModule, args: [{
declarations: [
ContextualActionBarComponent,
ScrollhandlerDirective,
DmlActionsOverflowMenuComponent
],
imports: [
CommonModule,
PortalModule,
...MATERIAL_MODULES
],
exports: [
ContextualActionBarComponent,
],
providers: [
ContextualActionBarService,
]
},] }
];
/*
* Public API Surface of ngx-contextual-action-bar
*/
/**
* Generated bundle index. Do not edit.
*/
export { ActionBarLayerModes, ActionBarLayerRegistration, ActionBarLayerToggleRegistration, ContextualActionBarComponent, ContextualActionBarModule, ContextualActionBarService, navbarAnimation as ɵb, medium_enter as ɵc, medium_leave as ɵd, standard as ɵe, decelerated as ɵf, accelerated as ɵg, buttonAnimation as ɵh, paddingAnimation as ɵi, layerSwitchAnimation as ɵj, ScrollhandlerDirective as ɵk, DmlActionsOverflowMenuComponent as ɵl, ActionAnimation as ɵm };
//# sourceMappingURL=ngx-contextual-action-bar.js.map