chromium/chrome/browser/resources/print_preview/ui/destination_list.ts

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

import 'chrome://resources/cr_elements/cr_hidden_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
// <if expr="not is_chromeos">
import './destination_list_item.js';
// </if>
// <if expr="is_chromeos">
import './destination_list_item_cros.js';
// </if>
import './print_preview_vars.css.js';
import '../strings.m.js';
import './throbber.css.js';

import {ListPropertyUpdateMixin} from 'chrome://resources/cr_elements/list_property_update_mixin.js';
import type {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {Destination} from '../data/destination.js';

import {getTemplate} from './destination_list.html.js';

const DESTINATION_ITEM_HEIGHT = 32;

export interface PrintPreviewDestinationListElement {
  $: {
    list: IronListElement,
  };
}

const PrintPreviewDestinationListElementBase =
    ListPropertyUpdateMixin(PolymerElement);

export class PrintPreviewDestinationListElement extends
    PrintPreviewDestinationListElementBase {
  static get is() {
    return 'print-preview-destination-list';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      destinations: Array,

      searchQuery: Object,

      loadingDestinations: {
        type: Boolean,
        value: false,
      },

      matchingDestinations_: {
        type: Array,
        value: () => [],
      },

      hasDestinations_: {
        type: Boolean,
        value: true,
      },

      throbberHidden_: {
        type: Boolean,
        value: false,
      },

      hideList_: {
        type: Boolean,
        value: false,
      },
    };
  }

  destinations: Destination[];
  searchQuery: RegExp|null;
  loadingDestinations: boolean;
  private matchingDestinations_: Destination[];
  private hasDestinations_: boolean;
  private throbberHidden_: boolean;
  private hideList_: boolean;

  private boundUpdateHeight_: ((e: Event) => void)|null = null;

  static get observers() {
    return [
      'updateMatchingDestinations_(' +
          'destinations.*, searchQuery, loadingDestinations)',
    ];
  }

  override connectedCallback() {
    super.connectedCallback();

    this.boundUpdateHeight_ = () => this.updateHeight_();
    window.addEventListener('resize', this.boundUpdateHeight_);
  }

  override disconnectedCallback() {
    super.disconnectedCallback();

    window.removeEventListener('resize', this.boundUpdateHeight_!);
    this.boundUpdateHeight_ = null;
  }

  /**
   * This is a workaround to ensure that the iron-list correctly updates the
   * displayed destination information when the elements in the
   * |matchingDestinations_| array change, instead of using stale information
   * (a known iron-list issue). The event needs to be fired while the list is
   * visible, so firing it immediately when the change occurs does not always
   * work.
   */
  private forceIronResize_() {
    this.$.list.fire('iron-resize');
  }

  private updateHeight_(numDestinations?: number) {
    const count = numDestinations === undefined ?
        this.matchingDestinations_.length :
        numDestinations;

    const maxDisplayedItems = this.offsetHeight / DESTINATION_ITEM_HEIGHT;
    const isListFullHeight = maxDisplayedItems <= count;

    // Update the throbber and "No destinations" message.
    this.hasDestinations_ = count > 0 || this.loadingDestinations;
    this.throbberHidden_ =
        !this.loadingDestinations || isListFullHeight || !this.hasDestinations_;

    this.hideList_ = count === 0;
    if (this.hideList_) {
      return;
    }

    const listHeight =
        isListFullHeight ? this.offsetHeight : count * DESTINATION_ITEM_HEIGHT;
    this.$.list.style.height = listHeight > DESTINATION_ITEM_HEIGHT ?
        `${listHeight}px` :
        `${DESTINATION_ITEM_HEIGHT}px`;
  }

  private updateMatchingDestinations_() {
    if (this.destinations === undefined) {
      return;
    }

    const matchingDestinations = this.searchQuery ?
        this.destinations.filter(d => d.matches(this.searchQuery!)) :
        this.destinations.slice();

    // Update the height before updating the list.
    this.updateHeight_(matchingDestinations.length);
    this.updateList(
        'matchingDestinations_', destination => destination.key,
        matchingDestinations);

    this.forceIronResize_();
  }

  private onKeydown_(e: KeyboardEvent) {
    if (e.key === 'Enter') {
      this.onDestinationSelected_(e);
      e.stopPropagation();
    }
  }

  /**
   * @param e Event containing the destination that was selected.
   */
  private onDestinationSelected_(e: Event) {
    if ((e.composedPath()[0] as HTMLElement).tagName === 'A') {
      return;
    }

    this.dispatchEvent(new CustomEvent(
        'destination-selected',
        {bubbles: true, composed: true, detail: e.target}));
  }

  /**
   * Returns a 1-based index for aria-rowindex.
   */
  private getAriaRowindex_(index: number): number {
    return index + 1;
  }

  // <if expr="is_chromeos">
  updatePrinterStatusIcon(destinationKey: string) {
    const index = this.matchingDestinations_.findIndex(
        destination => destination.key === destinationKey);
    if (index === -1) {
      return;
    }

    this.notifyPath(`matchingDestinations_.${index}.printerStatusReason`);
  }
  // </if>
}

declare global {
  interface HTMLElementTagNameMap {
    'print-preview-destination-list': PrintPreviewDestinationListElement;
  }
}

customElements.define(
    PrintPreviewDestinationListElement.is, PrintPreviewDestinationListElement);