HELP Column resize buttons user/sleary/grid-classes-wip
authorStephanie Leary <stephanie.leary@equinoxoli.org>
Wed, 29 Mar 2023 16:08:37 +0000 (16:08 +0000)
committerStephanie Leary <stephanie.leary@equinoxoli.org>
Wed, 29 Mar 2023 16:08:37 +0000 (16:08 +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 47e4d7f..581ac75 100644 (file)
@@ -75,6 +75,7 @@ table.table.eg-grid {
 
 .eg-grid-header-cell {
   font-weight: 600;
+  position: relative;
   white-space: normal;
 }
 
@@ -97,6 +98,27 @@ table.table.eg-grid {
   font-size: 14px;
 }
 
+.col-resize {
+  display: block;
+  background: transparent;
+  border: 0;
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 5px;
+  height: 100%;
+  cursor: col-resize;
+}
+
+.col-resize:hover,
+.col-resize:active,
+.col-resize:focus,
+.resizing {
+  background-color: rgba(0,0,225,0.05);
+  border-right: 2px solid blue;
+  cursor: col-resize;
+}
+
 .eg-grid-toolbar {
   display: flex;
 }
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..d930ed7 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,11 +1028,35 @@ 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':
+                        //str = this.insert_wbr(str);
+                        break;
+                    default: break;
+                }
+                return str;
             }
         }
     }
 
+    insert_wbr(txt: string): string {
+        const doubleSlash = txt.split('//');
+        const formatted = doubleSlash.map(str =>
+          // Insert a word break opportunity after a colon
+          str.replace(/(?<after>:)/giu, '$1<wbr>')
+            // Before a single slash, tilde, at symbol, comma, hyphen, underline, question mark, number sign, or percent symbol
+            .replace(/(?<before>[/~@,\-_?#%])/giu, '<wbr>$1')
+            // Before and after an equals sign, period, or ampersand
+            .replace(/(?<beforeAndAfter>[=.&])/giu, '<wbr>$1<wbr>')
+          // Reconnect the strings with word break opportunities after double slashes
+          ).join('//<wbr>');
+      
+        return formatted;
+    }
+
     selectOneRow(index: any) {
         this.rowSelector.clear();
         this.rowSelector.select(index);
@@ -1423,6 +1453,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);
+        });
+    }
 }