首页 > 解决方案 > 角垫表和移位点击选择

问题描述

我正在尝试在MatDataTable使用角度和打字稿的排序上实现移位单击功能。

基本的失败是,每当在表上注册点击事件时,都会存储选择的行。

如果检测到移位单击,组件将尝试在最后选定的行和当前选定的行之间进行选择(就像 Windows 中的移位单击选择一样)。

我拥有的事件处理代码如下:

clickHandler(event, row, index) {
    console.log('index clicked: ' + index);
    if (event.ctrlKey) {
        this.selectRows(row, index); // This records this.lastSelected
    } else if (event.shiftKey) {
        this.selectRowsFill(row, index); 
    } else {
        this.selectElement(row, index); // As does this call.
    }
}

// This function computes a start and end point for autoselection of 
// rows between this.lastSelected, and the currently clicked row
selectRowsFill(row, index) {
    const indexA = this.lastSelected;
    const indexB = index;
    if (indexA > indexB) {
        // Descending order
        this.selectRowsBetween(indexB, indexA);
    } else {
        // Ascending order
        this.selectRowsBetween(indexA, indexB);
    }
}

// And this performs the actual selection.
private selectRowsBetween(start, end) {
    let currentIndex = 0;
    this.dataSource.data.forEach(row => {
        if (currentIndex >= start && currentIndex <= end) {
            this.selection.select(row);
        }
        currentIndex++;
    });
}

和 HTML:

<mat-row *matRowDef="let row; let i = index; columns: cols;" (click)="clickHandler($event, row, i)" [ngClass]="{'inDatabase' : isAdded(row), 'highlight': isSelectedAndAdded(row) || isSelected(row) }">

只要表格未排序,此代码就可以正常工作。一旦我对 应用排序算法MatTableDataSource,它就会改变数据的顺序,导致选择出现故障。看起来选择是基于 中数据的原始(未排序)顺序MatTableDataSource,这是有道理的。

那么如何让 shift click selection 处理已排序的数据,而不是未排序的数据?

标签: angulartypescript

解决方案


这是我的解决方案:>STACKBLITZ<

代码保存以防 STACKBLITZ 离线

  • html
<table [shiftClickSource]="dataSource"
       [shiftClickSelectModel]="selection"
       [shiftClickSourceId]="['position']"
       mat-table [dataSource]="dataSource" class="mat-elevation-z8" matSort>

....
  • ts(指令)
    import { Directive, Input, HostListener, OnInit, OnDestroy, ElementRef } from '@angular/core';

    import {SelectionModel, SelectionChange} from '@angular/cdk/collections';

    import { Subject, BehaviorSubject, Observable, merge, pipe, } from 'rxjs';
    import { shareReplay, takeUntil, withLatestFrom, tap } from 'rxjs/operators';

    import {MatTable, MatTableDataSource} from '@angular/material/table';

    /**
     * Directive that adds shift-click selection to your mat-table.
     * It needs the datasource from the host, the selectionModel of the checkboxes 
     * and optionally an array of [ids] to find the correct rows in the
     * (possibly sorted/ filtered) source array.
     */

    @Directive({
      selector: '[shiftClickSource]'
    })
    export class ShiftClickDirective implements OnInit, OnDestroy {

      finishHim = new Subject<void>();

      lastItem: any;
      lastShiftSelection: any[];

      alwaysTheRightSourceArray$: Observable<any>;
      // the always right source array :-)
      source: any[];
      shiftHolding$ = new BehaviorSubject<boolean>(false);
      //click that happens on tbody of mat-table
      click$ = new Subject<void>();
      // observable to record the last change of the checkbox selectionModel
      recordSelectionChange$: Observable<SelectionChange<any>>;

      @HostListener('document:keydown.shift', ['$event'])
      shiftDown(_) {
        this.shiftHolding$.next(true);
      }
      @HostListener('document:keyup.shift', ['$event'])
      shiftUp(event: KeyboardEvent) {
        this.shiftHolding$.next(false);
      }

      // datasource that is used on the Checkbox-Table
      @Input('shiftClickSource') dataSource: MatTableDataSource<any>;
      // Is optional, if id col is known this will improve performance.
      @Input('shiftClickSourceId') idArr: string[];
      @Input('shiftClickSelectModel') selection: SelectionModel<any>;

      constructor(private host: ElementRef) {}

      ngOnInit() {

        // data can change order (sorting) and can be reduced (filtering)
        this.alwaysTheRightSourceArray$ = this.dataSource.connect()
        .pipe(
          tap(_ => {
            this.source = this.dataSource
            .sortData(
              this.dataSource.filteredData,
              this.dataSource.sort
            );
          })
        );

        // lets record the last change of 
        this.recordSelectionChange$ = this.selection.changed.pipe(
          shareReplay(1)
        )

        // clicks on tbody mean that we need to do something
        this.host.nativeElement.childNodes[1].addEventListener("click", function() {
          this.click$.next();
        }.bind(this));

        const reactOnClickOnTbody$ = 
        this.click$.pipe(
          withLatestFrom(this.shiftHolding$.asObservable()),
          withLatestFrom(this.recordSelectionChange$, (arr, c): [SelectionChange<any>, boolean ] => [c, arr[1]] ),
          tap( arr => {
                const v = arr[0];
                const sh = arr[1];
                const ans = [...v.added, ...v.removed][0];
                if ( sh && this.lastItem ) {
                  this.onTbodyClicked(this.lastItem, ans);
                } else {
                  this.lastItem = ans;
                  this.lastShiftSelection = undefined;
                  // console.log('clear');
                }
          }),
        );

        merge(
          reactOnClickOnTbody$,
          this.alwaysTheRightSourceArray$
        ).pipe(takeUntil(this.finishHim.asObservable()))
        .subscribe();
      }

      // This function does all the real work.
      onTbodyClicked(from: object, to: object) {

        // console.log('onTbodyClickedRuns');
        const fIdx = this.getIndexBasedOnData(from);
        const tIdx = this.getIndexBasedOnData(to);

        let ans;
        let ans_last;
        if( fIdx > tIdx ) {
          ans  = this.source.slice(tIdx, fIdx);
        } else {
          // console.log('seconds index case');
          ans  = this.source.slice(fIdx +1 , tIdx + 1);

        }

        if (this.lastShiftSelection) {
          this.selection['_emitChanges']=false;
          this.selection.deselect(...this.lastShiftSelection);
          this.selection['_emitChanges']=true;
        }

        this.lastShiftSelection = [...ans];

        if( fIdx > tIdx ) {
          ans_last = ans.shift();
        } else {
          ans_last = ans.pop();      
        }
        // console.log(this.lastShiftSelection);

        const cond =  ans.every(el => this.selection.isSelected(el)) && !this.selection.isSelected(ans_last)

        if ( cond ) {
            // console.log('deselect')
            this.selection['_emitChanges']=false;
            this.selection.deselect(...ans);
            this.selection['_emitChanges']=true;
        } else {
            // console.log('select')
            this.selection['_emitChanges']=false;
            this.selection.select(...ans, ans_last);
            this.selection['_emitChanges']=true;
        }    

      }

      // helper function
      private getIndexBasedOnData(row, source = this.source): number {

        let ind: number;
        if (this.idArr) {
          ind = source.findIndex( _d => this.idArr.every(id => _d[id] === row[id] ));
        } else {
          ind = source.findIndex( _d => Object.keys(row).every(k => _d[k] === row[k] ));
        }

        return ind;
      }

      ngOnDestroy() {
        this.finishHim.next();
        this.finishHim.complete();
      }

    }

推荐阅读