FE-848: Float panel (#30)

This commit is contained in:
Rinat Arsaev 2019-06-11 12:41:14 +03:00 committed by GitHub
parent 387da3ad4d
commit 94c4c8240c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1052 additions and 69 deletions

6
.vsc-templates/README.md Normal file
View File

@ -0,0 +1,6 @@
# VSC scaffolding
- [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=alfnielsen.vsc-scaffolding)
- [Documentation](http://vsc-base.org/)
- [Source code](https://github.com/alfnielsen/vsc-base)

View File

@ -0,0 +1,67 @@
import * as vsc from 'vsc-base';
const PREFIX = 'dsh';
function getComponentName(name: string) {
return `${vsc.toPascalCase(name)}Component`;
}
function getComponentPath(name: string) {
return `${vsc.toKebabCase(name)}.component`;
}
export function Template(path: string, templatePath: string): vsc.vscTemplate {
return {
userInputs: [
{
title: 'Component name',
argumentName: 'name',
defaultValue: ''
}
],
template: [
{
type: 'folder',
name: inputs => vsc.toKebabCase(inputs.name),
children: [
{
type: 'file',
name: inputs => `${getComponentPath(inputs.name)}.html`,
content: inputs => ``
},
{
type: 'file',
name: inputs => `${getComponentPath(inputs.name)}.scss`,
content: inputs => `
.${PREFIX}-${vsc.toKebabCase(inputs.name)} {
}
`
},
{
type: 'file',
name: inputs => `${getComponentPath(inputs.name)}.ts`,
content: inputs => `
import { Component } from '@angular/core';
@Component({
selector: '${PREFIX}-${vsc.toKebabCase(inputs.name)}',
templateUrl: '${getComponentPath(inputs.name)}.html',
styleUrls: ['${getComponentPath(inputs.name)}.scss']
})
export class ${getComponentName(inputs.name)} {
constructor() {}
}
`
},
{
type: 'file',
name: inputs => `index.ts`,
content: inputs => `
export * from './${getComponentPath(inputs.name)}'
`
}
]
}
]
};
}

View File

@ -0,0 +1,92 @@
import * as vsc from 'vsc-base';
const PREFIX = 'dsh';
function getModuletName(name: string) {
return `${vsc.toPascalCase(name)}Module`;
}
function getModulePath(name: string) {
return `${vsc.toKebabCase(name)}.module`;
}
function getComponentName(name: string) {
return `${vsc.toPascalCase(name)}Component`;
}
function getComponentPath(name: string) {
return `${vsc.toKebabCase(name)}.component`;
}
export function Template(path: string, templatePath: string): vsc.vscTemplate {
return {
userInputs: [
{
title: 'Component name',
argumentName: 'name',
defaultValue: ''
}
],
template: [
{
type: 'folder',
name: inputs => vsc.toKebabCase(inputs.name),
children: [
{
type: 'file',
name: inputs => `${getModulePath(inputs.name)}.ts`,
content: inputs => `
import { NgModule } from '@angular/core';
import { ${getComponentName(inputs.name)} } from './${getComponentPath(inputs.name)}';
@NgModule({
imports: [],
exports: [${getComponentName(inputs.name)}],
declarations: [${getComponentName(inputs.name)}]
})
export class ${getModuletName(inputs.name)} {}
`
},
{
type: 'file',
name: inputs => `${getComponentPath(inputs.name)}.html`,
content: inputs => ``
},
{
type: 'file',
name: inputs => `${getComponentPath(inputs.name)}.scss`,
content: inputs => `
.${PREFIX}-${vsc.toKebabCase(inputs.name)} {
}
`
},
{
type: 'file',
name: inputs => `${getComponentPath(inputs.name)}.ts`,
content: inputs => `
import { Component } from '@angular/core';
@Component({
selector: '${PREFIX}-${vsc.toKebabCase(inputs.name)}',
templateUrl: '${getComponentPath(inputs.name)}.html',
styleUrls: ['${getComponentPath(inputs.name)}.scss']
})
export class ${getComponentName(inputs.name)} {
constructor() {}
}
`
},
{
type: 'file',
name: inputs => `index.ts`,
content: inputs => `
export * from './${getModulePath(inputs.name)}';
export * from './${getComponentPath(inputs.name)}'
`
}
]
}
]
};
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

79
package-lock.json generated
View File

@ -1224,6 +1224,15 @@
}
}
},
"@types/child-process-promise": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/child-process-promise/-/child-process-promise-2.2.1.tgz",
"integrity": "sha512-xZ4kkF82YkmqPCERqV9Tj0bVQj3Tk36BqGlNgxv5XhifgDRhwAqp+of+sccksdpZRbbPsNwMOkmUqOnLgxKtGw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/d3": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.7.2.tgz",
@ -1484,6 +1493,15 @@
"@types/d3-selection": "*"
}
},
"@types/fs-extra": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.1.0.tgz",
"integrity": "sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/geojson": {
"version": "7946.0.7",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
@ -2909,6 +2927,29 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"child-process-promise": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz",
"integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=",
"dev": true,
"requires": {
"cross-spawn": "^4.0.2",
"node-version": "^1.0.0",
"promise-polyfill": "^6.0.1"
},
"dependencies": {
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
"integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=",
"dev": true,
"requires": {
"lru-cache": "^4.0.1",
"which": "^1.2.9"
}
}
}
},
"chokidar": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
@ -3459,6 +3500,11 @@
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
"integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
},
"css-element-queries": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.2.0.tgz",
"integrity": "sha512-4gaxpioSFueMcp9yj1TJFCLjfooGv38y6ZdwFUS3GuS+9NIVijdeiExXKwSIHoQDADfpgnaYSTzmJs+bV+Hehg=="
},
"css-parse": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz",
@ -8523,6 +8569,12 @@
}
}
},
"node-version": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz",
"integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==",
"dev": true
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@ -9398,6 +9450,12 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
"promise-polyfill": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz",
"integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=",
"dev": true
},
"promise-retry": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz",
@ -12106,6 +12164,27 @@
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
"dev": true
},
"vsc-base": {
"version": "0.8.24",
"resolved": "https://registry.npmjs.org/vsc-base/-/vsc-base-0.8.24.tgz",
"integrity": "sha512-I71mp/uQBmZ/HCJSqPgeymkNTGLwSonrkVA4Z33F8aedqAnckM4445mxwTj0PZp+l8EzznG3em6z/OUUp3zOOA==",
"dev": true,
"requires": {
"@types/child-process-promise": "^2.2.1",
"@types/fs-extra": "^5.1.0",
"child-process-promise": "^2.2.1",
"fs-extra": "^7.0.1",
"typescript": "^3.4.5"
},
"dependencies": {
"typescript": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.1.tgz",
"integrity": "sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==",
"dev": true
}
}
},
"watchpack": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",

