mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 10:35:21 +00:00
FE-848: Float panel (#30)
This commit is contained in:
parent
387da3ad4d
commit
94c4c8240c
6
.vsc-templates/README.md
Normal file
6
.vsc-templates/README.md
Normal 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)
|
67
.vsc-templates/component.vsc-template.ts
Normal file
67
.vsc-templates/component.vsc-template.ts
Normal 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)}'
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
92
.vsc-templates/module-with-component.vsc-template.ts
Normal file
92
.vsc-templates/module-with-component.vsc-template.ts
Normal 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
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
79
package-lock.json
generated
79
package-lock.json
generated
@ -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": {
|
"@types/d3": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.7.2.tgz",
|
||||||
@ -1484,6 +1493,15 @@
|
|||||||
"@types/d3-selection": "*"
|
"@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": {
|
"@types/geojson": {
|
||||||
"version": "7946.0.7",
|
"version": "7946.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
|
||||||
@ -2909,6 +2927,29 @@
|
|||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"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": {
|
"chokidar": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
|
||||||
"integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
|
"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": {
|
"css-parse": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz",
|
"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": {
|
"nopt": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
|
||||||
@ -9398,6 +9450,12 @@
|
|||||||
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
|
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
|
||||||
"dev": true
|
"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": {
|
"promise-retry": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz",
|
||||||
@ -12106,6 +12164,27 @@
|
|||||||
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
|
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
|
||||||
"dev": true
|
"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": {
|
"watchpack": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"@angular/router": "~7.2.14",
|
"@angular/router": "~7.2.14",
|
||||||
"angular2-text-mask": "^9.0.0",
|
"angular2-text-mask": "^9.0.0",
|
||||||
"core-js": "^2.5.4",
|
"core-js": "^2.5.4",
|
||||||
|
"css-element-queries": "^1.2.0",
|
||||||
"d3-axis": "^1.0.12",
|
"d3-axis": "^1.0.12",
|
||||||
"d3-scale": "^2.1.2",
|
"d3-scale": "^2.1.2",
|
||||||
"d3-shape": "^1.2.2",
|
"d3-shape": "^1.2.2",
|
||||||
@ -70,6 +71,7 @@
|
|||||||
"quicktype": "^15.0.187",
|
"quicktype": "^15.0.187",
|
||||||
"ts-node": "~7.0.0",
|
"ts-node": "~7.0.0",
|
||||||
"tslint": "~5.16.0",
|
"tslint": "~5.16.0",
|
||||||
"typescript": "~3.2.4"
|
"typescript": "~3.2.4",
|
||||||
|
"vsc-base": "^0.8.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<button routerLink="/analytics">Графики</button>
|
<button routerLink="/analytics">Графики</button>
|
||||||
<button routerLink="/table">Таблица</button>
|
<button routerLink="/table">Таблица</button>
|
||||||
<button routerLink="/buttons">Кнопки</button>
|
<button routerLink="/buttons">Кнопки</button>
|
||||||
|
<button routerLink="/operations">Операции</button>
|
||||||
<button routerLink="/inputs">Поля ввода</button>
|
<button routerLink="/inputs">Поля ввода</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -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 { TemplatePortal } from '@angular/cdk/portal';
|
||||||
import { OverlayRef, OverlayConfig, Overlay, FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
|
import { OverlayRef, OverlayConfig, Overlay, FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
@ -17,12 +17,15 @@ export class DropdownTriggerDirective implements OnDestroy {
|
|||||||
@Input('dshDropdownTriggerFor')
|
@Input('dshDropdownTriggerFor')
|
||||||
dropdown: DropdownComponent;
|
dropdown: DropdownComponent;
|
||||||
|
|
||||||
|
private removeWindowListenersFns: (() => void)[] = [];
|
||||||
|
|
||||||
private _overlayRef: OverlayRef;
|
private _overlayRef: OverlayRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private viewContainerRef: ViewContainerRef,
|
private viewContainerRef: ViewContainerRef,
|
||||||
private overlay: Overlay,
|
private overlay: Overlay,
|
||||||
private origin: ElementRef<HTMLElement>
|
private origin: ElementRef<HTMLElement>,
|
||||||
|
private renderer: Renderer2
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private get dropdownEl(): HTMLElement {
|
private get dropdownEl(): HTMLElement {
|
||||||
@ -42,6 +45,7 @@ export class DropdownTriggerDirective implements OnDestroy {
|
|||||||
this._overlayRef.dispose();
|
this._overlayRef.dispose();
|
||||||
this._overlayRef = null;
|
this._overlayRef = null;
|
||||||
}
|
}
|
||||||
|
this.removeWindowListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('click')
|
@HostListener('click')
|
||||||
@ -55,15 +59,13 @@ export class DropdownTriggerDirective implements OnDestroy {
|
|||||||
this.overlayRef.attach(portal);
|
this.overlayRef.attach(portal);
|
||||||
this.dropdown.state = State.open;
|
this.dropdown.state = State.open;
|
||||||
this.updatePosition();
|
this.updatePosition();
|
||||||
window.addEventListener('mousedown', this.backdropClickHandler);
|
this.addWindowListeners();
|
||||||
window.addEventListener('keyup', this.keypressHandler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.dropdown.state = State.closed;
|
this.dropdown.state = State.closed;
|
||||||
window.removeEventListener('mousedown', this.backdropClickHandler);
|
this.removeWindowListeners();
|
||||||
window.removeEventListener('keyup', this.keypressHandler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
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 {
|
private getPortal(): TemplatePortal {
|
||||||
return new TemplatePortal(this.dropdown.templateRef, this.viewContainerRef);
|
return new TemplatePortal(this.dropdown.templateRef, this.viewContainerRef);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,25 @@
|
|||||||
@import '~@angular/material/theming';
|
@import '~@angular/material/theming';
|
||||||
|
@import '../../../styles/shadow';
|
||||||
|
|
||||||
@mixin dsh-card-theme($theme) {
|
@mixin dsh-card-theme($theme) {
|
||||||
$background: map-get($theme, background);
|
$background: map-get($theme, background);
|
||||||
|
|
||||||
.card {
|
.dsh-card {
|
||||||
|
@include dsh-shadow($theme);
|
||||||
background-color: mat-color($background, card);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<ng-content></ng-content>
|
|
@ -1,4 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
margin: 30px 20px;
|
|
||||||
}
|
|
@ -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 {}
|
|
@ -1 +0,0 @@
|
|||||||
<ng-content></ng-content>
|
|
@ -1,4 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
margin: 30px 20px;
|
|
||||||
}
|
|
@ -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 {}
|
|
@ -1 +0,0 @@
|
|||||||
<ng-content></ng-content>
|
|
@ -1,5 +0,0 @@
|
|||||||
:host {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
@ -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() {}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
<div class="card"><ng-content></ng-content></div>
|
|
@ -1,7 +1,34 @@
|
|||||||
.card {
|
$dsh-card-padding: 20px !default;
|
||||||
|
$dsh-card-border-radius: 4px !default;
|
||||||
|
|
||||||
|
%dsh-card-section-base {
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 4px;
|
}
|
||||||
box-shadow: 0 8px 20px 0 rgba(174, 188, 230, 0.24);
|
|
||||||
// fix of margin problem in child blocks
|
.dsh-card {
|
||||||
overflow: hidden;
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,54 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, ViewEncapsulation, ChangeDetectionStrategy, HostBinding, Directive } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dsh-card',
|
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;
|
||||||
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { CardComponent } from './card.component';
|
import {
|
||||||
import { CardContentComponent } from './card-content/card-content.component';
|
CardComponent,
|
||||||
import { CardHeaderComponent } from './card-header/card-header.component';
|
CardContentComponent,
|
||||||
import { CardTitleComponent } from './card-header/card-title/card-title.component';
|
CardTitleDirective,
|
||||||
|
CardHeaderComponent,
|
||||||
|
CardActionsComponent
|
||||||
|
} from './card.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [CardComponent, CardContentComponent, CardHeaderComponent, CardTitleComponent],
|
imports: [CommonModule],
|
||||||
exports: [CardComponent, CardContentComponent, CardHeaderComponent, CardTitleComponent],
|
declarations: [CardComponent, CardContentComponent, CardTitleDirective, CardHeaderComponent, CardActionsComponent],
|
||||||
providers: []
|
exports: [CardComponent, CardContentComponent, CardTitleDirective, CardHeaderComponent, CardActionsComponent]
|
||||||
})
|
})
|
||||||
export class CardModule {}
|
export class CardModule {}
|
||||||
|
22
src/app/layout/float-panel/_float-panel-theme.scss
Normal file
22
src/app/layout/float-panel/_float-panel-theme.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/app/layout/float-panel/animations/expand-animation.ts
Normal file
14
src/app/layout/float-panel/animations/expand-animation.ts
Normal 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])
|
||||||
|
]);
|
6
src/app/layout/float-panel/animations/hide-animation.ts
Normal file
6
src/app/layout/float-panel/animations/hide-animation.ts
Normal 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 }))])
|
||||||
|
]);
|
42
src/app/layout/float-panel/float-panel-overlay.service.ts
Normal file
42
src/app/layout/float-panel/float-panel-overlay.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
53
src/app/layout/float-panel/float-panel.component.html
Normal file
53
src/app/layout/float-panel/float-panel.component.html
Normal 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>
|
51
src/app/layout/float-panel/float-panel.component.scss
Normal file
51
src/app/layout/float-panel/float-panel.component.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
src/app/layout/float-panel/float-panel.component.ts
Normal file
148
src/app/layout/float-panel/float-panel.component.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/app/layout/float-panel/float-panel.module.ts
Normal file
18
src/app/layout/float-panel/float-panel.module.ts
Normal 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 {}
|
104
src/app/layout/float-panel/float-panel.spec.ts
Normal file
104
src/app/layout/float-panel/float-panel.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
1
src/app/layout/float-panel/index.ts
Normal file
1
src/app/layout/float-panel/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './float-panel.module';
|
@ -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<{}>;
|
||||||
|
}
|
@ -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<{}>;
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { CardModule } from './card';
|
import { CardModule } from './card';
|
||||||
|
import { FloatPanelModule } from './float-panel';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CardModule],
|
imports: [CardModule, FloatPanelModule],
|
||||||
exports: [CardModule]
|
exports: [CardModule, FloatPanelModule]
|
||||||
})
|
})
|
||||||
export class LayoutModule {}
|
export class LayoutModule {}
|
||||||
|
2
src/app/resized/index.ts
Normal file
2
src/app/resized/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './resized.module';
|
||||||
|
export * from './resized-event';
|
9
src/app/resized/resized-event.ts
Normal file
9
src/app/resized/resized-event.ts
Normal 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;
|
||||||
|
}
|
46
src/app/resized/resized.directive.ts
Normal file
46
src/app/resized/resized.directive.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
11
src/app/resized/resized.module.ts
Normal file
11
src/app/resized/resized.module.ts
Normal 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 {}
|
10
src/app/resized/resized.spec.ts
Normal file
10
src/app/resized/resized.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
62
src/app/sections/operations/operations.component.html
Normal file
62
src/app/sections/operations/operations.component.html
Normal 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>
|
7
src/app/sections/operations/operations.component.ts
Normal file
7
src/app/sections/operations/operations.component.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: 'operations.component.html',
|
||||||
|
styleUrls: []
|
||||||
|
})
|
||||||
|
export class OperationsComponent {}
|
13
src/app/sections/operations/operations.module.ts
Normal file
13
src/app/sections/operations/operations.module.ts
Normal 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 {}
|
@ -7,6 +7,7 @@ import { AnalyticsComponent } from './analytics';
|
|||||||
import { TableComponent } from './table';
|
import { TableComponent } from './table';
|
||||||
import { routes as onboargindRoutes } from './onboarding';
|
import { routes as onboargindRoutes } from './onboarding';
|
||||||
import { ButtonsComponent } from './buttons';
|
import { ButtonsComponent } from './buttons';
|
||||||
|
import { OperationsComponent } from './operations/operations.component';
|
||||||
import { InputsComponent } from './inputs/inputs.component';
|
import { InputsComponent } from './inputs/inputs.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@ -27,6 +28,10 @@ const routes: Routes = [
|
|||||||
path: 'buttons',
|
path: 'buttons',
|
||||||
component: ButtonsComponent
|
component: ButtonsComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'operations',
|
||||||
|
component: OperationsComponent
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'inputs',
|
path: 'inputs',
|
||||||
component: InputsComponent
|
component: InputsComponent
|
||||||
|
@ -7,6 +7,7 @@ import { PageNotFoundModule } from './page-not-found';
|
|||||||
import { TableModule } from './table';
|
import { TableModule } from './table';
|
||||||
import { OnboardingModule } from './onboarding';
|
import { OnboardingModule } from './onboarding';
|
||||||
import { ButtonsModule } from './buttons';
|
import { ButtonsModule } from './buttons';
|
||||||
|
import { OperationsModule } from './operations/operations.module';
|
||||||
import { InputsModule } from './inputs/inputs.module';
|
import { InputsModule } from './inputs/inputs.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -18,6 +19,7 @@ import { InputsModule } from './inputs/inputs.module';
|
|||||||
TableModule,
|
TableModule,
|
||||||
OnboardingModule,
|
OnboardingModule,
|
||||||
ButtonsModule,
|
ButtonsModule,
|
||||||
|
OperationsModule,
|
||||||
InputsModule
|
InputsModule
|
||||||
],
|
],
|
||||||
declarations: [],
|
declarations: [],
|
||||||
|
12
src/styles/_shadow.scss
Normal file
12
src/styles/_shadow.scss
Normal 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)};
|
||||||
|
}
|
@ -2,12 +2,16 @@
|
|||||||
@import '../app/button/button-theme';
|
@import '../app/button/button-theme';
|
||||||
@import '../app/button-toggle/button-toggle-theme';
|
@import '../app/button-toggle/button-toggle-theme';
|
||||||
@import '../app/state-nav/state-nav-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';
|
@import '../app/status/status-theme';
|
||||||
|
|
||||||
@mixin dsh-typography($config) {
|
@mixin dsh-typography($config) {
|
||||||
@include dsh-button-typography($config);
|
@include dsh-button-typography($config);
|
||||||
@include dsh-state-nav-typography($config);
|
@include dsh-state-nav-typography($config);
|
||||||
|
@include dsh-card-typography($config);
|
||||||
@include dsh-button-toggle-typography($config);
|
@include dsh-button-toggle-typography($config);
|
||||||
|
@include dsh-float-panel-typography($config);
|
||||||
@include dsh-status-typography($config);
|
@include dsh-status-typography($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
@import '../app/table/table-theme';
|
@import '../app/table/table-theme';
|
||||||
@import '../app/charts/bar-chart/bar-chart-theme';
|
@import '../app/charts/bar-chart/bar-chart-theme';
|
||||||
@import '../app/layout/card/card-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/charts/linear-chart/linear-chart-theme';
|
||||||
@import '../app/state-nav/state-nav-theme';
|
@import '../app/state-nav/state-nav-theme';
|
||||||
@import '../app/charts/legend-tooltip/legend-tooltip-theme';
|
@import '../app/charts/legend-tooltip/legend-tooltip-theme';
|
||||||
@ -24,6 +25,7 @@
|
|||||||
@include dsh-state-nav-theme($theme);
|
@include dsh-state-nav-theme($theme);
|
||||||
@include dsh-legend-tooltip-theme($theme);
|
@include dsh-legend-tooltip-theme($theme);
|
||||||
@include dsh-button-theme($theme);
|
@include dsh-button-theme($theme);
|
||||||
|
@include dsh-float-panel-theme($theme);
|
||||||
@include dsh-button-toggle-theme($theme);
|
@include dsh-button-toggle-theme($theme);
|
||||||
@include dsh-status-theme($theme);
|
@include dsh-status-theme($theme);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user