chromium/chrome/browser/resources/print_preview/ui/provisional_destination_resolver.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_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/cr_elements/cr_hidden_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import './print_preview_shared.css.js';
import './print_preview_vars.css.js';
import '../strings.m.js';
import './throbber.css.js';

import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

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

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

/**
 * @fileoverview PrintPreviewProvisionalDestinationResolver
 * This class is a dialog for resolving provisional destinations. Provisional
 * destinations are extension controlled destinations that need access to a USB
 * device and have not yet been granted access by the user. Destinations are
 * resolved when the user confirms they wish to grant access and the handler
 * has successfully granted access.
 */

/**
 * States that the provisional destination resolver can be in.
 */
enum ResolverState {
  INITIAL = 'INITIAL',
  ACTIVE = 'ACTIVE',
  GRANTING_PERMISSION = 'GRANTING_PERMISSION',
  ERROR = 'ERROR',
  DONE = 'DONE'
}

export interface PrintPreviewProvisionalDestinationResolverElement {
  $: {
    dialog: CrDialogElement,
  };
}

const PrintPreviewProvisionalDestinationResolverElementBase =
    I18nMixin(PolymerElement);

export class PrintPreviewProvisionalDestinationResolverElement extends
    PrintPreviewProvisionalDestinationResolverElementBase {
  static get is() {
    return 'print-preview-provisional-destination-resolver';
  }

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

  static get properties() {
    return {
      destinationStore: Object,

      destination_: {
        type: Object,
        value: null,
      },

      state_: {
        type: String,
        value: ResolverState.INITIAL,
      },
    };
  }

  destinationStore: DestinationStore;
  private destination_: Destination|null;
  private state_: ResolverState;
  private promiseResolver_: PromiseResolver<Destination>|null = null;

  override ready() {
    super.ready();
    this.addEventListener('keydown', (e: KeyboardEvent) => this.onKeydown_(e));
  }

  /**
   * @param destination The destination this dialog is needed to resolve.
   * @return Promise that is resolved when the destination has been resolved.
   */
  resolveDestination(destination: Destination): Promise<Destination> {
    this.state_ = ResolverState.ACTIVE;
    this.destination_ = destination;
    this.$.dialog.showModal();
    const icon = this.shadowRoot!.querySelector<HTMLElement>('.extension-icon');
    assert(icon);
    icon.style.backgroundImage = 'image-set(' +
        'url(chrome://extension-icon/' + this.destination_!.extensionId +
        '/24/1) 1x,' +
        'url(chrome://extension-icon/' + this.destination_!.extensionId +
        '/48/1) 2x)';
    this.promiseResolver_ = new PromiseResolver();
    return this.promiseResolver_!.promise;
  }

  /**
   * Handler for click on OK button. It attempts to resolve the destination.
   * If successful, promiseResolver_.promise is resolved with the
   * resolved destination and the dialog closes.
   */
  private startResolveDestination_() {
    assert(
        this.state_ === ResolverState.ACTIVE,
        'Invalid state in request grant permission');

    this.state_ = ResolverState.GRANTING_PERMISSION;
    const destination = this.destination_!;
    this.destinationStore.resolveProvisionalDestination(destination)
        .then((resolvedDestination: Destination|null) => {
          if (this.state_ !== ResolverState.GRANTING_PERMISSION) {
            return;
          }

          if (destination.id !== this.destination_!.id) {
            return;
          }

          if (resolvedDestination) {
            this.state_ = ResolverState.DONE;
            this.promiseResolver_!.resolve(resolvedDestination!);
            this.promiseResolver_ = null;
            this.$.dialog.close();
          } else {
            this.state_ = ResolverState.ERROR;
          }
        });
  }

  private onKeydown_(e: KeyboardEvent) {
    e.stopPropagation();
    if (e.key === 'Escape') {
      this.$.dialog.cancel();
      e.preventDefault();
    }
  }

  private onCancelClick_() {
    this.$.dialog.cancel();
  }

  private onCancel_() {
    this.promiseResolver_!.reject();
    this.state_ = ResolverState.INITIAL;
  }

  /**
   * @return The USB permission message to display.
   */
  private getPermissionMessage_(): string {
    return this.state_ === ResolverState.ERROR ?
        this.i18n(
            'resolveExtensionUSBErrorMessage',
            this.destination_!.extensionName) :
        this.i18n('resolveExtensionUSBPermissionMessage');
  }

  /**
   * @return Whether the resolver is in the ERROR state.
   */
  private isInErrorState_(): boolean {
    return this.state_ === ResolverState.ERROR;
  }

  /**
   * @return Whether the resolver is in the ACTIVE state.
   */
  private isInActiveState_(): boolean {
    return this.state_ === ResolverState.ACTIVE;
  }

  /**
   * @return 'throbber' if the resolver is in the GRANTING_PERMISSION state,
   *     empty otherwise.
   */
  private getThrobberClass_(): string {
    return this.state_ === ResolverState.GRANTING_PERMISSION ? 'throbber' : '';
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'print-preview-provisional-destination-resolver':
        PrintPreviewProvisionalDestinationResolverElement;
  }
}

customElements.define(
    PrintPreviewProvisionalDestinationResolverElement.is,
    PrintPreviewProvisionalDestinationResolverElement);