chromium/content/browser/resources/private_aggregation/private_aggregation_internals_table.ts

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';
import {CustomElement} from 'chrome://resources/js/custom_element.js';

import {getTemplate} from './private_aggregation_internals_table.html.js';
import type {TableModel} from './table_model.js';

/**
 * Helper function for setting sort attributes on |th|.
 */
function setSortAttrs(th: HTMLElement, sortDesc: boolean|null) {
  let nextDir;
  if (sortDesc === null) {
    th.ariaSort = 'none';
    nextDir = 'ascending';
  } else if (sortDesc) {
    th.ariaSort = 'descending';
    nextDir = 'ascending';
  } else {
    th.ariaSort = 'ascending';
    nextDir = 'descending';
  }

  th.title = `Sort by ${th.innerText} ${nextDir}`;
  th.ariaLabel = th.title;
}

/**
 * Table abstracts the logic for rendering and sorting a table. The table's
 * columns are supplied by a TableModel supplied to the decorate function. Each
 * Column knows how to render the underlying value of the row type T, and
 * optionally sort rows of type T by that value.
 */
export class PrivateAggregationInternalsTableElement<T> extends CustomElement {
  static override get template() {
    return getTemplate();
  }

  private model_: TableModel<T>|null = null;
  private sortDesc_: boolean = false;

  setModel(model: TableModel<T>) {
    this.model_ = model;
    this.sortDesc_ = false;

    const tr = this.getRequiredElement('tr');
    model.cols.forEach((col, idx) => {
      const th = document.createElement('th');
      th.scope = 'col';
      col.renderHeader(th);

      if (col.compare) {
        th.setAttribute('role', 'button');
        setSortAttrs(th, /*sortDesc=*/ null);
        th.addEventListener('click', () => this.changeSortHeader_(idx));
      }

      tr.appendChild(th);
    });

    this.addEmptyStateRow_();
    this.model_.rowsChangedListeners.add(() => this.updateTbody());
  }

  private addEmptyStateRow_() {
    const td = document.createElement('td');
    assert(this.model_);
    td.textContent = this.model_.emptyRowText;
    td.colSpan = this.model_.cols.length;
    const tr = document.createElement('tr');
    tr.appendChild(td);
    const tbody = this.getRequiredElement('tbody');
    tbody.appendChild(tr);
  }

  private changeSortHeader_(idx: number) {
    const ths = this.$all<HTMLElement>('thead th');

    assert(this.model_);
    if (idx === this.model_.sortIdx) {
      this.sortDesc_ = !this.sortDesc_;
    } else {
      this.sortDesc_ = false;
      if (this.model_.sortIdx >= 0) {
        setSortAttrs(ths[this.model_.sortIdx]!, /*descending=*/ null);
      }
    }

    this.model_.sortIdx = idx;
    setSortAttrs(ths[this.model_.sortIdx]!, this.sortDesc_);
    this.updateTbody();
  }

  private sort_(rows: T[]) {
    assert(this.model_);
    if (this.model_.sortIdx < 0) {
      return;
    }

    const multiplier = this.sortDesc_ ? -1 : 1;
    rows.sort(
        (a, b) => this.model_!.cols[this.model_!.sortIdx]!.compare!(a, b) *
            multiplier);
  }

  updateTbody() {
    const tbody = this.getRequiredElement('tbody');
    tbody.innerText = '';

    assert(this.model_);
    const rows = this.model_.getRows();
    if (rows.length === 0) {
      this.addEmptyStateRow_();
      return;
    }

    this.sort_(rows);

    rows.forEach((row) => {
      const tr = document.createElement('tr');
      assert(this.model_);
      this.model_.cols.forEach((col) => {
        const td = document.createElement('td');
        col.render(td, row);
        tr.appendChild(td);
      });
      this.model_.styleRow(tr, row);
      tbody.appendChild(tr);
    });
  }
}

customElements.define(
    'aggregation-service-internals-table',
    PrivateAggregationInternalsTableElement);