chromium/chrome/browser/resources/pdf/elements/viewer_download_controls.ts

// Copyright 2020 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_action_menu/cr_action_menu.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/icons_lit.html.js';

import type {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import {AnchorAlignment} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';

import {SaveRequestType} from '../constants.js';

import {getCss} from './viewer_download_controls.css.js';
import {getHtml} from './viewer_download_controls.html.js';

export interface ViewerDownloadControlsElement {
  $: {
    download: CrIconButtonElement,
    menu: CrActionMenuElement,
  };
}

export class ViewerDownloadControlsElement extends CrLitElement {
  static get is() {
    return 'viewer-download-controls';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      hasEdits: {type: Boolean},
      hasEnteredAnnotationMode: {type: Boolean},
      // <if expr="enable_pdf_ink2">
      hasInk2Edits: {type: Boolean},
      // </if>
      isFormFieldFocused: {type: Boolean},

      menuOpen_: {
        type: Boolean,
        reflect: true,
      },
    };
  }

  hasEdits: boolean = false;
  hasEnteredAnnotationMode: boolean = false;
  // <if expr="enable_pdf_ink2">
  hasInk2Edits: boolean = false;
  // </if>
  isFormFieldFocused: boolean = false;
  private menuOpen_: boolean = false;
  private waitForFormFocusChange_: PromiseResolver<boolean>|null = null;

  override updated(changedProperties: PropertyValues<this>) {
    super.updated(changedProperties);

    if (changedProperties.has('isFormFieldFocused') &&
        this.waitForFormFocusChange_ !== null) {
      // Resolving the promise in updated(), since this can trigger
      // showDownloadMenu_() which accesses the element's DOM.
      this.waitForFormFocusChange_.resolve(this.hasEdits);
      this.waitForFormFocusChange_ = null;
    }
  }

  isMenuOpen(): boolean {
    return this.menuOpen_;
  }

  closeMenu() {
    this.$.menu.close();
  }

  protected onOpenChanged_(e: CustomEvent<{value: boolean}>) {
    this.menuOpen_ = e.detail.value;
  }

  private hasEditsToSave_(): boolean {
    // <if expr="enable_pdf_ink2">
    return this.hasEnteredAnnotationMode || this.hasEdits || this.hasInk2Edits;
    // </if>
    // <if expr="not enable_pdf_ink2">
    return this.hasEnteredAnnotationMode || this.hasEdits;
    // </if>
  }

  /**
   * @return The value for the aria-haspopup attribute for the download button.
   */
  protected downloadHasPopup_(): string {
    return this.hasEditsToSave_() ? 'menu' : 'false';
  }

  private showDownloadMenu_() {
    this.$.menu.showAt(this.$.download, {
      anchorAlignmentX: AnchorAlignment.CENTER,
    });
    // For tests
    this.dispatchEvent(new CustomEvent(
        'download-menu-shown-for-testing', {bubbles: true, composed: true}));
  }

  protected onDownloadClick_() {
    this.waitForEdits_().then(hasEdits => {
      if (hasEdits) {
        this.showDownloadMenu_();
      } else {
        this.dispatchSaveEvent_(SaveRequestType.ORIGINAL);
      }
    });
  }

  /**
   * @return Promise that resolves with true if the PDF has edits and/or
   *     annotations, and false otherwise.
   */
  private waitForEdits_(): Promise<boolean> {
    if (this.hasEditsToSave_()) {
      return Promise.resolve(true);
    }
    if (!this.isFormFieldFocused) {
      return Promise.resolve(false);
    }
    this.waitForFormFocusChange_ = new PromiseResolver();
    return this.waitForFormFocusChange_.promise;
  }

  private dispatchSaveEvent_(type: SaveRequestType) {
    this.dispatchEvent(
        new CustomEvent('save', {detail: type, bubbles: true, composed: true}));
  }

  protected onDownloadOriginalClick_() {
    this.dispatchSaveEvent_(SaveRequestType.ORIGINAL);
    this.$.menu.close();
  }

  protected onDownloadEditedClick_() {
    this.dispatchSaveEvent_(
        this.hasEnteredAnnotationMode ? SaveRequestType.ANNOTATION :
                                        SaveRequestType.EDITED);
    this.$.menu.close();
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'viewer-download-controls': ViewerDownloadControlsElement;
  }
}

customElements.define(
    ViewerDownloadControlsElement.is, ViewerDownloadControlsElement);