UNPKG

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
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