Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get selected area in angular material table?

I want to allow the users to select the area in angular table like excel sheets. Something like in the below image

enter image description here

I have achieved multi selection rows on click.

Stackblitz | multi selection rows

I am not sure how to proceed.

I found some similar plugins but not sure how to achieve that in Angular

  1. Excel-style Table Cell Selector Plugin With jQuery - TableCellsSelection
  2. Table Selection | CKEditor.com

Any help , suggestion is appreciated

like image 377
IamGrooot Avatar asked Mar 02 '23 05:03

IamGrooot


2 Answers

You can create similar plugin for Angular with the help of custom directive.

Ng-run Example

It supports:

  • Click - to select cell

  • Ctrl + Click - to toggle cell

  • Shift + Click to select range

  • Click and Drag to select range

  • rowSpan and colSpan behavior

range-selection.directive.ts

import { Directive, ElementRef, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
import { fromEvent, pipe, Subject } from 'rxjs';
import { filter, map, switchMap, takeUntil, tap } from 'rxjs/operators';

@Directive({
  selector: 'table[range-selection]',
})
export class RangeSelectionDirective implements OnDestroy, OnInit {
  @Input() selectionClass = 'state--selected';

  selectedRange = new Set<HTMLTableCellElement>();

  private readonly table: HTMLTableElement;

  private startCell: HTMLTableCellElement = null;

  private cellIndices = new Map<HTMLTableCellElement, { row: number; column: number }>();

  private selecting: boolean;

  private destroy$ = new Subject<void>();

  constructor(private zone: NgZone, {nativeElement}: ElementRef<HTMLTableElement>) {
    this.table = nativeElement;
  }

  ngOnInit() {
    this.zone.runOutsideAngular(() => this.initListeners());
  }

  private initListeners() {
    const withCell = pipe(
      map((event: MouseEvent) => ({event, cell: (event.target as HTMLElement).closest<HTMLTableCellElement>('th,td')})),
      filter(({cell}) => !!cell),
    );
    const mouseDown$ = fromEvent<MouseEvent>(this.table, 'mousedown')
      .pipe(
        filter(event => event.button === 0),
        withCell,
        tap(this.startSelection)
      );
    const mouseOver$ = fromEvent<MouseEvent>(this.table, 'mouseover');
    const mouseUp$ = fromEvent(document, 'mouseup').pipe(
      tap(() => this.selecting = false)
    );
    this.handleOutsideClick();

    mouseDown$.pipe(
      switchMap(() => mouseOver$.pipe(takeUntil(mouseUp$))),
      takeUntil(this.destroy$),
      withCell
    ).subscribe(this.select);
  }

  private handleOutsideClick() {
    fromEvent(document, 'mouseup').pipe(
      takeUntil(this.destroy$)
    ).subscribe((event: any) => {
      if (!this.selecting && !this.table.contains(event.target as HTMLElement)) {
        this.clearCells();
      }
    });
  }

  private startSelection = ({cell, event}: { event: MouseEvent, cell: HTMLTableCellElement }) => {
    this.updateCellIndices();
    if (!event.ctrlKey && !event.shiftKey) {
      this.clearCells();
    }

    if (event.shiftKey) {
      this.select({cell});
    }

    this.selecting = true;
    this.startCell = cell;

    if (!event.shiftKey) {
      if (this.selectedRange.has(cell)) {
        this.selectedRange.delete(cell);
      } else {
        this.selectedRange.add(cell);
      }
      cell.classList.toggle(this.selectionClass);
    }
  };

  private select = ({cell}: { cell: HTMLTableCellElement }) => {
    this.clearCells();
    this.getCellsBetween(this.startCell, cell).forEach(item => {
      this.selectedRange.add(item);
      item.classList.add(this.selectionClass);
    });
  };

  private clearCells() {
    Array.from(this.selectedRange).forEach(cell => {
      cell.classList.remove(this.selectionClass);
    });
    this.selectedRange.clear();
  }

  private getCellsBetween(start: HTMLTableCellElement, end: HTMLTableCellElement) {
    const startCoords = this.cellIndices.get(start);
    const endCoords = this.cellIndices.get(end);
    const boundaries = {
      top: Math.min(startCoords.row, endCoords.row),
      right: Math.max(startCoords.column + start.colSpan - 1, endCoords.column + end.colSpan - 1),
      bottom: Math.max(startCoords.row + start.rowSpan - 1, endCoords.row + end.rowSpan - 1),
      left: Math.min(startCoords.column, endCoords.column),
    };

    const cells = [];

    iterateCells(this.table, (cell) => {
      const { column, row } = this.cellIndices.get(cell);
      if (column >= boundaries.left && column <= boundaries.right &&
        row >= boundaries.top && row <= boundaries.bottom) {
        cells.push(cell);
      }
    });

    return cells;
  }

  private updateCellIndices() {
    this.cellIndices.clear();
    const matrix = [];
    iterateCells(this.table, (cell, y, x) => {
      for (; matrix[y] && matrix[y][x]; x++) {}
      for (let spanX = x; spanX < x + cell.colSpan; spanX++) {
        for (let spanY = y; spanY < y + cell.rowSpan; spanY++) {
          (matrix[spanY] = matrix[spanY] || [])[spanX] = 1;
        }
      }
      this.cellIndices.set(cell, {row: y, column: x});
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
  }
}

function iterateCells(table: HTMLTableElement, callback: (cell: HTMLTableCellElement, y: number, x: number) => void): void {
  for (let y = 0; y < table.rows.length; y++) {
    for (let x = 0; x < table.rows[y].cells.length; x++) {
      callback(table.rows[y].cells[x], y, x);
    }
  }
}

enter image description here

Your Forked Stackblitz

like image 102
yurzui Avatar answered Mar 04 '23 19:03

yurzui


But it's not so complex do it stackblitz

If our tds are like

<ng-container matColumnDef="position">
    <th mat-header-cell *matHeaderCellDef> No. </th>
    <td mat-cell #cell  *matCellDef="let element;let i=index" 
    (click)="select($event,cell)"
    <!--use isSelected(i,0) for the first column
            isSelected(i,1) for the second column
            ...
    -->
    [ngClass]="{'selected':isSelected(i,0)}"
    > {{element.position}} </td>
</ng-container>

We get the cells using viewChildren, and declare an array with the selection

  selection: number[]=[];
  @ViewChildren("cell", { read: ElementRef }) cells: QueryList<ElementRef>;

In cells we has all the "td" order from top to bottom and from left to rigth

The function select take account if you are pressed the shifh key or the control key

  select(event: MouseEvent, cell: any) {
    //search the cell "clicked"
    const cellClick = this.cells.find(x => x.nativeElement == event.target);

    //get the index of this cells
    let indexSelected = -1;
    this.cells.forEach((x, i) => {
      if (x == cellClick) indexSelected = i;
    });

   
    if (event.ctrlKey) { //if ctrl pressed

      if (this.selection.indexOf(indexSelected)>=0) //if its yet selected
        this.selection=this.selection.filter(x=>x!=indexSelected);
      else                                          //if it's not selected
        this.selection.push(indexSelected)
    } else {
      if (event.shiftKey) {    //if the key shift is pressed
        if (this.selection.length)      //if there any more selected
        {

          //calculate the row and col of fisrt element we selected
          let rowFrom=this.selection[0]%this.dataSource.data.length;
          let colFrom=Math.floor(this.selection[0]/this.dataSource.data.length)

          //idem from the index selected
          let rowTo=indexSelected%this.dataSource.data.length;
          let colTo=Math.floor(indexSelected/this.dataSource.data.length)

           //interchange if from is greater than to
          if (rowFrom>rowTo)
            [rowFrom, rowTo] = [rowTo, rowFrom]
          if (colFrom>colTo)
            [colFrom, colTo] = [colTo, colFrom]

          //clean the array
          this.selection=[]

          //we run througth all the td to check if we need push or not
          this.cells.forEach((x,index)=>{
            const row=index%this.dataSource.data.length;
            const col=Math.floor(index/this.dataSource.data.length)
            if (row>=rowFrom && row<=rowTo && col>=colFrom && col<=colTo)
              this.selection.push(index)
          })
        }
        else   //if there're anything selected and the shit is pressed
        this.selection = [indexSelected]
      } else {  //if no key shit nor key ctrl
        this.selection = [indexSelected]
      }
    }
  }

And a function isSelected give as posibility to change the class

  isSelected(row,column)
  {
    const index=column*this.dataSource.data.length+row
    return this.selection.indexOf(index)>=0
  }

with a .css

table {
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
td { 
  outline: none!important 
}
.selected
{
  border:1px solid red;
}

Update

In the example you need shift & ctrl. But we can has a checkbox or a select to "change the way of selection" -use [(ngModel)]- and replace the conditions event.shiftKey and event.ctrlKey.

If you allways select a range you can use two variables "fromIndex" and "toIndex",

fromIndex:number=-1
toIndex:number=-1

select(event: MouseEvent, cell: any){
   ....
  ..get the indexSelected ...
  if (this.fromIndex==-1){ //If nothing select
     this.fromIndex=indexSelected
     this.toIndex=-1
  }
  else{
     this.toIndex=indexSelected;
  }
  if (this.toIndex>=0 && this.fromIndex>=0)
  {
      ...the code to select range
  }
} 

 
like image 34
Eliseo Avatar answered Mar 04 '23 18:03

Eliseo