chromium/ui/file_manager/file_manager/foreground/js/ui/list_single_selection_model.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.

import {dispatchPropertyChange} from 'chrome://resources/ash/common/cr_deprecated.js';
import {NativeEventTarget as EventTarget} from 'chrome://resources/ash/common/event_target.js';

/**
 * Creates a new selection model that is to be used with lists. This only
 * allows a single index to be selected.
 */
export class ListSingleSelectionModel extends EventTarget {
  private length_: number;
  // True if any item could be lead or anchor. False if only selected ones.
  private independentLeadItem_: boolean = false;
  private leadIndex_: number = -1;
  private selectedIndex_: number = -1;
  private selectedIndexBefore_: number = -1;
  private changeCount_: number|null = null;

  /**
   * @param length The number items in the selection.
   */
  constructor(length?: number) {
    super();
    this.length_ = length ?? 0;
  }

  /**
   * The number of items in the model.
   */
  get length() {
    return this.length_;
  }

  /**
   *   The selected indexes.
   */
  get selectedIndexes() {
    const i = this.selectedIndex;
    return i !== -1 ? [this.selectedIndex] : [];
  }

  set selectedIndexes(indexes) {
    this.selectedIndex_ = indexes.length ? indexes[0]! : -1;
  }

  /**
   * Convenience getter which returns the first selected index.
   * Setter also changes lead and anchor indexes if value is nonegative.
   */
  get selectedIndex(): number {
    return this.selectedIndex_;
  }

  set selectedIndex(selectedIndex: number) {
    const oldSelectedIndex = this.selectedIndex;
    const i = Math.max(-1, Math.min(this.length_ - 1, selectedIndex));

    if (i !== oldSelectedIndex) {
      this.beginChange();
      this.selectedIndex_ = i;
      this.leadIndex = i >= 0 ? i : this.leadIndex;
      this.endChange();
    }
  }

  /**
   * Selects a range of indexes, starting with {@code start} and ends with
   * {@code end}.
   * @param start The first index to select.
   * @param end The last index to select.
   */
  selectRange(start: number, end: number) {
    // Only select first index.
    this.selectedIndex = Math.min(start, end);
  }

  /**
   * Selects all indexes.
   */
  selectAll() {
    // Select all is not allowed on a single selection model
  }

  /**
   * Clears the selection
   */
  clear() {
    this.beginChange();
    this.length_ = 0;
    this.selectedIndex = this.anchorIndex = this.leadIndex = -1;
    this.endChange();
  }

  /**
   * Unselects all selected items.
   */
  unselectAll() {
    this.selectedIndex = -1;
  }

  /**
   * Sets the selected state for an index.
   * @param index The index to set the selected state for.
   * @param b Whether to select the index or not.
   */
  setIndexSelected(index: number, b: boolean) {
    // Only allow selection
    const oldSelected = index === this.selectedIndex_;
    if (oldSelected === b) {
      return;
    }

    if (b) {
      this.selectedIndex = index;
    } else if (index === this.selectedIndex_) {
      this.selectedIndex = -1;
    }
  }

  /**
   * Whether a given index is selected or not.
   * @param index The index to check.
   * @return Whether an index is selected.
   */
  getIndexSelected(index: number): boolean {
    return index === this.selectedIndex_;
  }

  /**
   * This is used to begin batching changes. Call {@code endChange} when you
   * are done making changes.
   */
  beginChange() {
    if (!this.changeCount_) {
      this.changeCount_ = 0;
      this.selectedIndexBefore_ = this.selectedIndex_;
    }
    this.changeCount_++;
  }

  /**
   * Call this after changes are done and it will dispatch a change event if
   * any changes were actually done.
   */
  endChange() {
    this.changeCount_!--;
    if (!this.changeCount_) {
      if (this.selectedIndexBefore_ !== this.selectedIndex_) {
        const indexes = [this.selectedIndexBefore_, this.selectedIndex_];
        this.dispatchEvent(new CustomEvent('change', {
          detail: {
            changes: indexes.filter(index => index !== -1)
                         .map((index) => ({
                                index: index,
                                selected: index === this.selectedIndex_,
                              })),
          },
        }));
      }
    }
  }

  /**
   * The leadIndex is used with multiple selection and it is the index that
   * the user is moving using the arrow keys.
   */
  get leadIndex() {
    return this.leadIndex_;
  }

  set leadIndex(leadIndex) {
    const li = this.adjustIndex_(leadIndex);
    if (li !== this.leadIndex_) {
      const oldLeadIndex = this.leadIndex_;
      this.leadIndex_ = li;
      dispatchPropertyChange(this, 'leadIndex', li, oldLeadIndex);
      dispatchPropertyChange(this, 'anchorIndex', li, oldLeadIndex);
    }
  }

  private adjustIndex_(index: number): number {
    index = Math.max(-1, Math.min(this.length_ - 1, index));
    if (!this.independentLeadItem_) {
      index = this.selectedIndex!;
    }
    return index;
  }

  /**
   * The anchorIndex is used with multiple selection.
   */
  get anchorIndex() {
    return this.leadIndex;
  }

  set anchorIndex(anchorIndex) {
    this.leadIndex = anchorIndex;
  }

  /**
   * Whether the selection model supports multiple selected items.
   */
  get multiple() {
    return false;
  }

  /**
   * Adjusts the selection after reordering of items in the table.
   * @param permutation The reordering permutation.
   */
  adjustToReordering(permutation: number[]) {
    if (this.leadIndex !== -1) {
      this.leadIndex = permutation[this.leadIndex]!;
    }

    const oldSelectedIndex = this.selectedIndex!;
    if (oldSelectedIndex !== -1) {
      this.selectedIndex = permutation[oldSelectedIndex]!;
    }
  }

  /**
   * Adjusts selection model length.
   * @param length New selection model length.
   */
  adjustLength(length: number) {
    this.length_ = length;
  }
}