如何在角材料表中获得选定区域?

Posted

技术标签:

【中文标题】如何在角材料表中获得选定区域?【英文标题】:How can I get selected area in angular material table? 【发布时间】:2020-07-31 04:01:38 【问题描述】:

我想让用户像 Excel 表一样选择角表中的区域。如下图所示

我已经实现了点击多选行。

Stackblitz | multi selection rows

我不知道如何继续。

我发现了一些类似的插件,但不知道如何在 Angular 中实现这一点

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

任何帮助,建议表示赞赏

【问题讨论】:

【参考方案1】:

但它并没有那么复杂stackblitz

如果我们的 tds 是这样的

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

我们使用 viewChildren 获取单元格,并使用选择声明一个数组

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

在单元格中,我们有从上到下和从左到右的所有“td”顺序

如果你按下了 shifh 键或 control 键,功能选择会考虑

  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]
      
    
  

一个函数 isSelected 提供了改变类的可能性

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

带有 .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;

更新

在示例中,您需要shift &amp; ctrl。但是我们可以有一个复选框或一个选择来“改变选择方式” - 使用 [(ngModel)] - 并替换条件 event.shiftKey 和 event.ctrlKey。

如果你总是选择一个范围,你可以使用两个变量“fromIndex”和“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
  
 

 

【讨论】:

完美。有什么方法可以在没有shift&ctrl 按键的情况下选择单元格?像这个例子plnkr.co/edit/8wy5oMGEkLi1e9mg?preview 在示例中您需要shift &amp; ctrl。您可以使用复选框或选择来“更改选择方式”-使用 [(ngModel)]- 并替换条件 event.shiftKey 和 event.ctrlKey。如果您总是选择一个范围,则可以使用两个变量“fromIndex”和“toIndex”,请参阅更新后的答案【参考方案2】:

您可以借助自定义指令为 Angular 创建类似的插件。

Ng-run Example

它支持:

单击 - 选择单元格

Ctrl + 单击 - 切换单元格

Shift + 单击选择范围

单击并拖动以选择范围

rowSpan 和 colSpan 行为

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);
    
  

Your Forked Stackblitz

【讨论】:

完美。唯一的困惑是我如何区分行?我检查了所选项目,但无法获取行详细信息 你有selectedRange,这是一组单元格,每个单元格都有一个父级,即一行 我在 ParentElement 中找不到任何可以用来区分行的属性。我尝试将唯一 ID 添加到父元素以区分行。 stackblitz.com/edit/… 是否要获取所有选定的行? 尝试选择一些东西并查看控制台日志stackblitz.com/edit/…。它应该向您显示带有 rowId 的单元格列表【参考方案3】:

以下库提供类似的功能,但不是免费的。我不确定其他库

SyncFusion :

https://www.syncfusion.com/angular-ui-components/angular-spreadsheet#:~:text=The%20Angular%20Spreadsheet%20component%20can,Microsoft%20Excel%2097%2D2003%2

SyncFusion | Excel

https://ej2.syncfusion.com/angular/demos/?&_ga=2.263488904.50430700.1596175160-2025228585.1595393533#/material/spreadsheet/default

StackBlitz | Demo :

https://stackblitz.com/edit/angular-swdvq9?file=app.component.html

【讨论】:

【参考方案4】:

我建议尝试一些似乎被 angular 社区使用的第三方库:

ag 网格:

https://www.ag-grid.com/javascript-grid-range-selection/

Plunker example 用于农业网格范围选择

[enableRangeSelection]="true"

素面

https://www.primefaces.org/primeng/showcase/#/table/sections

Stackblitz example 用于primefaces

【讨论】:

以上是关于如何在角材料表中获得选定区域?的主要内容,如果未能解决你的问题,请参考以下文章

防止在角材料设计中自动填充保存的密码

如何避免使用角度材料从角度 2 中的 mat-table 传输相同的行

在ArcGIS中,如何获得指定区域内所有点的经纬度坐标

在角材料设计中调整 sidenav 的大小。

如何在角度材料 2 中使用 mat-chip 和自动完成来保存选定的对象

POI操作Excel如何禁止Excel中的复制和选定?