From 501c2e88d1d8496b5a6ab518371eae9f00f86d60 Mon Sep 17 00:00:00 2001 From: James Fusia Date: Wed, 18 Sep 2024 20:37:25 -0400 Subject: [PATCH] Added windows --- package-lock.json | 13 ++ package.json | 1 + .../metadata/metadata.component.html | 7 +- src/components/metadata/metadata.component.ts | 23 ++- src/components/query/query.component.html | 2 +- src/components/query/query.component.ts | 2 + src/components/table/table.component.html | 34 ++++- src/components/table/table.component.scss | 22 +++ src/components/table/table.component.ts | 99 ++++++++++++- src/components/tree/tree.component.html | 41 +++-- src/components/window/window-config.ts | 11 ++ src/components/window/window.component.html | 36 +++++ src/components/window/window.component.scss | 93 ++++++++++++ .../window/window.component.spec.ts | 23 +++ src/components/window/window.component.ts | 140 ++++++++++++++++++ 15 files changed, 511 insertions(+), 36 deletions(-) create mode 100644 src/components/window/window-config.ts create mode 100644 src/components/window/window.component.html create mode 100644 src/components/window/window.component.scss create mode 100644 src/components/window/window.component.spec.ts create mode 100644 src/components/window/window.component.ts diff --git a/package-lock.json b/package-lock.json index 52ed1e6..781e4d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5acbc55..14eb1d9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/metadata/metadata.component.html b/src/components/metadata/metadata.component.html index 1c170da..b2f2614 100644 --- a/src/components/metadata/metadata.component.html +++ b/src/components/metadata/metadata.component.html @@ -1,6 +1,11 @@ -
Fields!
+
Fields
+ +content content content content content content content content content + content content content content content content content content content + diff --git a/src/components/metadata/metadata.component.ts b/src/components/metadata/metadata.component.ts index 095260e..8f835e2 100644 --- a/src/components/metadata/metadata.component.ts +++ b/src/components/metadata/metadata.component.ts @@ -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) => 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', } diff --git a/src/components/query/query.component.html b/src/components/query/query.component.html index e19c5f3..b1b910f 100644 --- a/src/components/query/query.component.html +++ b/src/components/query/query.component.html @@ -1,4 +1,4 @@ -
Query!
+
Query
- - Showing {{ (data.page.page - 1) * data.page.size + 1 }} to - {{ data.page.page * data.page.size }} of {{ data.total }} +
+ @for (btn of pagesize; track btn) { + + } +
+ + {{ (data.page.page - 1) * data.page.size + 1 }}-{{ + Math.min(data.page.page * data.page.size, data.total) + }} + of {{ data.total }} - +
+ @for (btn of pager; track page) { @if (btn.ellipsis) { + ... + } @else { + } } +
diff --git a/src/components/table/table.component.scss b/src/components/table/table.component.scss index 29f92e0..36f1410 100644 --- a/src/components/table/table.component.scss +++ b/src/components/table/table.component.scss @@ -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; +} diff --git a/src/components/table/table.component.ts b/src/components/table/table.component.ts index ef2755d..1341dce 100644 --- a/src/components/table/table.component.ts +++ b/src/components/table/table.component.ts @@ -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(); @Output() sort = new EventEmitter
(); + 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) { + this.page = data?.page ?? -1; + this.current = data?.current ?? false; + this.ellipsis = data?.ellipsis ?? false; + } } diff --git a/src/components/tree/tree.component.html b/src/components/tree/tree.component.html index 0efd0ba..2546ef6 100644 --- a/src/components/tree/tree.component.html +++ b/src/components/tree/tree.component.html @@ -1,32 +1,29 @@ +@if (!node.hidden) {
- - - + @if (!node.leaf) { + + + } {{ node.label }} - - - + @if (node.leaf && !node.disabled) { @for (action of node.actions; track + action) { + + } }
- -
- +} @if (node && node.children && node.expanded) { +
+ @for (child of node.children; track child) { + + }
+} diff --git a/src/components/window/window-config.ts b/src/components/window/window-config.ts new file mode 100644 index 0000000..6f71e33 --- /dev/null +++ b/src/components/window/window-config.ts @@ -0,0 +1,11 @@ +export interface WindowConfig { + height?: number; + width?: number; + x?: number; + y?: number; + + resizable?: boolean; + closable?: boolean; + + title?: string; +} diff --git a/src/components/window/window.component.html b/src/components/window/window.component.html new file mode 100644 index 0000000..8fb59a6 --- /dev/null +++ b/src/components/window/window.component.html @@ -0,0 +1,36 @@ +
+
+
+
+ @if (config.title) { + {{ config.title }} + } @if (config.closable ?? true) { + + } +
+
+ +
+
+
+
diff --git a/src/components/window/window.component.scss b/src/components/window/window.component.scss new file mode 100644 index 0000000..929f8d9 --- /dev/null +++ b/src/components/window/window.component.scss @@ -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; +} diff --git a/src/components/window/window.component.spec.ts b/src/components/window/window.component.spec.ts new file mode 100644 index 0000000..46ffab2 --- /dev/null +++ b/src/components/window/window.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WindowComponent } from './window.component'; + +describe('WindowComponent', () => { + let component: WindowComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WindowComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(WindowComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/components/window/window.component.ts b/src/components/window/window.component.ts new file mode 100644 index 0000000..34aca0b --- /dev/null +++ b/src/components/window/window.component.ts @@ -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(); + + 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(); + } +}