chromium/ui/file_manager/file_manager/foreground/js/ui/table/table_list.ts

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

/**
 * @fileoverview This extends List for use in the table.
 */

import {jsSetter} from '../../../../common/js/cr_ui.js';
import {List} from '../list.js';
import type {ListItem} from '../list_item.js';

import type {Table} from './table.js';


/**
 * Creates a new table list element.
 */
export abstract class TableList extends List {
  private table_: Table|null = null;

  override initialize() {
    super.initialize();
    this.className = 'list';
  }

  /**
   * Resizes columns. Called when column width changed.
   */
  resize() {
    if (this.needsFullRedraw_()) {
      this.redraw();
      return;
    }
    if (this.updateScrollbars_()) {
      List.prototype.redraw.call(this);
    }  // Redraw items only.
    this.resizeCells_();
  }

  /**
   * Updates width of cells.
   */
  private resizeCells_() {
    const cm = this.table_!.columnModel;
    for (let row = this.firstElementChild; row; row = row.nextElementSibling) {
      if (row.tagName !== 'LI') {
        continue;
      }

      for (let i = 0; i < cm.size; i++) {
        const child = row.children[i]! as HTMLElement;
        child.style.width = cm.getWidth(i) + 'px';
      }
      (row as HTMLElement).style.width = cm.totalWidth + 'px';
    }
    if (this.afterFiller_) {
      this.afterFiller_.style.width = cm.totalWidth + 'px';
    }
  }

  /**
   * Redraws the viewport.
   */
  override redraw() {
    if (this.batchCount_ !== 0) {
      return;
    }
    this.updateScrollbars_();

    List.prototype.redraw.call(this);
    this.resizeCells_();
  }

  /**
   * Returns the height of after filler in the list.
   * @param lastIndex The index of item past the last in viewport.
   * @return The height of after filler.
   */
  override getAfterFillerHeight(lastIndex: number): number {
    // If the list is empty set height to 1 to show horizontal
    // scroll bar.
    return lastIndex === 0 ?
        1 :
        List.prototype.getAfterFillerHeight.call(this, lastIndex);
  }

  /**
   * Shows or hides vertical and horizontal scroll bars in the list.
   * @return True if horizontal scroll bar changed.
   */
  private updateScrollbars_(): boolean {
    const cm = this.table_!.columnModel;
    const style = this.style;
    if (!cm || cm.size === 0) {
      if (style.overflow !== 'hidden') {
        style.overflow = 'hidden';
        return true;
      } else {
        return false;
      }
    }

    let height = this.offsetHeight;
    let changed = false;
    const offsetWidth = this.offsetWidth;
    if (cm.totalWidth > offsetWidth) {
      if (style.overflowX !== 'scroll') {
        style.overflowX = 'scroll';
      }
      // Once we sure there will be horizontal
      // scrollbar calculate with this height.
      height = this.clientHeight;
    }
    if (this.areAllItemsVisible_(height)) {
      if (cm.totalWidth <= offsetWidth && style.overflowX !== 'hidden') {
        style.overflowX = 'hidden';
      }
      changed = this.showVerticalScrollBar_(false);
    } else {
      changed = this.showVerticalScrollBar_(true);
      const x = cm.totalWidth <= this.clientWidth ? 'hidden' : 'scroll';
      if (style.overflowX !== x) {
        style.overflowX = x;
      }
    }
    return changed;
  }

  /**
   * Shows or hides vertical scroll bar.
   * @param show True to show.
   * @return True if visibility changed.
   */
  private showVerticalScrollBar_(show: boolean): boolean {
    const style = this.style;
    if (show && style.overflowY === 'scroll') {
      return false;
    }
    if (!show && style.overflowY === 'hidden') {
      return false;
    }
    style.overflowY = show ? 'scroll' : 'hidden';
    return true;
  }

  /**
   * @param visibleHeight Height in pixels.
   * @return True if all rows could be accomodiated in
   *                   visibleHeight pixels.
   */
  private areAllItemsVisible_(visibleHeight: number): boolean {
    if (!this.dataModel || this.dataModel.length === 0) {
      return true;
    }
    return this.getItemTop(this.dataModel.length) <= visibleHeight;
  }

  /**
   * Creates a new list item.
   * @param dataItem The value to use for the item.
   * @return The newly created list item.
   */
  override createItem(dataItem: unknown): ListItem {
    return this.table_!.getRenderFunction().call(
               null, dataItem, this.table_!) as ListItem;
  }

  /**
   * Determines whether a full redraw is required.
   */
  private needsFullRedraw_(): boolean {
    const cm = this.table_!.columnModel;
    const row = this.firstElementChild as HTMLElement;
    // If the number of columns in the model has changed, a full redraw is
    // needed.
    if (row.children.length !== cm.size) {
      return true;
    }
    // If the column visibility has changed, a full redraw is required.
    for (let i = 0; i < cm.size; ++i) {
      const child = row.children[i]! as HTMLElement;
      if (cm.isVisible(i) === child.hidden) {
        return true;
      }
    }
    return false;
  }

  /**
   * The table associated with the list.
   */
  get table(): Table|null {
    return this.table_;
  }

  set table(value: Table) {
    jsSetter(this, 'table', value);
  }
}