Compare commits

..

2 Commits

Author SHA1 Message Date
bc2c62f6b3 Merge pull request 'Added windows' (#4) from windows into main
Reviewed-on: #4
2024-09-18 22:49:41 -04:00
501c2e88d1 Added windows 2024-09-18 20:37:25 -04:00
15 changed files with 511 additions and 36 deletions

13
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"@fortawesome/angular-fontawesome": "^0.14.1",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"chart.js": "^4.4.3",
"papaparse": "^5.4.1",
@@ -2720,6 +2721,18 @@
"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": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",

View File

@@ -19,6 +19,7 @@
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"@fortawesome/angular-fontawesome": "^0.14.1",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"chart.js": "^4.4.3",
"papaparse": "^5.4.1",

View File

@@ -1,6 +1,11 @@
<div class="header">Fields!</div>
<div class="header">Fields</div>
<fbi-tree
class="tree"
[node]="node"
(actionClick)="onActionClick($event)"
></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>

View File

@@ -1,22 +1,30 @@
import { Component } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { TreeComponent } from '../tree/tree.component';
import { TreeNode } from '../../models/tree-node';
import { MetaService } from '../../services/meta.service';
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 { combineLatest } from 'rxjs';
import { Query } from '../../models/query';
import { WindowComponent } from '../window/window.component';
@Component({
selector: 'fbi-metadata',
standalone: true,
imports: [TreeComponent],
imports: [TreeComponent, WindowComponent],
templateUrl: './metadata.component.html',
styleUrl: './metadata.component.scss',
})
export class MetadataComponent {
node: TreeNode = new TreeNode({});
@ViewChild(WindowComponent) window!: WindowComponent;
windowComponent = WindowComponent;
windowConfig = {
title: 'test',
};
constructor(metaService: MetaService, private queryService: QueryService) {
combineLatest({
@@ -33,7 +41,10 @@ export class MetadataComponent {
const children = node.children ?? [];
children.forEach((child: Partial<TreeNode>) => recurse(child));
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[];
}
};
@@ -53,6 +64,9 @@ export class MetadataComponent {
case ACTIONS.ADD:
this.queryService.add(event.node.data);
break;
case ACTIONS.FILTER:
this.window.show();
break;
}
}
@@ -72,4 +86,5 @@ export class MetadataComponent {
enum ACTIONS {
ADD = 'add',
FILTER = 'filter',
}

View File

@@ -1,4 +1,4 @@
<div class="header">Query!</div>
<div class="header">Query</div>
<fbi-tree
class="tree"
[node]="node"

View File

@@ -50,12 +50,14 @@ export class QueryComponent {
label: 'Filters',
expanded: true,
leaf: false,
hidden: filters.length === 0,
children: filters,
},
{
label: 'Sort',
expanded: true,
leaf: false,
hidden: sort.length === 0,
children: sort,
},
] as TreeNode[],

View File

@@ -46,11 +46,37 @@
<tfoot>
<tr>
<td [attr.colspan]="data.headers.length + 1">
<span>
Showing {{ (data.page.page - 1) * data.page.size + 1 }} to
{{ data.page.page * data.page.size }} of {{ data.total }}
<div class="left">
@for (btn of pagesize; track btn) {
<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>
<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>
</tr>
</tfoot>

View File

@@ -1,5 +1,6 @@
table {
border-collapse: collapse;
min-width: 400px;
}
thead {
@@ -56,6 +57,27 @@ tfoot {
color: #777;
}
div.left {
float: left;
}
div.right {
float: right;
}
.clickable {
cursor: pointer;
}
input {
border: 1px solid #ddd;
background-color: #eee;
}
input:hover {
background-color: #eff;
}
input.current {
background-color: #95b9c7;
}

View File

@@ -1,5 +1,12 @@
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 {
faSort,
@@ -18,24 +25,108 @@ import { Page } from '../../models/page';
templateUrl: './table.component.html',
styleUrl: './table.component.scss',
})
export class TableComponent {
export class TableComponent implements OnChanges {
@Input() data!: Result;
@Output() page = new EventEmitter<Page>();
@Output() sort = new EventEmitter<Header>();
Math = Math;
faSortDown = faSortDown;
faSortUp = faSortUp;
faSort = faSort;
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();
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 {
event.stopPropagation();
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;
}
}

View File

@@ -1,32 +1,29 @@
@if (!node.hidden) {
<div
*ngIf="!node?.hidden"
class="treenode"
[class.disabled]="node.disabled"
[class.clickable]="!node.leaf"
(click)="toggle($event)"
>
<span *ngIf="!node.leaf" class="icon">
<fa-icon *ngIf="node.expanded" [icon]="faCaretDown"></fa-icon>
<fa-icon *ngIf="!node.expanded" [icon]="faCaretRight"></fa-icon>
@if (!node.leaf) {
<span class="icon">
<fa-icon [icon]="node.expanded ? faCaretDown : faCaretRight"></fa-icon>
</span>
}
<span [title]="node.label" class="fill ellipsis">{{ node.label }}</span>
<ng-container *ngIf="node.leaf && !node.disabled" class="actions">
<fa-icon
*ngFor="let action of node.actions"
class="action clickable"
[icon]="action.icon"
(click)="onActionClick($event, action)"
></fa-icon>
</ng-container>
@if (node.leaf && !node.disabled) { @for (action of node.actions; track
action) {
<fa-icon
class="action clickable"
[icon]="action.icon"
(click)="onActionClick($event, action)"
></fa-icon>
} }
</div>
<div
*ngIf="node && node.children && node.expanded"
[class.branch]="node.label !== ''"
>
<fbi-tree
*ngFor="let child of node.children"
[node]="child"
(actionClick)="passActionClick($event)"
></fbi-tree>
} @if (node && node.children && node.expanded) {
<div [class.branch]="node.label !== ''">
@for (child of node.children; track child) {
<fbi-tree [node]="child" (actionClick)="passActionClick($event)"></fbi-tree>
}
</div>
}

View File

@@ -0,0 +1,11 @@
export interface WindowConfig {
height?: number;
width?: number;
x?: number;
y?: number;
resizable?: boolean;
closable?: boolean;
title?: string;
}

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

View 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;
}

View 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();
});
});

View 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();
}
}