View File

@ -30,6 +30,7 @@
"@angular/router": "~7.2.14",
"angular2-text-mask": "^9.0.0",
"core-js": "^2.5.4",
"css-element-queries": "^1.2.0",
"d3-axis": "^1.0.12",
"d3-scale": "^2.1.2",
"d3-shape": "^1.2.2",
@ -70,6 +71,7 @@
"quicktype": "^15.0.187",
"ts-node": "~7.0.0",
"tslint": "~5.16.0",
"typescript": "~3.2.4"
"typescript": "~3.2.4",
"vsc-base": "^0.8.24"
}
}

View File

@ -5,6 +5,7 @@
<button routerLink="/analytics">Графики</button>
<button routerLink="/table">Таблица</button>
<button routerLink="/buttons">Кнопки</button>
<button routerLink="/operations">Операции</button>
<button routerLink="/inputs">Поля ввода</button>
</div>
<div class="content">

View File

@ -1,4 +1,4 @@
import { Directive, Input, HostListener, ViewContainerRef, ElementRef, OnDestroy } from '@angular/core';
import { Directive, Input, HostListener, ViewContainerRef, ElementRef, OnDestroy, Renderer2 } from '@angular/core';
import { TemplatePortal } from '@angular/cdk/portal';
import { OverlayRef, OverlayConfig, Overlay, FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
import get from 'lodash.get';
@ -17,12 +17,15 @@ export class DropdownTriggerDirective implements OnDestroy {
@Input('dshDropdownTriggerFor')
dropdown: DropdownComponent;
private removeWindowListenersFns: (() => void)[] = [];
private _overlayRef: OverlayRef;
constructor(
private viewContainerRef: ViewContainerRef,
private overlay: Overlay,
private origin: ElementRef<HTMLElement>
private origin: ElementRef<HTMLElement>,
private renderer: Renderer2
) {}
private get dropdownEl(): HTMLElement {
@ -42,6 +45,7 @@ export class DropdownTriggerDirective implements OnDestroy {
this._overlayRef.dispose();
this._overlayRef = null;
}
this.removeWindowListeners();
}
@HostListener('click')
@ -55,15 +59,13 @@ export class DropdownTriggerDirective implements OnDestroy {
this.overlayRef.attach(portal);
this.dropdown.state = State.open;
this.updatePosition();
window.addEventListener('mousedown', this.backdropClickHandler);
window.addEventListener('keyup', this.keypressHandler);
this.addWindowListeners();
}
}
close() {
this.dropdown.state = State.closed;
window.removeEventListener('mousedown', this.backdropClickHandler);
window.removeEventListener('keyup', this.keypressHandler);
this.removeWindowListeners();
}
toggle() {
@ -76,6 +78,20 @@ export class DropdownTriggerDirective implements OnDestroy {
}
}
private addWindowListeners() {
this.removeWindowListenersFns.push(
this.renderer.listen(window, 'mousedown', this.backdropClickHandler),
this.renderer.listen(window, 'keyup', this.keypressHandler)
);
}
private removeWindowListeners() {
let unlisten: () => void;
while ((unlisten = this.removeWindowListenersFns.pop())) {
unlisten();
}
}
private getPortal(): TemplatePortal {
return new TemplatePortal(this.dropdown.templateRef, this.viewContainerRef);
}

View File

@ -1,9 +1,25 @@
@import '~@angular/material/theming';
@import '../../../styles/shadow';
@mixin dsh-card-theme($theme) {
$background: map-get($theme, background);
.card {
.dsh-card {
@include dsh-shadow($theme);
background-color: mat-color($background, card);
}
}
@mixin dsh-card-typography($config) {
.dsh-card {
font-family: mat-font-family($config);
}
.dsh-card-header .dsh-card-title {
font-size: mat-font-size($config, title);
}
.dsh-card-content {
font-size: mat-font-size($config, body-1);
}
}

View File

@ -1 +0,0 @@
<ng-content></ng-content>

View File

@ -1,4 +0,0 @@
:host {
display: block;
margin: 30px 20px;
}

View File

@ -1,8 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'dsh-card-content',
templateUrl: './card-content.component.html',
styleUrls: ['./card-content.component.scss']
})
export class CardContentComponent {}

