chromium/chrome/browser/resources/extensions/options_dialog.ts

// Copyright 2016 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_dialog/cr_dialog.js';

import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {Debouncer, PolymerElement, timeOut} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {navigation, Page} from './navigation_helper.js';
import {getTemplate} from './options_dialog.html.js';

/**
 * @return A signal that the document is ready. Need to wait for this, otherwise
 *     the custom ExtensionOptions element might not have been registered yet.
 */
function whenDocumentReady(): Promise<void> {
  if (document.readyState === 'complete') {
    return Promise.resolve();
  }

  return new Promise<void>(function(resolve) {
    document.addEventListener('readystatechange', function f() {
      if (document.readyState === 'complete') {
        document.removeEventListener('readystatechange', f);
        resolve();
      }
    });
  });
}

// The minimum width in pixels for the options dialog.
export const OptionsDialogMinWidth = 400;

// The maximum height in pixels for the options dialog.
export const OptionsDialogMaxHeight = 640;

export interface ExtensionsOptionsDialogElement {
  $: {
    body: HTMLElement,
    dialog: CrDialogElement,
  };
}

export class ExtensionsOptionsDialogElement extends PolymerElement {
  static get is() {
    return 'extensions-options-dialog';
  }

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

  static get properties() {
    return {
      extensionOptions_: Object,
      data_: Object,
    };
  }

  private extensionOptions_: any;
  private data_: chrome.developerPrivate.ExtensionInfo;
  private preferredSize_: {height: number, width: number}|null = null;
  private debouncer_: Debouncer|null = null;
  private eventTracker_: EventTracker = new EventTracker();

  get open() {
    return this.$.dialog.open;
  }

  /**
   * Resizes the dialog to the width/height stored in |preferredSize_|, taking
   * into account the window width/height.
   */
  private updateDialogSize_() {
    let headerHeight = this.$.body.offsetTop;
    if (this.$.body.assignedSlot && this.$.body.assignedSlot.parentElement) {
      headerHeight = this.$.body.assignedSlot.parentElement.offsetTop;
    }
    const maxHeight =
        Math.min(0.9 * window.innerHeight, OptionsDialogMaxHeight);
    const effectiveHeight =
        Math.min(maxHeight, headerHeight + this.preferredSize_!.height);
    const effectiveWidth =
        Math.max(OptionsDialogMinWidth, this.preferredSize_!.width);

    this.$.dialog.style.setProperty('--dialog-height', `${effectiveHeight}px`);
    this.$.dialog.style.setProperty('--dialog-width', `${effectiveWidth}px`);
    this.$.dialog.style.setProperty('--dialog-opacity', '1');
  }

  show(data: chrome.developerPrivate.ExtensionInfo) {
    this.data_ = data;
    whenDocumentReady().then(() => {
      if (!this.extensionOptions_) {
        this.extensionOptions_ = document.createElement('ExtensionOptions');
      }
      this.extensionOptions_.extension = this.data_.id;
      this.extensionOptions_.onclose = () => this.$.dialog.close();

      const boundUpdateDialogSize = this.updateDialogSize_.bind(this);
      this.extensionOptions_.onpreferredsizechanged =
          (e: {height: number, width: number}) => {
            if (!this.$.dialog.open) {
              this.$.dialog.showModal();
            }
            this.preferredSize_ = e;
            this.debouncer_ = Debouncer.debounce(
                this.debouncer_, timeOut.after(50), boundUpdateDialogSize);
          };

      // Add a 'resize' such that the dialog is resized when window size
      // changes.
      this.eventTracker_.add(window, 'resize', boundUpdateDialogSize);
      this.$.body.appendChild(this.extensionOptions_);
    });
  }

  private onClose_() {
    this.extensionOptions_.onpreferredsizechanged = null;
    this.eventTracker_.removeAll();

    const currentPage = navigation.getCurrentPage();
    // We update the page when the options dialog closes, but only if we're
    // still on the details page. We could be on a different page if the
    // user hit back while the options dialog was visible; in that case, the
    // new page is already correct.
    if (currentPage && currentPage.page === Page.DETAILS) {
      // This will update the currentPage_ and the NavigationHelper; since
      // the active page is already the details page, no main page
      // transition occurs.
      navigation.navigateTo(
          {page: Page.DETAILS, extensionId: currentPage.extensionId});
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'extensions-options-dialog': ExtensionsOptionsDialogElement;
  }
}

customElements.define(
    ExtensionsOptionsDialogElement.is, ExtensionsOptionsDialogElement);