chromium/ui/file_manager/file_manager/foreground/js/ui/combobutton.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 implements a combobutton control.
 */

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

import {boolAttrSetter, crInjectTypeAndInit} from '../../../common/js/cr_ui.js';
import type {DropdownItem} from '../task_controller.js';

import {FilesMenuItem} from './files_menu.js';
import type {MenuItemActivationEvent} from './menu_item.js';
import {MultiMenuButton} from './multi_menu_button.js';


export type ComboButtonSelectEvent = CustomEvent<DropdownItem|null>;

// Internal interface used for ComboButton, it attaches the `data` to a MenuItem
// to read it back in the `MenuActivate` event.
type MenuItemForComboButton = FilesMenuItem&{
  data: DropdownItem,
};

function isFilesMenuItem(menuItem: HTMLElement):
    menuItem is MenuItemForComboButton {
  const menu = menuItem.closest('cr-menu');
  if (!menu) {
    return false;
  }

  return menu && menu.classList.contains('files-menu');
}

/**
 * Creates a new combo button element.
 */
export class ComboButton extends MultiMenuButton {
  private defaultItem_: null|DropdownItem = null;
  private trigger_: null|HTMLElement = null;
  private actionNode_: HTMLElement|null = null;

  /**
   * Truncates drop-down list.
   */
  clear() {
    this.menu?.clear();
    this.multiple = false;
  }

  addDropDownItem(item: DropdownItem) {
    assert(this.menu);
    this.multiple = true;
    const menuItem = this.menu!.addMenuItem(item);
    assert(isFilesMenuItem(menuItem));
    crInjectTypeAndInit(menuItem, FilesMenuItem);
    menuItem.toggleManagedIcon(/*visible=*/ !!item.isPolicyDefault);
    if (item.isDefault) {
      menuItem.setIsDefaultAttribute();
    }

    menuItem.data = item;
    // Move backgroundImage from the menu item container to the child icon.
    menuItem.iconStartImage = menuItem.style.backgroundImage;
    menuItem.style.backgroundImage = '';

    if (item.iconType) {
      menuItem.iconStartImage = '';
      menuItem.iconStartFileType = item.iconType;
    }
    menuItem.toggleAttribute('disabled', !!item.isDlpBlocked);
    return menuItem;
  }

  /**
   * Adds separator to drop-down list.
   */
  addSeparator() {
    this.menu?.addSeparator();
  }

  /**
   * Default item to fire on combobox click
   */
  get defaultItem(): DropdownItem|null {
    return this.defaultItem_;
  }

  set defaultItem(defaultItem) {
    this.defaultItem_ = defaultItem;
    this.actionNode_!.textContent = defaultItem?.label || '';
  }

  override initialize() {
    super.initialize();

    assert(this.menu);

    this.classList.add('combobutton');

    const buttonLayer = this.ownerDocument.createElement('div');
    buttonLayer.classList.add('button');
    this.appendChild(buttonLayer);

    this.actionNode_ = this.ownerDocument.createElement('div');
    this.actionNode_.classList.add('action');
    buttonLayer.appendChild(this.actionNode_);

    const triggerIcon = this.ownerDocument.createElement('iron-icon');
    triggerIcon.setAttribute('icon', 'files:arrow-drop-down');
    this.trigger_ = this.ownerDocument.createElement('div');
    this.trigger_.classList.add('trigger');
    this.trigger_.appendChild(triggerIcon);

    buttonLayer.appendChild(this.trigger_);

    this.addEventListener('click', this.handleButtonClick_.bind(this));

    this.trigger_.addEventListener(
        'click', this.handleTriggerClicked_.bind(this));

    this.menu.addEventListener('activate', this.handleMenuActivate_.bind(this));

    // Remove mousedown event listener created by MultiMenuButton::decorate,
    // and move it down to trigger_.
    this.removeEventListener('mousedown', this);
    this.trigger_.addEventListener('mousedown', this);
  }

  private handleTriggerClicked_(event: MouseEvent) {
    event.stopPropagation();
  }

  private handleMenuActivate_(e: Event) {
    const event = e as MenuItemActivationEvent;
    const target = event.target as HTMLElement;
    assert(isFilesMenuItem(target));
    this.dispatchSelectEvent_(target.data);
  }

  private handleButtonClick_(_event: MouseEvent) {
    if (this.multiple) {
      // When there are multiple choices just show/hide menu.
      if (this.isMenuShown()) {
        this.hideMenu();
      } else {
        this.showMenu(true);
      }
    } else {
      // When there is only 1 choice, just dispatch to open.
      this.blur();
      this.dispatchSelectEvent_(this.defaultItem_);
    }
  }

  private dispatchSelectEvent_(item: DropdownItem|null) {
    const selectEvent = new CustomEvent('combobutton-select', {detail: item});
    this.dispatchEvent(selectEvent);
  }

  /**
   * When there are multiple choices just show/hide menu.
   */
  get multiple(): boolean {
    return this.hasAttribute('multiple');
  }

  set multiple(value: boolean) {
    boolAttrSetter(this, 'multiple', value);
  }
}

declare global {
  interface HTMLElementEventMap {
    'combobutton-select': ComboButtonSelectEvent;
  }
}