Added windows
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||||
"@angular/router": "^17.3.0",
|
"@angular/router": "^17.3.0",
|
||||||
"@fortawesome/angular-fontawesome": "^0.14.1",
|
"@fortawesome/angular-fontawesome": "^0.14.1",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.3",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
@@ -2720,6 +2721,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
|
||||||
|
"license": "(CC-BY-4.0 AND MIT)",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "6.6.0",
|
"version": "6.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||||
"@angular/router": "^17.3.0",
|
"@angular/router": "^17.3.0",
|
||||||
"@fortawesome/angular-fontawesome": "^0.14.1",
|
"@fortawesome/angular-fontawesome": "^0.14.1",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.3",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<div class="header">Fields!</div>
|
<div class="header">Fields</div>
|
||||||
<fbi-tree
|
<fbi-tree
|
||||||
class="tree"
|
class="tree"
|
||||||
[node]="node"
|
[node]="node"
|
||||||
(actionClick)="onActionClick($event)"
|
(actionClick)="onActionClick($event)"
|
||||||
></fbi-tree>
|
></fbi-tree>
|
||||||
|
|
||||||
|
<fbi-window #filter [config]="windowConfig"
|
||||||
|
>content content content content content content content content content
|
||||||
|
content content content content content content content content content
|
||||||
|
</fbi-window>
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import { TreeComponent } from '../tree/tree.component';
|
import { TreeComponent } from '../tree/tree.component';
|
||||||
import { TreeNode } from '../../models/tree-node';
|
import { TreeNode } from '../../models/tree-node';
|
||||||
import { MetaService } from '../../services/meta.service';
|
import { MetaService } from '../../services/meta.service';
|
||||||
import { Action } from '../../models/action';
|
import { Action } from '../../models/action';
|
||||||
import { faAdd } from '@fortawesome/free-solid-svg-icons';
|
import { faAdd, faFilter } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { QueryService } from '../../services/query.service';
|
import { QueryService } from '../../services/query.service';
|
||||||
import { combineLatest } from 'rxjs';
|
import { combineLatest } from 'rxjs';
|
||||||
import { Query } from '../../models/query';
|
import { Query } from '../../models/query';
|
||||||
|
import { WindowComponent } from '../window/window.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fbi-metadata',
|
selector: 'fbi-metadata',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TreeComponent],
|
imports: [TreeComponent, WindowComponent],
|
||||||
templateUrl: './metadata.component.html',
|
templateUrl: './metadata.component.html',
|
||||||
styleUrl: './metadata.component.scss',
|
styleUrl: './metadata.component.scss',
|
||||||
})
|
})
|
||||||
export class MetadataComponent {
|
export class MetadataComponent {
|
||||||
node: TreeNode = new TreeNode({});
|
node: TreeNode = new TreeNode({});
|
||||||
|
@ViewChild(WindowComponent) window!: WindowComponent;
|
||||||
|
|
||||||
|
windowComponent = WindowComponent;
|
||||||
|
|
||||||
|
windowConfig = {
|
||||||
|
title: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
constructor(metaService: MetaService, private queryService: QueryService) {
|
constructor(metaService: MetaService, private queryService: QueryService) {
|
||||||
combineLatest({
|
combineLatest({
|
||||||
@@ -33,7 +41,10 @@ export class MetadataComponent {
|
|||||||
const children = node.children ?? [];
|
const children = node.children ?? [];
|
||||||
children.forEach((child: Partial<TreeNode>) => recurse(child));
|
children.forEach((child: Partial<TreeNode>) => recurse(child));
|
||||||
if (children.length === 0) {
|
if (children.length === 0) {
|
||||||
const actions = [{ label: 'Add', icon: faAdd, data: ACTIONS.ADD }];
|
const actions = [
|
||||||
|
{ label: 'Filter', icon: faFilter, data: ACTIONS.FILTER },
|
||||||
|
{ label: 'Add', icon: faAdd, data: ACTIONS.ADD },
|
||||||
|
];
|
||||||
node.actions = actions as Action[];
|
node.actions = actions as Action[];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -53,6 +64,9 @@ export class MetadataComponent {
|
|||||||
case ACTIONS.ADD:
|
case ACTIONS.ADD:
|
||||||
this.queryService.add(event.node.data);
|
this.queryService.add(event.node.data);
|
||||||
break;
|
break;
|
||||||
|
case ACTIONS.FILTER:
|
||||||
|
this.window.show();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,4 +86,5 @@ export class MetadataComponent {
|
|||||||
|
|
||||||
enum ACTIONS {
|
enum ACTIONS {
|
||||||
ADD = 'add',
|
ADD = 'add',
|
||||||
|
FILTER = 'filter',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="header">Query!</div>
|
<div class="header">Query</div>
|
||||||
<fbi-tree
|
<fbi-tree
|
||||||
class="tree"
|
class="tree"
|
||||||
[node]="node"
|
[node]="node"
|
||||||
|
|||||||
@@ -50,12 +50,14 @@ export class QueryComponent {
|
|||||||
label: 'Filters',
|
label: 'Filters',
|
||||||
expanded: true,
|
expanded: true,
|
||||||
leaf: false,
|
leaf: false,
|
||||||
|
hidden: filters.length === 0,
|
||||||
children: filters,
|
children: filters,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Sort',
|
label: 'Sort',
|
||||||
expanded: true,
|
expanded: true,
|
||||||
leaf: false,
|
leaf: false,
|
||||||
|
hidden: sort.length === 0,
|
||||||
children: sort,
|
children: sort,
|
||||||
},
|
},
|
||||||
] as TreeNode[],
|
] as TreeNode[],
|
||||||
|
|||||||
@@ -46,11 +46,37 @@
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td [attr.colspan]="data.headers.length + 1">
|
<td [attr.colspan]="data.headers.length + 1">
|
||||||
<span>
|
<div class="left">
|
||||||
Showing {{ (data.page.page - 1) * data.page.size + 1 }} to
|
@for (btn of pagesize; track btn) {
|
||||||
{{ data.page.page * data.page.size }} of {{ data.total }}
|
<input
|
||||||
|
role="button"
|
||||||
|
type="submit"
|
||||||
|
(click)="onPageSize($event, btn.page)"
|
||||||
|
[class.clickable]="!btn.current"
|
||||||
|
[class.current]="btn.current"
|
||||||
|
[attr.value]="btn.page"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span class="center">
|
||||||
|
{{ (data.page.page - 1) * data.page.size + 1 }}-{{
|
||||||
|
Math.min(data.page.page * data.page.size, data.total)
|
||||||
|
}}
|
||||||
|
of {{ data.total }}
|
||||||
</span>
|
</span>
|
||||||
<span> </span>
|
<div class="right">
|
||||||
|
@for (btn of pager; track page) { @if (btn.ellipsis) {
|
||||||
|
<span>...</span>
|
||||||
|
} @else {
|
||||||
|
<input
|
||||||
|
role="button"
|
||||||
|
type="submit"
|
||||||
|
(click)="onPage($event, btn.page)"
|
||||||
|
[class.clickable]="!btn.current"
|
||||||
|
[class.current]="btn.current"
|
||||||
|
[attr.value]="btn.page"
|
||||||
|
/>} }
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
@@ -56,6 +57,27 @@ tfoot {
|
|||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:hover {
|
||||||
|
background-color: #eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.current {
|
||||||
|
background-color: #95b9c7;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
Output,
|
||||||
|
SimpleChanges,
|
||||||
|
} from '@angular/core';
|
||||||
import { Header } from '../../models/header';
|
import { Header } from '../../models/header';
|
||||||
import {
|
import {
|
||||||
faSort,
|
faSort,
|
||||||
@@ -18,24 +25,108 @@ import { Page } from '../../models/page';
|
|||||||
templateUrl: './table.component.html',
|
templateUrl: './table.component.html',
|
||||||
styleUrl: './table.component.scss',
|
styleUrl: './table.component.scss',
|
||||||
})
|
})
|
||||||
export class TableComponent {
|
export class TableComponent implements OnChanges {
|
||||||
@Input() data!: Result;
|
@Input() data!: Result;
|
||||||
|
|
||||||
@Output() page = new EventEmitter<Page>();
|
@Output() page = new EventEmitter<Page>();
|
||||||
@Output() sort = new EventEmitter<Header>();
|
@Output() sort = new EventEmitter<Header>();
|
||||||
|
|
||||||
|
Math = Math;
|
||||||
faSortDown = faSortDown;
|
faSortDown = faSortDown;
|
||||||
faSortUp = faSortUp;
|
faSortUp = faSortUp;
|
||||||
faSort = faSort;
|
faSort = faSort;
|
||||||
SORT = SORT;
|
SORT = SORT;
|
||||||
|
pagesize: PageButton[] = [];
|
||||||
|
pager: PageButton[] = [];
|
||||||
|
|
||||||
onPage(event: MouseEvent): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
const data = changes['data']?.currentValue;
|
||||||
|
if (data) {
|
||||||
|
this.updatePageSize(data);
|
||||||
|
this.updatePager(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPage(event: MouseEvent, page: number): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.page.emit();
|
const current = this.data?.page?.page;
|
||||||
|
const size = this.data?.page?.size;
|
||||||
|
if (current === page) return;
|
||||||
|
this.page.emit(new Page({ page: page, size: size }));
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSize(event: MouseEvent, size: number): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
const current = this.data?.page?.size;
|
||||||
|
if (current === size) return;
|
||||||
|
|
||||||
|
const page = this.data?.page ?? { page: 1, size: 100 };
|
||||||
|
const start = (page.page - 1) * page.size + 1;
|
||||||
|
const newPage = Math.ceil(start / size);
|
||||||
|
this.page.emit(new Page({ page: newPage, size: size }));
|
||||||
}
|
}
|
||||||
|
|
||||||
onSort(event: MouseEvent, header: Header): void {
|
onSort(event: MouseEvent, header: Header): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.sort.emit(header);
|
this.sort.emit(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updatePageSize(data: Result): void {
|
||||||
|
const size = data.page?.size ?? 20;
|
||||||
|
const sizes = [10, 20, 50].map(
|
||||||
|
(i) => new PageButton({ page: i, current: i === size })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.pagesize = sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePager(data: Result): void {
|
||||||
|
const page = data.page?.page ?? 1;
|
||||||
|
const last = Math.ceil(data.total / (data.page?.size ?? data.total));
|
||||||
|
|
||||||
|
// get the list of pages
|
||||||
|
const pages = [
|
||||||
|
1,
|
||||||
|
page + 1 > last ? page - 2 : 1,
|
||||||
|
page - 1,
|
||||||
|
page,
|
||||||
|
page + 1,
|
||||||
|
page < 3 ? 3 : last,
|
||||||
|
last,
|
||||||
|
]
|
||||||
|
// unique
|
||||||
|
.filter((v, i, a) => a.indexOf(v) === i)
|
||||||
|
// in range
|
||||||
|
.filter((i) => 1 <= i && i <= last);
|
||||||
|
|
||||||
|
const buttons = pages.map(
|
||||||
|
(i) =>
|
||||||
|
new PageButton({
|
||||||
|
page: i,
|
||||||
|
current: i === page,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (buttons.length > 2) {
|
||||||
|
if (buttons[0].page !== buttons[1].page - 1) {
|
||||||
|
buttons.splice(1, 0, new PageButton({ ellipsis: true }));
|
||||||
|
}
|
||||||
|
const length = buttons.length;
|
||||||
|
if (buttons[length - 2].page !== buttons[length - 1].page - 1) {
|
||||||
|
buttons.splice(length - 1, 0, new PageButton({ ellipsis: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pager = buttons;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageButton {
|
||||||
|
page: number;
|
||||||
|
current: boolean;
|
||||||
|
ellipsis: boolean;
|
||||||
|
|
||||||
|
constructor(data: Partial<PageButton>) {
|
||||||
|
this.page = data?.page ?? -1;
|
||||||
|
this.current = data?.current ?? false;
|
||||||
|
this.ellipsis = data?.ellipsis ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
|
@if (!node.hidden) {
|
||||||
<div
|
<div
|
||||||
*ngIf="!node?.hidden"
|
|
||||||
class="treenode"
|
class="treenode"
|
||||||
[class.disabled]="node.disabled"
|
[class.disabled]="node.disabled"
|
||||||
[class.clickable]="!node.leaf"
|
[class.clickable]="!node.leaf"
|
||||||
(click)="toggle($event)"
|
(click)="toggle($event)"
|
||||||
>
|
>
|
||||||
<span *ngIf="!node.leaf" class="icon">
|
@if (!node.leaf) {
|
||||||
<fa-icon *ngIf="node.expanded" [icon]="faCaretDown"></fa-icon>
|
<span class="icon">
|
||||||
<fa-icon *ngIf="!node.expanded" [icon]="faCaretRight"></fa-icon>
|
<fa-icon [icon]="node.expanded ? faCaretDown : faCaretRight"></fa-icon>
|
||||||
</span>
|
</span>
|
||||||
|
}
|
||||||
<span [title]="node.label" class="fill ellipsis">{{ node.label }}</span>
|
<span [title]="node.label" class="fill ellipsis">{{ node.label }}</span>
|
||||||
<ng-container *ngIf="node.leaf && !node.disabled" class="actions">
|
@if (node.leaf && !node.disabled) { @for (action of node.actions; track
|
||||||
<fa-icon
|
action) {
|
||||||
*ngFor="let action of node.actions"
|
<fa-icon
|
||||||
class="action clickable"
|
class="action clickable"
|
||||||
[icon]="action.icon"
|
[icon]="action.icon"
|
||||||
(click)="onActionClick($event, action)"
|
(click)="onActionClick($event, action)"
|
||||||
></fa-icon>
|
></fa-icon>
|
||||||
</ng-container>
|
} }
|
||||||
</div>
|
</div>
|
||||||
|
} @if (node && node.children && node.expanded) {
|
||||||
<div
|
<div [class.branch]="node.label !== ''">
|
||||||
*ngIf="node && node.children && node.expanded"
|
@for (child of node.children; track child) {
|
||||||
[class.branch]="node.label !== ''"
|
<fbi-tree [node]="child" (actionClick)="passActionClick($event)"></fbi-tree>
|
||||||
>
|
}
|
||||||
<fbi-tree
|
|
||||||
*ngFor="let child of node.children"
|
|
||||||
[node]="child"
|
|
||||||
(actionClick)="passActionClick($event)"
|
|
||||||
></fbi-tree>
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
11
src/components/window/window-config.ts
Normal file
11
src/components/window/window-config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface WindowConfig {
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
|
||||||
|
resizable?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
36
src/components/window/window.component.html
Normal file
36
src/components/window/window.component.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<div
|
||||||
|
class="resize vertical left"
|
||||||
|
[class.grab]="resizing"
|
||||||
|
(mousedown)="startResize($event, Resize.Left)"
|
||||||
|
></div>
|
||||||
|
<div class="wrap">
|
||||||
|
<div
|
||||||
|
class="resize horizontal top"
|
||||||
|
[class.grab]="resizing"
|
||||||
|
(mousedown)="startResize($event, Resize.Top)"
|
||||||
|
></div>
|
||||||
|
<div class="header" [class.grab]="dragging" (mousedown)="startDrag($event)">
|
||||||
|
@if (config.title) {
|
||||||
|
<span class="title ellipsis">{{ config.title }}</span>
|
||||||
|
} @if (config.closable ?? true) {
|
||||||
|
<fa-icon
|
||||||
|
[icon]="faRectangleXmark"
|
||||||
|
class="icon clickable"
|
||||||
|
(click)="onClose($event)"
|
||||||
|
></fa-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="resize horizontal bottom"
|
||||||
|
[class.grab]="resizing"
|
||||||
|
(mousedown)="startResize($event, Resize.Bottom)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="resize vertical right"
|
||||||
|
[class.grab]="resizing"
|
||||||
|
(mousedown)="startResize($event, Resize.Right)"
|
||||||
|
></div>
|
||||||
93
src/components/window/window.component.scss
Normal file
93
src/components/window/window.component.scss
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
:host {
|
||||||
|
position: absolute;
|
||||||
|
// border: 1px solid black;
|
||||||
|
background-color: transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: lightgray;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
padding: 2px 4px 0px 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
|
height: 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical {
|
||||||
|
height: 100%;
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
cursor: ns-resize;
|
||||||
|
// margin-top: 2px;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
cursor: ns-resize;
|
||||||
|
// margin-bottom: 2px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
cursor: ew-resize;
|
||||||
|
// margin-left: 2px;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
cursor: ew-resize;
|
||||||
|
// margin-right: 2px;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grab {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
background-color: white;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
23
src/components/window/window.component.spec.ts
Normal file
23
src/components/window/window.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WindowComponent } from './window.component';
|
||||||
|
|
||||||
|
describe('WindowComponent', () => {
|
||||||
|
let component: WindowComponent;
|
||||||
|
let fixture: ComponentFixture<WindowComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [WindowComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WindowComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
140
src/components/window/window.component.ts
Normal file
140
src/components/window/window.component.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
HostBinding,
|
||||||
|
Inject,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||||
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { faRectangleXmark } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { WindowConfig } from './window-config';
|
||||||
|
|
||||||
|
export enum Resize {
|
||||||
|
Top = 't',
|
||||||
|
Right = 'r',
|
||||||
|
Bottom = 'b',
|
||||||
|
Left = 'l',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-window',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FontAwesomeModule],
|
||||||
|
templateUrl: './window.component.html',
|
||||||
|
styleUrls: ['./window.component.scss'],
|
||||||
|
})
|
||||||
|
export class WindowComponent {
|
||||||
|
@HostBinding('style.height.px') height: number = 200;
|
||||||
|
@HostBinding('style.width.px') width: number = 400;
|
||||||
|
@HostBinding('style.left.px') x: number = 200;
|
||||||
|
@HostBinding('style.top.px') y: number = 200;
|
||||||
|
@HostBinding('style.visibility') visibility: string = 'hidden';
|
||||||
|
private _display: boolean = false;
|
||||||
|
private get visible() {
|
||||||
|
return this._display;
|
||||||
|
}
|
||||||
|
private set visible(value: boolean) {
|
||||||
|
this._display = value;
|
||||||
|
this.visibility = value ? 'visible' : 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input() config!: WindowConfig;
|
||||||
|
@Output() closing = new EventEmitter<void>();
|
||||||
|
|
||||||
|
dragging: boolean = false;
|
||||||
|
resizing: boolean = false;
|
||||||
|
|
||||||
|
faRectangleXmark = faRectangleXmark;
|
||||||
|
Resize = Resize;
|
||||||
|
|
||||||
|
constructor(@Inject(DOCUMENT) private _document: Document) {
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = (display?: boolean) => (this.visible = display ?? !this.visible);
|
||||||
|
hide = () => this.toggle(false);
|
||||||
|
show = () => this.toggle(true);
|
||||||
|
|
||||||
|
startDrag(event: MouseEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
const { innerHeight, innerWidth } = window;
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
const x = this.x;
|
||||||
|
const y = this.y;
|
||||||
|
const h = this.height;
|
||||||
|
const w = this.width;
|
||||||
|
const doc = this._document;
|
||||||
|
|
||||||
|
this.dragging = true;
|
||||||
|
const dragging = (e: MouseEvent) => {
|
||||||
|
this.x = Math.min(Math.max(x + e.clientX - clientX, 0), innerWidth - w);
|
||||||
|
this.y = Math.min(Math.max(y + e.clientY - clientY, 0), innerHeight - h);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragEnd = (e: MouseEvent) => {
|
||||||
|
this.dragging = false;
|
||||||
|
doc.removeEventListener('mousemove', dragging);
|
||||||
|
doc.removeEventListener('mouseup', dragEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
doc.addEventListener('mousemove', dragging);
|
||||||
|
doc.addEventListener('mouseup', dragEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
startResize(event: MouseEvent, anchor: Resize): void {
|
||||||
|
event.preventDefault();
|
||||||
|
const { innerHeight, innerWidth } = window;
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
const x = this.x;
|
||||||
|
const y = this.y;
|
||||||
|
const h = this.height;
|
||||||
|
const w = this.width;
|
||||||
|
const doc = this._document;
|
||||||
|
const minH = 32;
|
||||||
|
const minW = 100;
|
||||||
|
|
||||||
|
this.resizing = true;
|
||||||
|
const resizing = (e: MouseEvent) => {
|
||||||
|
const dx = e.clientX - clientX;
|
||||||
|
const dy = e.clientY - clientY;
|
||||||
|
|
||||||
|
switch (anchor) {
|
||||||
|
case Resize.Top:
|
||||||
|
this.y = Math.min(Math.max(y + dy, 0), y + h - minH);
|
||||||
|
if (this.y > 0)
|
||||||
|
this.height = Math.min(
|
||||||
|
Math.max(h - dy, minH),
|
||||||
|
innerHeight - this.y
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Resize.Bottom:
|
||||||
|
this.height = Math.min(Math.max(h + dy, minH), innerHeight - this.y);
|
||||||
|
break;
|
||||||
|
case Resize.Left:
|
||||||
|
this.x = Math.min(Math.max(x + dx, 0), x + w - minW);
|
||||||
|
if (this.x > 0)
|
||||||
|
this.width = Math.min(Math.max(w - dx, minW), innerWidth - this.x);
|
||||||
|
break;
|
||||||
|
case Resize.Right:
|
||||||
|
this.width = Math.min(Math.max(w + dx, minW), innerWidth - this.x);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeEnd = (e: MouseEvent) => {
|
||||||
|
this.resizing = false;
|
||||||
|
doc.removeEventListener('mousemove', resizing);
|
||||||
|
doc.removeEventListener('mouseup', resizeEnd);
|
||||||
|
};
|
||||||
|
doc.addEventListener('mousemove', resizing);
|
||||||
|
doc.addEventListener('mouseup', resizeEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(event: MouseEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
this.hide();
|
||||||
|
this.closing.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user