chromium/third_party/google-closure-library/closure/goog/editor/plugins/abstractdialogplugin.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview An abstract superclass for TrogEdit dialog plugins. Each
 * Trogedit dialog has its own plugin.
 */

goog.provide('goog.editor.plugins.AbstractDialogPlugin');
goog.provide('goog.editor.plugins.AbstractDialogPlugin.EventType');

goog.require('goog.dom');
goog.require('goog.dom.Range');
goog.require('goog.editor.Field');
goog.require('goog.editor.Plugin');
goog.require('goog.editor.range');
goog.require('goog.events');
goog.require('goog.ui.editor.AbstractDialog');
goog.requireType('goog.dom.SavedRange');
goog.requireType('goog.events.Event');


// *** Public interface ***************************************************** //



/**
 * An abstract superclass for a Trogedit plugin that creates exactly one
 * dialog. By default dialogs are not reused -- each time execCommand is called,
 * a new instance of the dialog object is created (and the old one disposed of).
 * To enable reusing of the dialog object, subclasses should call
 * setReuseDialog() after calling the superclass constructor.
 * @param {string} command The command that this plugin handles.
 * @constructor
 * @extends {goog.editor.Plugin}
 */
goog.editor.plugins.AbstractDialogPlugin = function(command) {
  'use strict';
  goog.editor.plugins.AbstractDialogPlugin.base(this, 'constructor');

  /**
   * The command that this plugin handles.
   * @private {string}
   */
  this.command_ = command;

  /** @private {function()} */
  this.restoreScrollPosition_ = function() {};

  /**
   * The current dialog that was created and opened by this plugin.
   * @private {?goog.ui.editor.AbstractDialog}
   */
  this.dialog_ = null;

  /**
   * Whether this plugin should reuse the same instance of the dialog each time
   * execCommand is called or create a new one.
   * @private {boolean}
   */
  this.reuseDialog_ = false;

  /**
   * Mutex to prevent recursive calls to disposeDialog_.
   * @private {boolean}
   */
  this.isDisposingDialog_ = false;

  /**
   * SavedRange representing the selection before the dialog was opened.
   * @private {?goog.dom.SavedRange}
   */
  this.savedRange_ = null;
};
goog.inherits(goog.editor.plugins.AbstractDialogPlugin, goog.editor.Plugin);


/** @override */
goog.editor.plugins.AbstractDialogPlugin.prototype.isSupportedCommand =
    function(command) {
  'use strict';
  return command == this.command_;
};


/**
 * Handles execCommand. Dialog plugins don't make any changes when they open a
 * dialog, just when the dialog closes (because only modal dialogs are
 * supported). Hence this method does not dispatch the change events that the
 * superclass method does.
 * @param {string} command The command to execute.
 * @param {...*} var_args Any additional parameters needed to
 *     execute the command.
 * @return {*} The result of the execCommand, if any.
 * @override
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.execCommand = function(
    command, var_args) {
  'use strict';
  return this.execCommandInternal.apply(this, arguments);
};


// *** Events *************************************************************** //


/**
 * Event type constants for events the dialog plugins fire.
 * @enum {string}
 */
goog.editor.plugins.AbstractDialogPlugin.EventType = {
  // This event is fired when a dialog has been opened.
  OPENED: 'dialogOpened',
  // This event is fired when a dialog has been closed.
  CLOSED: 'dialogClosed'
};


// *** Protected interface ************************************************** //