View File

@ -1 +0,0 @@
<ng-content></ng-content>

View File

@ -1,4 +0,0 @@
:host {
display: block;
margin: 30px 20px;
}

View File

@ -1,8 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'dsh-card-header',
templateUrl: './card-header.component.html',
styleUrls: ['./card-header.component.scss']
})
export class CardHeaderComponent {}

View File

@ -1 +0,0 @@
<ng-content></ng-content>

View File

@ -1,5 +0,0 @@
:host {
font-size: 18px;
font-weight: 400;
line-height: 1.5;
}

View File

@ -1,10 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'dsh-card-title',
templateUrl: './card-title.component.html',
styleUrls: ['./card-title.component.scss']
})
export class CardTitleComponent {
constructor() {}
}

View File

@ -1 +0,0 @@
<div class="card"><ng-content></ng-content></div>

View File

@ -1,7 +1,34 @@
.card {
$dsh-card-padding: 20px !default;
$dsh-card-border-radius: 4px !default;
%dsh-card-section-base {
display: block;
border-radius: 4px;
box-shadow: 0 8px 20px 0 rgba(174, 188, 230, 0.24);
// fix of margin problem in child blocks
overflow: hidden;
}
.dsh-card {
@extend %dsh-card-section-base;
position: relative;
padding: $dsh-card-padding;
border-radius: $dsh-card-border-radius;
}
.dsh-card-content {
@extend %dsh-card-section-base;
}
.dsh-card-actions {
@extend %dsh-card-section-base;
margin-left: -$dsh-card-padding / 2;
margin-right: -$dsh-card-padding / 2;
margin-bottom: -$dsh-card-padding / 2;
padding-top: $dsh-card-padding;
}
.dsh-card-header {
display: flex;
flex-direction: row;
.dsh-card-title {
margin-bottom: $dsh-card-padding;
}
}

View File

@ -1,8 +1,54 @@
import { Component } from '@angular/core';
import { Component, ViewEncapsulation, ChangeDetectionStrategy, HostBinding, Directive } from '@angular/core';
@Component({
selector: 'dsh-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.scss']
styleUrls: ['card.component.scss'],
template: `
<ng-content></ng-content>
`,
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CardComponent {}
export class CardComponent {
@HostBinding('class.dsh-card') class = true;
}
@Component({
selector: 'dsh-card-header',
template: `
<ng-content select="dsh-card-title, [dsh-card-title], [dshCardTitle]"></ng-content>
`,
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CardHeaderComponent {
@HostBinding('class.dsh-card-header') class = true;
}
@Component({
selector: 'dsh-card-content',
template: `
<ng-content></ng-content>
`
})
export class CardContentComponent {
@HostBinding('class.dsh-card-content') class = true;
}
@Component({
selector: 'dsh-card-actions',
exportAs: 'dshCardActions',
template: `
<ng-content></ng-content>
`
})
export class CardActionsComponent {
@HostBinding('class.dsh-card-actions') class = true;
}
@Directive({
selector: `dsh-card-title, [dsh-card-title], [dshCardTitle]`
})
export class CardTitleDirective {
@HostBinding('class.dsh-card-title') class = true;
}

View File

@ -1,13 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CardComponent } from './card.component';
import { CardContentComponent } from './card-content/card-content.component';
import { CardHeaderComponent } from './card-header/card-header.component';
import { CardTitleComponent } from './card-header/card-title/card-title.component';
import {
CardComponent,
CardContentComponent,
CardTitleDirective,
CardHeaderComponent,
CardActionsComponent
} from './card.component';
@NgModule({
declarations: [CardComponent, CardContentComponent, CardHeaderComponent, CardTitleComponent],
exports: [CardComponent, CardContentComponent, CardHeaderComponent, CardTitleComponent],
providers: []
imports: [CommonModule],
declarations: [CardComponent, CardContentComponent, CardTitleDirective, CardHeaderComponent, CardActionsComponent],
exports: [CardComponent, CardContentComponent, CardTitleDirective, CardHeaderComponent, CardActionsComponent]
})
export class CardModule {}

View File

@ -0,0 +1,22 @@
@import '~@angular/material/theming';
@import '../../../styles/shadow';
@mixin dsh-float-panel-theme($theme) {
$background: map-get($theme, background);
.dsh-float-panel {
&-card {
@include dsh-shadow($theme);
background-color: mat-color($background, card);
}
}
}
@mixin dsh-float-panel-typography($config) {
.dsh-float-panel {
&-template-wrapper {
font-family: mat-font-family($config);
font-size: mat-font-size($config, body-1);
}
}
}

View File

@ -0,0 +1,14 @@
import { trigger, state, style, transition, animate } from '@angular/animations';
export enum ExpandState {
expanded = 'expanded',
collapsed = 'collapsed'
}
const animation = animate('150ms ease');
export const expandAnimation = trigger('expand', [
state(ExpandState.expanded, style({ height: '{{height}}px' }), { params: { height: 0 } }),
transition(`${ExpandState.collapsed} <=> ${ExpandState.expanded}`, [animation]),
transition(`${ExpandState.expanded} => void`, [animation])
]);

View File

@ -0,0 +1,6 @@
import { trigger, style, transition, animate } from '@angular/animations';
export const hideAnimation = trigger('hide', [
transition(':enter', [style({ opacity: 0 }), animate('.25s ease', style({ opacity: 1 }))]),
transition(':leave', [style({ opacity: 1 }), animate('.25s ease', style({ opacity: 0 }))])
]);

View File

@ -0,0 +1,42 @@
import { Injectable, ElementRef } from '@angular/core';
import { OverlayRef, Overlay, OverlayConfig } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
@Injectable()
export class FloatPanelOverlayService {
private overlayRef: OverlayRef;
constructor(private overlay: Overlay) {}
attach(templatePortal: TemplatePortal, elementRef: ElementRef, config: { width: number }) {
this.detach();
this.overlayRef = this.overlay.create(this.createConfig(elementRef, config));
this.overlayRef.attach(templatePortal);
}
detach() {
if (this.overlayRef && this.overlayRef.hasAttached()) {
this.overlayRef.detach();
}
}
updateSize({ width }: { width: number }) {
if (this.overlayRef) {
this.overlayRef.updateSize({ width: `${width}px` });
}
}
private createConfig(elementRef: ElementRef, { width }: { width: number }): OverlayConfig {
const positionStrategy = this.overlay
.position()
.connectedTo(elementRef, { originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'top' });
const overlayConfig = new OverlayConfig({
width: `${width}px`,
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition()
});
return overlayConfig;
}
}

View File

@ -0,0 +1,53 @@
<div class="dsh-float-panel">
<div
#substrate
(dshResized)="onSubstrateResize($event)"
[ngStyle]="{ height: pinned ? 0 : cardHeight.base + 'px' }"
></div>
<div [ngStyle]="{ height: cardHeight.current + 'px' }" class="dsh-float-panel-body-extender"></div>
<ng-container #container [ngTemplateOutlet]="template" *ngIf="pinned"></ng-container>
</div>
<ng-template #template>
<div class="dsh-float-panel-template-wrapper">
<div
class="dsh-float-panel-card"
(dshResized)="setCardHeight($event.height)"
[ngClass]="{ 'dsh-float-panel-card-expanded': expanded }"
>
<div class="dsh-float-panel-base" fxLayout="row" fxLayoutGap="20px" fxLayoutAlign=" center">
<div fxFlex><ng-content></ng-content></div>
<div @hide *ngIf="!expanded" class="dsh-float-panel-base-action">
<button dsh-icon-button (click)="expandToggle()">
<mat-icon svgIcon="user"></mat-icon>
</button>
</div>
</div>
<div
class="dsh-float-panel-more"
[@expand]="expandTrigger"
(@expand.start)="expandStart($event)"
(@expand.done)="expandDone($event)"
*ngIf="expanded"
>
<div (dshResized)="setMoreContentHeight($event.height)">
<div fxLayout="column" fxLayoutGap="20px">
<div class="dsh-float-panel-more-content">
<ng-container *ngTemplateOutlet="floatPanelMore?.templateRef"></ng-container>
</div>
<div fxLayout="row" fxLayoutGap="20px" fxLayoutAlign="space-between">
<button dsh-icon-button (click)="pinToggle()">
<mat-icon svgIcon="notification"></mat-icon>
</button>
<div>
<ng-container *ngTemplateOutlet="floatPanelActions?.templateRef"></ng-container>
</div>
<button dsh-icon-button (click)="expandToggle()">
<mat-icon svgIcon="user"></mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,51 @@
$base-height: 51.313px;
$card-padding: 20px !default;
$actions-padding: 10px !default;
$card-border-radius: 4px !default;
$card-without-actions-padding: $card-padding - $actions-padding;
.dsh-float-panel {
position: relative;
&-body-extender {
position: absolute;
top: 0;
left: 0;
width: 100%;
margin-bottom: $card-padding;
}
&-template-wrapper {
display: flex;
width: 100%;
}
&-card {
width: 100%;
border-radius: $card-border-radius;
padding: $card-padding $actions-padding $card-padding $actions-padding;
transition: padding 150ms ease;
&-expanded {
padding-bottom: $actions-padding;
}
}
&-base {
min-height: $base-height;
margin: 0 $card-without-actions-padding;
&-action {
margin-right: -$card-without-actions-padding;
}
}
&-more {
height: 0;
overflow: hidden;
&-content {
padding: 0 $card-without-actions-padding;
}
}
}

View File

@ -0,0 +1,148 @@
import {
Component,
ContentChild,
ViewChild,
ElementRef,
Input,
TemplateRef,
AfterViewInit,
Output,
EventEmitter,
ViewContainerRef,
ChangeDetectorRef,
OnDestroy
} from '@angular/core';
import { TemplatePortal } from '@angular/cdk/portal';
import { AnimationEvent } from '@angular/animations';
import { FloatPanelMoreTemplateComponent } from './templates/float-panel-more-template.component';
import { FloatPanelActionsTemplateComponent } from './templates/float-panel-actions-template.component';
import { expandAnimation, ExpandState } from './animations/expand-animation';
import { hideAnimation } from './animations/hide-animation';
import { FloatPanelOverlayService } from './float-panel-overlay.service';
import { ResizedEvent } from '../../resized';
@Component({
selector: 'dsh-float-panel',
templateUrl: 'float-panel.component.html',
styleUrls: ['float-panel.component.scss'],
animations: [expandAnimation, hideAnimation],
providers: [FloatPanelOverlayService]
})
export class FloatPanelComponent implements AfterViewInit, OnDestroy {
private _expanded = false;
@Input()
get expanded() {
return this._expanded;
}
set expanded(expanded) {
this.expandedChange.emit((this._expanded = expanded !== false));
}
@Output() expandedChange = new EventEmitter<boolean>();
private _pinned = false;
@Input()
get pinned() {
return this._pinned;
}
set pinned(pinned) {
this.pinnedChange.emit((this._pinned = pinned !== false));
}
@Output() pinnedChange = new EventEmitter<boolean>();
@ContentChild(FloatPanelMoreTemplateComponent) floatPanelMore: FloatPanelMoreTemplateComponent;
@ContentChild(FloatPanelActionsTemplateComponent) floatPanelActions: FloatPanelActionsTemplateComponent;
@ViewChild('template') templateRef: TemplateRef<{}>;
@ViewChild('substrate') private substrate: ElementRef<HTMLDivElement>;
expandTrigger: { value: ExpandState; params: { height: number } } | ExpandState = ExpandState.collapsed;
cardHeight = {
base: 0,
current: 0
};
private isExpanding = false;
constructor(
private viewContainerRef: ViewContainerRef,
private floatPanelOverlayService: FloatPanelOverlayService,
private ref: ChangeDetectorRef
) {
this.expandedChange.subscribe(() => this.resetExpandTriggerManage());
this.pinnedChange.subscribe(() => this.overlayAttachManage());
}
ngAfterViewInit() {
this.overlayAttachManage();
}
ngOnDestroy() {
this.detach();
}
onSubstrateResize({ width }: ResizedEvent) {
this.floatPanelOverlayService.updateSize({ width });
this.ref.detectChanges();
}
expandStart(e: AnimationEvent) {
this.isExpanding = true;
}
expandDone(e: AnimationEvent) {
this.isExpanding = false;
}
expandToggle() {
this.expanded = !this.expanded;
}
pinToggle() {
this.pinned = !this.pinned;
}
setMoreContentHeight(height: number) {
if (height !== 0) {
this.expandTrigger = { value: ExpandState.expanded, params: { height } };
this.ref.detectChanges();
}
}
setCardHeight(height: number) {
if (!this.expanded && !this.isExpanding && height !== 0) {
this.cardHeight.base = height;
}
this.cardHeight.current = height;
this.ref.detectChanges();
}
private attach() {
this.floatPanelOverlayService.attach(
new TemplatePortal(this.templateRef, this.viewContainerRef),
this.substrate,
{ width: this.substrate.nativeElement.clientWidth }
);
}
private detach() {
this.floatPanelOverlayService.detach();
}
private resetExpandTriggerManage() {
if (!this.expanded) {
this.expandTrigger = ExpandState.collapsed;
}
}
private overlayAttachManage() {
if (this.pinned) {
this.detach();
} else {
this.attach();
}
}
}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { MatIconModule } from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { CommonModule } from '@angular/common';
import { OverlayModule } from '@angular/cdk/overlay';
import { FloatPanelComponent } from './float-panel.component';
import { FloatPanelMoreTemplateComponent } from './templates/float-panel-more-template.component';
import { FloatPanelActionsTemplateComponent } from './templates/float-panel-actions-template.component';
import { ButtonModule } from '../../button';
import { ResizedModule } from '../../resized';
@NgModule({
imports: [MatIconModule, FlexLayoutModule, CommonModule, ButtonModule, OverlayModule, ResizedModule],
declarations: [FloatPanelComponent, FloatPanelMoreTemplateComponent, FloatPanelActionsTemplateComponent],
exports: [FloatPanelComponent, FloatPanelMoreTemplateComponent, FloatPanelActionsTemplateComponent]
})
export class FloatPanelModule {}

View File

@ -0,0 +1,104 @@
import { Component, Type, Provider, ViewChild } from '@angular/core';
import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { OverlayContainer, FullscreenOverlayContainer } from '@angular/cdk/overlay';
import { By } from '@angular/platform-browser';
import { FloatPanelModule } from './float-panel.module';
import { FloatPanelComponent } from './float-panel.component';
@Component({
template: `
<dsh-float-panel [pinned]="pinned" [expanded]="expanded" #floatPanel>
Базовый фильтр
<dsh-float-panel-actions>
<button>Сбросить</button>
</dsh-float-panel-actions>
<dsh-float-panel-more>
Фильтр
</dsh-float-panel-more>
</dsh-float-panel>
`
})
class SimpleFloatPanelComponent {
@ViewChild('floatPanel') floatPanel: FloatPanelComponent;
pinned = true;
expanded = true;
}
const WRAPPER = By.css('.dsh-float-panel-more');
describe('FloatPanelComponent', () => {
let overlayContainer: OverlayContainer;
let overlayContainerElement: HTMLElement;
function createComponent<T>(
component: Type<T>,
providers: Provider[] = [],
declarations: any[] = []
): ComponentFixture<T> {
TestBed.configureTestingModule({
imports: [FloatPanelModule, NoopAnimationsModule],
declarations: [component, ...declarations],
providers
}).compileComponents();
inject([OverlayContainer], (oc: FullscreenOverlayContainer) => {
overlayContainer = oc;
overlayContainerElement = oc.getContainerElement();
})();
return TestBed.createComponent<T>(component);
}
afterEach(inject([OverlayContainer], (currentOverlayContainer: OverlayContainer) => {
currentOverlayContainer.ngOnDestroy();
overlayContainer.ngOnDestroy();
}));
describe('Pin/unpin', () => {
it('should pinned', () => {
createComponent(SimpleFloatPanelComponent);
expect(overlayContainerElement.textContent).toBe('');
});
it('should float', () => {
const fixture = createComponent(SimpleFloatPanelComponent);
fixture.componentInstance.pinned = false;
fixture.detectChanges();
expect(overlayContainerElement.textContent).not.toBe('');
});
});
describe('Pinned expand/collapse', () => {
it('should expanded', () => {
const fixture = createComponent(SimpleFloatPanelComponent);
fixture.detectChanges();
expect(fixture.debugElement.query(WRAPPER)).toBeDefined();
});
it('should collapsed', () => {
const fixture = createComponent(SimpleFloatPanelComponent);
fixture.componentInstance.expanded = false;
fixture.detectChanges();
expect(fixture.debugElement.query(WRAPPER)).toBeNull();
});
});
describe('Float expand/collapse', () => {
it('should expanded', () => {
const fixture = createComponent(SimpleFloatPanelComponent);
fixture.componentInstance.pinned = false;
fixture.detectChanges();
expect(fixture.debugElement.query(WRAPPER)).toBeDefined();
});
it('should collapsed', () => {
const fixture = createComponent(SimpleFloatPanelComponent);
fixture.componentInstance.pinned = false;
fixture.componentInstance.expanded = false;
fixture.detectChanges();
expect(fixture.debugElement.query(WRAPPER)).toBeNull();
});
});
});

View File

@ -0,0 +1 @@
export * from './float-panel.module';

View File

@ -0,0 +1,11 @@
import { Component, TemplateRef, ViewChild } from '@angular/core';
@Component({
selector: 'dsh-float-panel-actions',
template: `
<ng-template><ng-content></ng-content></ng-template>
`
})
export class FloatPanelActionsTemplateComponent {
@ViewChild(TemplateRef) templateRef: TemplateRef<{}>;
}

View File

@ -0,0 +1,11 @@
import { Component, TemplateRef, ViewChild } from '@angular/core';
@Component({
selector: 'dsh-float-panel-more',
template: `
<ng-template><ng-content></ng-content></ng-template>
`
})
export class FloatPanelMoreTemplateComponent {
@ViewChild(TemplateRef) templateRef: TemplateRef<{}>;
}

View File

@ -1,9 +1,10 @@
import { NgModule } from '@angular/core';
import { CardModule } from './card';
import { FloatPanelModule } from './float-panel';
@NgModule({
imports: [CardModule],
exports: [CardModule]
imports: [CardModule, FloatPanelModule],
exports: [CardModule, FloatPanelModule]
})
export class LayoutModule {}

2
src/app/resized/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './resized.module';
export * from './resized-event';

View File

@ -0,0 +1,9 @@
import { ElementRef } from '@angular/core';
export interface ResizedEvent {
readonly element: ElementRef;
readonly width: number;
readonly height: number;
readonly oldWidth?: number;
readonly oldHeight?: number;
}

View File

@ -0,0 +1,46 @@
import { Directive, ElementRef, EventEmitter, OnInit, Output, OnDestroy } from '@angular/core';
import { ResizeSensor } from 'css-element-queries';
import { ResizedEvent } from './resized-event';
@Directive({
selector: '[dshResized]'
})
export class ResizedDirective implements OnInit, OnDestroy {
@Output() readonly dshResized = new EventEmitter<ResizedEvent>();
private currentEvent: ResizedEvent;
private resizeSensor: ResizeSensor;
constructor(private readonly element: ElementRef) {
this.currentEvent = {
element: this.element,
width: 0,
height: 0
};
}
ngOnInit() {
this.resizeSensor = new ResizeSensor(this.element.nativeElement, () => this.resize());
}
ngOnDestroy() {
this.resizeSensor.detach();
}
private resize() {
const width = this.element.nativeElement.clientWidth;
const height = this.element.nativeElement.clientHeight;
if (width === this.currentEvent.width && height === this.currentEvent.height) {
return;
}
const event = {
element: this.element,
width,
height,
oldWidth: this.currentEvent.width,
oldHeight: this.currentEvent.height
};
this.currentEvent = event;
this.dshResized.emit(event);
}
}

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ResizedDirective } from './resized.directive';
@NgModule({
imports: [CommonModule],
declarations: [ResizedDirective],
exports: [ResizedDirective]
})
export class ResizedModule {}

View File

@ -0,0 +1,10 @@
import { ElementRef } from '@angular/core';
import { ResizedDirective } from './resized.directive';
describe('ResizedDirective', () => {
it('should create an instance', () => {
const directive = new ResizedDirective({} as ElementRef);
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,62 @@
<h1 class="mat-headline">Заявка на подключение</h1>
<div fxLayout="column" fxLayoutGap="20px">
<dsh-float-panel>
<mat-form-field style="margin: -3.5px 0 -22.313px 0">
<mat-label>Form field</mat-label>
<input matInput placeholder="Favorite food" />
</mat-form-field>
<dsh-float-panel-actions>
<button dsh-button>
Сбросить параметры поиска
</button>
</dsh-float-panel-actions>
<dsh-float-panel-more>
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
Остальной фильтр<br />
</dsh-float-panel-more>
</dsh-float-panel>
<dsh-card>
<dsh-card-content>
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
Последнее обновление: Только что<br />
</dsh-card-content>
</dsh-card>
</div>

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
templateUrl: 'operations.component.html',
styleUrls: []
})
export class OperationsComponent {}

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatFormFieldModule, MatInputModule } from '@angular/material';
import { OperationsComponent } from './operations.component';
import { LayoutModule } from '../../layout';
import { ButtonModule } from '../../button';
@NgModule({
imports: [LayoutModule, FlexLayoutModule, ButtonModule, MatFormFieldModule, MatInputModule],
declarations: [OperationsComponent]
})
export class OperationsModule {}

View File

@ -7,6 +7,7 @@ import { AnalyticsComponent } from './analytics';
import { TableComponent } from './table';
import { routes as onboargindRoutes } from './onboarding';
import { ButtonsComponent } from './buttons';
import { OperationsComponent } from './operations/operations.component';
import { InputsComponent } from './inputs/inputs.component';
const routes: Routes = [
@ -27,6 +28,10 @@ const routes: Routes = [
path: 'buttons',
component: ButtonsComponent
},
{
path: 'operations',
component: OperationsComponent
},
{
path: 'inputs',
component: InputsComponent

View File

@ -7,6 +7,7 @@ import { PageNotFoundModule } from './page-not-found';
import { TableModule } from './table';
import { OnboardingModule } from './onboarding';
import { ButtonsModule } from './buttons';
import { OperationsModule } from './operations/operations.module';
import { InputsModule } from './inputs/inputs.module';
@NgModule({
@ -18,6 +19,7 @@ import { InputsModule } from './inputs/inputs.module';
TableModule,
OnboardingModule,
ButtonsModule,
OperationsModule,
InputsModule
],
declarations: [],

12
src/styles/_shadow.scss Normal file
View File

@ -0,0 +1,12 @@
@function get-shadow($color, $opacity) {
$color: rgba($color, $opacity);
@return '0px 8px 20px 0px #{$color}';
}
@mixin dsh-shadow($theme) {
$primary: map-get($theme, primary);
$shadow-color: mat-color($primary, 100);
box-shadow: #{get-shadow($shadow-color, 0.24)};
}

View File

@ -2,12 +2,16 @@
@import '../app/button/button-theme';
@import '../app/button-toggle/button-toggle-theme';
@import '../app/state-nav/state-nav-theme';
@import '../app/layout/card/card-theme';
@import '../app/layout/float-panel/float-panel-theme';
@import '../app/status/status-theme';
@mixin dsh-typography($config) {
@include dsh-button-typography($config);
@include dsh-state-nav-typography($config);
@include dsh-card-typography($config);
@include dsh-button-toggle-typography($config);
@include dsh-float-panel-typography($config);
@include dsh-status-typography($config);
}

View File

@ -5,6 +5,7 @@
@import '../app/table/table-theme';
@import '../app/charts/bar-chart/bar-chart-theme';
@import '../app/layout/card/card-theme';
@import '../app/layout/float-panel/float-panel-theme';
@import '../app/charts/linear-chart/linear-chart-theme';
@import '../app/state-nav/state-nav-theme';
@import '../app/charts/legend-tooltip/legend-tooltip-theme';
@ -24,6 +25,7 @@
@include dsh-state-nav-theme($theme);
@include dsh-legend-tooltip-theme($theme);
@include dsh-button-theme($theme);
@include dsh-float-panel-theme($theme);
@include dsh-button-toggle-theme($theme);
@include dsh-status-theme($theme);
}