使用按键在 PrimeNG 滚动 Table 上选择行不会更改滚动位置

Selecting Rows On PrimeNG Scroll Table With Keypress Doesn't Change Scroll Location

Current Stackblitz Here

几个星期前,我问并回答了

截至目前,代码允许用户在“搜索”输入处于焦点时在 table 行上上下导航并单击“输入”以 select table 行。但是,当使用滚动 table 时,如 stackblitz 所示,selected table 行不会出现,因为它隐藏在滚动条下方。如果我使用 tab 导航到 table 单元格,那么它也会滚动,但不会像在这个用例中那样使用箭头键。

是否可以根据 selectedProduct 移动滚动条位置,这样 table 行就不会隐藏在下方?

Table 在 HTML 中设置:

<p-table
  #dl
  [columns]="cols"
  [value]="products"
  selectionMode="single"
  [(selection)]="selectedProduct"
  (onFilter)="onFilter($event, dt)"
  [scrollable]="true"
  scrollHeight="270px"
> 
...
<!-- table columns and code here -->
</p-table>

组件 TS(删除了一些不必要的信息 -- 可以在 StackBlitz 中找到)

// imports

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  @ViewChild('dl') table: Table;

  cpt = 0;
  products: Product[];
  visibleProducts: Product[];
  selectedProduct: Product;

  cols: any[];

  constructor( ...) {}

  ngOnInit() {
    this.productService.getProductsSmall().then((data) => {
      this.products = data.slice();
      this.visibleProducts = this.products;
      this.selectedProduct = this.visibleProducts[0];
    });

    this.cols = [...];
  }

  onFilter(event, dt) {
    this.cpt = 0;
    if (event.filteredValue.length > 0) {
      this.selectedProduct = event.filteredValue[0];
      this.visibleProducts = event.filteredValue;
    }
  }

  @HostListener('keydown.ArrowUp', ['$event']) ArrowUp($event: KeyboardEvent) {
    if (this.cpt > 0) {
      this.cpt--;
    }
    this.selectedProduct = this.visibleProducts[this.cpt];
  }

  @HostListener('keydown.ArrowDown', ['$event']) ArrowDown(
    $event: KeyboardEvent
  ) {
    if (this.cpt < this.visibleProducts.length - 1) {
      this.cpt++;
    }
    this.selectedProduct = this.visibleProducts[this.cpt];
  }

  @HostListener('keydown.Enter', ['$event']) Enter($event: KeyboardEvent) {
    alert('opening product: ' + this.selectedProduct.name);
  }
}


好的,我最终找到了一个可能的解决方案。我还创建了一个 issue for PrimeNG 以希望将来包含类似的内容,因为以前对此类功能的功能请求尚未完成。

我在我的组件中添加了一个名为 scrollTable() 的函数。