/**
 * Creates a new instance of this plugin's dialog. Must be overridden by
 * subclasses.
 * Implementations should expect that the editor is inactive and cannot be
 * focused, nor will its caret position (or selection) be determinable until
 * after the dialogs goog.ui.PopupBase.EventType.HIDE event has been handled.
 * @param {!goog.dom.DomHelper} dialogDomHelper The dom helper to be used to
 *     create the dialog.
 * @param {*=} opt_arg The dialog specific argument. Concrete subclasses should
 *     declare a specific type.
 * @return {goog.ui.editor.AbstractDialog} The newly created dialog.
 * @protected
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.createDialog =
    goog.abstractMethod;


/**
 * Returns the current dialog that was created and opened by this plugin.
 * @return {goog.ui.editor.AbstractDialog} The current dialog that was created
 *     and opened by this plugin.
 * @protected
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.getDialog = function() {
  'use strict';
  return this.dialog_;
};


/**
 * Sets whether this plugin should reuse the same instance of the dialog each
 * time execCommand is called or create a new one. This is intended for use by
 * subclasses only, hence protected.
 * @param {boolean} reuse Whether to reuse the dialog.
 * @protected
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.setReuseDialog = function(
    reuse) {
  'use strict';
  this.reuseDialog_ = reuse;
};


/**
 * Handles execCommand by opening the dialog. Dispatches
 * {@link goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED} after the
 * dialog is shown.
 * @param {string} command The command to execute.
 * @param {*=} opt_arg The dialog specific argument. Should be the same as
 *     {@link createDialog}.
 * @return {*} Always returns true, indicating the dialog was shown.
 * @protected
 * @override
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.execCommandInternal =
    function(command, opt_arg) {
  'use strict';
  // If this plugin should not reuse dialog instances, first dispose of the
  // previous dialog.
  if (!this.reuseDialog_) {
    this.disposeDialog_();
  }
  // If there is no dialog yet (or we aren't reusing the previous one), create
  // one.
  if (!this.dialog_) {
    this.dialog_ = this.createDialog(
        // TODO(user): Add Field.getAppDomHelper. (Note dom helper will
        // need to be updated if setAppWindow is called by clients.)
        goog.dom.getDomHelper(this.getFieldObject().getAppWindow()), opt_arg);
  }

  // Since we're opening a dialog, we need to clear the selection because the
  // focus will be going to the dialog, and if we leave an selection in the
  // editor while another selection is active in the dialog as the user is
  // typing, some browsers will screw up the original selection. But first we
  // save it so we can restore it when the dialog closes.
  // getRange may return null if there is no selection in the field.
  var tempRange = this.getFieldObject().getRange();
  // saveUsingDom() did not work as well as saveUsingNormalizedCarets(),
  // not sure why.

  this.restoreScrollPosition_ = this.saveScrollPosition();
  this.savedRange_ =
      tempRange && goog.editor.range.saveUsingNormalizedCarets(tempRange);
  goog.dom.Range.clearSelection(
      this.getFieldObject().getEditableDomHelper().getWindow());

  // Listen for the dialog closing so we can clean up.
  goog.events.listenOnce(
      this.dialog_, goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE,
      this.handleAfterHide, false, this);

  this.getFieldObject().setModalMode(true);
  this.dialog_.show();
  this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED);

  // Since the selection has left the document, dispatch a selection
  // change event.
  this.getFieldObject().dispatchSelectionChangeEvent();

  return true;
};


/**
 * Cleans up after the dialog has closed, including restoring the selection to
 * what it was before the dialog was opened. If a subclass modifies the editable
 * field's content such that the original selection is no longer valid (usually
 * the case when the user clicks OK, and sometimes also on Cancel), it is that
 * subclass' responsibility to place the selection in the desired place during
 * the OK or Cancel (or other) handler. In that case, this method will leave the
 * selection in place.
 * @param {goog.events.Event} e The AFTER_HIDE event object.
 * @protected
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.handleAfterHide = function(
    e) {
  'use strict';
  this.getFieldObject().setModalMode(false);
  this.restoreOriginalSelection();
  this.restoreScrollPosition_();

  if (!this.reuseDialog_) {
    this.disposeDialog_();
  }

  this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED);

  // Since the selection has returned to the document, dispatch a selection
  // change event.
  this.getFieldObject().dispatchSelectionChangeEvent();

  // When the dialog closes due to pressing enter or escape, that happens on the
  // keydown event. But the browser will still fire a keyup event after that,
  // which is caught by the editable field and causes it to try to fire a
  // selection change event. To avoid that, we "debounce" the selection change
  // event, meaning the editable field will not fire that event if the keyup
  // that caused it immediately after this dialog was hidden ("immediately"
  // means a small number of milliseconds defined by the editable field).
  this.getFieldObject().debounceEvent(
      goog.editor.Field.EventType.SELECTIONCHANGE);
};


/**
 * Restores the selection in the editable field to what it was before the dialog
 * was opened. This is not guaranteed to work if the contents of the field
 * have changed.
 * @protected
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.restoreOriginalSelection =
    function() {
  'use strict';
  this.getFieldObject().restoreSavedRange(this.savedRange_);
  this.savedRange_ = null;
};


/**
 * Cleans up the structure used to save the original selection before the dialog
 * was opened. Should be used by subclasses that don't restore the original
 * selection via restoreOriginalSelection.
 * @protected
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.disposeOriginalSelection =
    function() {
  'use strict';
  if (this.savedRange_) {
    this.savedRange_.dispose();
    this.savedRange_ = null;
  }
};


/** @override */
goog.editor.plugins.AbstractDialogPlugin.prototype.disposeInternal =
    function() {
  'use strict';
  this.disposeDialog_();
  goog.editor.plugins.AbstractDialogPlugin.base(this, 'disposeInternal');
};


// *** Private implementation *********************************************** //


/**
 * Disposes of the dialog if needed. It is this abstract class' responsibility
 * to dispose of the dialog. The "if needed" refers to the fact this method
 * might be called twice (nested calls, not sequential) in the dispose flow, so
 * if the dialog was already disposed once it should not be disposed again.
 * @private
 */
goog.editor.plugins.AbstractDialogPlugin.prototype.disposeDialog_ = function() {
  'use strict';
  // Wrap disposing the dialog in a mutex. Otherwise disposing it would cause it
  // to get hidden (if it is still open) and fire AFTER_HIDE, which in
  // turn would cause the dialog to be disposed again (closure only flags an
  // object as disposed after the dispose call chain completes, so it doesn't
  // prevent recursive dispose calls).
  if (this.dialog_ && !this.isDisposingDialog_) {
    this.isDisposingDialog_ = true;
    this.dialog_.dispose();
    this.dialog_ = null;
    this.isDisposingDialog_ = false;
  }
};