table grid column resize buttons user/sleary/grid-tables-and-column-widths
authorStephanie Leary <stephanie.leary@equinoxoli.org>
Mon, 22 May 2023 15:26:34 +0000 (15:26 +0000)
committerStephanie Leary <stephanie.leary@equinoxoli.org>
Mon, 22 May 2023 15:27:25 +0000 (15:27 +0000)
Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org>
Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
Open-ILS/src/eg2/src/app/share/grid/grid.component.css
Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts

index c603367..c160891 100644 (file)
@@ -39,5 +39,9 @@
     <ng-container *ngIf="context.isFilterable">
       <eg-grid-filter-control [context]="context" [col]="col"></eg-grid-filter-control>
     </ng-container>
+
+    <button class="col-resize">
+      <span class="visually-hidden" i18n>Adjust {{col.headerLabel}} width</span>
+    </button>
   </th>
 </tr>
\ No newline at end of file
index efd2ac9..b344816 100644 (file)
@@ -75,6 +75,7 @@ table.table.eg-grid {
 
 .eg-grid-header-cell {
   font-weight: 600;
+  position: relative;
   white-space: normal;
 }
 
index 0d8fcc1..95a9bfa 100644 (file)
@@ -1,5 +1,5 @@
 import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
-    OnDestroy, ViewChild, ViewEncapsulation} from '@angular/core';
+    OnDestroy, ViewChild, ViewEncapsulation, Renderer2, ElementRef} from '@angular/core';
 import {IdlService} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
@@ -145,10 +145,12 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
         private idl: IdlService,
         private org: OrgService,
         private store: ServerStoreService,
-        private format: FormatService
+        private format: FormatService,
+        private renderer: Renderer2,
+        private gridTable: ElementRef
     ) {
         this.context =
-            new GridContext(this.idl, this.org, this.store, this.format);
+            new GridContext(this.idl, this.org, this.store, this.format, this.renderer, this.gridTable);
         this.onRowActivate = new EventEmitter<any>();
         this.onRowClick = new EventEmitter<any>();
         this.rowSelectionChange = new EventEmitter<string[]>();
index 04d19c4..2fb783f 100644 (file)
@@ -1,7 +1,7 @@
 /**
  * Collection of grid related classses and interfaces.
  */
-import {TemplateRef, EventEmitter, QueryList} from '@angular/core';
+import {TemplateRef, EventEmitter, AfterViewInit, QueryList, Renderer2, ElementRef} from '@angular/core';
 import {Observable, Subscription, empty} from 'rxjs';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
@@ -663,6 +663,9 @@ export class GridContext {
     showDeclaredFieldsOnly: boolean;
     cellTextGenerator: GridCellTextGenerator;
     reloadOnColumnChange: boolean;
+    ColX: number;
+    ColW: number;
+    charWidth: number;
 
     // Allow calling code to know when the select-all-rows-in-page
     // action has occurred.
@@ -680,7 +683,9 @@ export class GridContext {
         idl: IdlService,
         org: OrgService,
         store: ServerStoreService,
-        format: FormatService) {
+        format: FormatService,
+        private renderer: Renderer2,
+        private gridTable: ElementRef) {
 
         this.idl = idl;
         this.org = org;
@@ -705,6 +710,7 @@ export class GridContext {
             this.pager.limit = this.disablePaging ? MAX_ALL_ROW_COUNT : 10;
         }
         this.generateColumns();
+        this.generateColumnResizers();
     }
 
     // Load initial settings and data.
@@ -1022,7 +1028,16 @@ export class GridContext {
             if (col.cellTemplate) {
                 return ''; // avoid 'undefined' values
             } else {
-                return this.getRowColumnValue(row, col);
+                let str = this.getRowColumnValue(row, col);
+                switch (col.name) {
+                    case 'name':
+                    case 'url':
+                    case 'email':
+                        //TODO: insert <wbr> around punctuation
+                        break;
+                    default: break;
+                }
+                return str;
             }
         }
     }
@@ -1423,6 +1438,87 @@ export class GridContext {
         // smush into string and replace dots in name and path
         return classes.join(' ').replaceAll('.', '');
     }
+
+    generateColumnResizers() {
+        if (!this.gridTable) { return; }
+        
+        const cols = this.gridTable.nativeElement.querySelectorAll('th');
+        cols.forEach((col) => {
+            // Find resizer element
+            const resizer = col.nativeElement.querySelector('button.col-resize');
+            if (resizer) {
+                this.createResizableColumn(col, resizer);
+            }
+        });
+    }
+
+    createResizableColumn(col, resizer) {
+        // Track the current position of mouse
+        let x = 0;
+        let w = 0;
+
+        const mouseDownHandler = function ($event) {
+            // Get the current mouse position
+            x = $event.clientX;
+
+            // Calculate the current width of column
+            const styles = window.getComputedStyle(col);
+            w = parseInt(styles.width);
+
+            // Attach listeners for document's events
+            document.addEventListener('pointermove', mouseMoveHandler);
+            document.addEventListener('pointerup', mouseUpHandler);
+        };
+
+        const mouseMoveHandler = function ($event) {
+            // Determine how far the mouse has been moved
+            const dx = $event.clientX - x;
+
+            // Update the width of column
+            col.style.width = `${w + dx}px`;
+        };
+
+        // When user releases the mouse, remove the existing event listeners
+        // also recalculate grabber height
+        // also save column width to user prefs
+        const mouseUpHandler = function ($event) {
+            document.removeEventListener('pointermove', mouseMoveHandler);
+            document.removeEventListener('pointerup', mouseUpHandler);
+
+            /* Recalculate grabber height in case cells reflowed */
+            this.setColumnHandleHeight(this.gridTable);
+
+            // TODO: save column width in ch
+        };
+
+        resizer.addEventListener('pointerdown', mouseDownHandler);
+
+        // Handle keyboard events
+
+        resizer.addEventListener("keydown", ($event) => {
+            const th = $event.currentTarget.closest("th");
+
+            // TODO: find out if screen reader users would prefer we use a combo (probably)
+            if ($event.code == "ArrowLeft") {
+                th.style.width = (th.offsetWidth - this.charWidth) + 'px';
+            }
+            if ($event.code == "ArrowRight") {
+                th.style.width = (th.offsetWidth + this.charWidth) + 'px';
+            }
+
+            /* Recalculate grabber height in case cells reflowed */ 
+            this.setColumnHandleHeight(this.gridTable);
+        });
+    }
+
+    setColumnHandleHeight($event) {
+        /* Recalculate all handle heights in case cells reflowed */
+        const tableHeight = this.gridTable.nativeElement.offsetHeight + 'px';
+        console.log("table height is " + tableHeight);
+        this.gridTable.nativeElement.querySelectorAll('.col-resize').forEach((btn) => {
+            this.renderer.setStyle(btn, 'height', tableHeight);
+        });
+    }
 }