这个函数所做的是首先获取数据table 包装器元素,然后是这个包装器的行。 接下来,我根据之前的 cpt 全局变量将 rowEl 设置为等于当前活动行。如果行项目是数组中的第一个,包装器将滚动到顶部。如果它不是第一项,那么一个小检查是 运行 查看偏移量是否大于总体大小(从 wrapper.offsetHeight 看起来与 p-table 相同' s scrollHeight 属性)。然后计算包装器的 scrolltop 位置并用一个小的偏移量更改以适应 table 行的大小:

app.component.ts

...

  scrollTable() {
    // No need to scroll if there aren't any products
    if (!this.products.length) {
      return;
    }

    let wrapper = this.table.el.nativeElement.children[0].getElementsByClassName(
      'p-datatable-wrapper'
    )[0];
    let rows = wrapper.getElementsByClassName('p-selectable-row');

    console.log(wrapper);
    let rowEl = rows[this.cpt];

    // Scroll to top if first item
    if (this.cpt === 0) {
      wrapper.scrollTop = 0;
    }
    // Change scroll position if not first item and at bottom of scrollbar
    if (rowEl.offsetTop + rowEl.offsetHeight > wrapper.offsetHeight) {
      wrapper.scrollTop +=
        rowEl.offsetTop +
        rowEl.offsetHeight -
        wrapper.scrollTop -
        wrapper.offsetHeight +
        18;
    }
  }
...

这是一个 updated StackBlitz 组件中的有效解决方案。

感谢 this GitHub issue 中的评论,它们为我提供了完成滚动部分的初始步骤(因为我已经拥有箭头键功能)

更新

我制定了一个指令,不仅处理 PrimeNG table 行的箭头键导航,还处理滚动。这适用于任何单个 selectable table 包括使用过滤器时。过滤器通过 @Input 传入,突出显示的行通过 @Output 传入。它可能并不完美,但在用户需要更多键盘功能时解决了很多用例。当按下 enter 时,onRowSelect(...) 从 table 中触发,您可以使用组件中的任何函数。希望对其他人有帮助...

StackBlitz with Directive.

import {
  Directive,
  HostListener,
  Output,
  EventEmitter,
  Input,
} from '@angular/core';
import { Table } from 'primeng/table';

@Directive({
  selector: '[tableNavigation]',
})
export class TableNavigationDirective {
  /////////
  // (onRowSelect)="doSomething()"
  // (highlightProduct)="selectedProduct = $event" for the highlighting.
  // 'selectedProduct' will be equal to the [(selection)] value in your component
  // [visibleItems]="visibleItems" from component for filtering
  /////////

  @Output() highlightProduct: EventEmitter<any> = new EventEmitter();

  // If you want to include filtering, you'll want to use this input.
  _visibleItems: any;
  @Input('visibleItems') set visibleItems(items: any) {
    this._visibleItems = items;
    if (this._visibleItems) {
      this.rowIndex = 0;
      this.highlightProduct.emit(this._visibleItems[0]);
      if (this.table.scrollable) {
        this.scrollTable();
      }
    }
  }

  wrapper: any;
  rowIndex = 0;

  constructor(private table: Table) {
    this._visibleItems = this.table.value;
  }

  @HostListener('keydown.ArrowUp', ['$event']) ArrowUp($event: KeyboardEvent) {
    $event.preventDefault();

    if (this.rowIndex > 0) {
      this.rowIndex--;
    }
    this.highlightProduct.emit(this._visibleItems[this.rowIndex]);
    if (this.table.scrollable) {
      this.scrollTable();
    }
  }

  @HostListener('keydown.ArrowDown', ['$event']) ArrowDown(
    $event: KeyboardEvent
  ) {
    $event.preventDefault();

    if (this.rowIndex < this._visibleItems.length - 1) {
      this.rowIndex++;
    }
    this.highlightProduct.emit(this._visibleItems[this.rowIndex]);
    if (this.table.scrollable) {
      this.scrollTable();
    }
  }

  @HostListener('keydown.Enter', ['$event']) Enter($event: KeyboardEvent) {
    this.table.onRowSelect.emit({
      originalEvent: $event,
      index: this.rowIndex,
      data: this.table.selection,
      type: 'row',
    });
  }

  scrollTable() {
    if (this.table.el.nativeElement.children[0].getElementsByClassName('p-datatable-scrollable-body')[0]) {
      this.wrapper = this.table.el.nativeElement.children[0].getElementsByClassName('p-datatable-scrollable-body')[0];
    } else if (this.table.el.nativeElement.children[0].getElementsByClassName('p-datatable-wrapper')[0]) {
      this.wrapper = this.table.el.nativeElement.children[0].getElementsByClassName('p-datatable-wrapper')[0];
    } else {
      this.wrapper = undefined;
      console.error("wrapper is undefined, scroll won't work");
    }

    let rows = this.wrapper.getElementsByClassName('p-selectable-row');

    // No need to scroll if there aren't any products
    if (!rows.length) {
      return;
    }
    let rowEl = rows[this.rowIndex];

    // Scroll all the way to top if first item
    if (rowEl === rows[0]) {
      this.wrapper.scrollTop = 0;
    }

    // Change scroll position if not first item and at bottom of scrollbar
    if (rowEl.offsetTop + rowEl.offsetHeight > this.wrapper.offsetHeight) {
      this.wrapper.scrollTop +=
        rowEl.offsetTop +
        rowEl.offsetHeight -
        this.wrapper.scrollTop -
        this.wrapper.offsetHeight;
    }
  }
}