// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview An abstract superclass for TrogEdit dialog plugins. Each * Trogedit dialog has its own plugin. * * @author nicksantos@google.com (Nick Santos) * @author marcosalmeida@google.com (Marcos Almeida) */ 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.EventType'); goog.require('goog.editor.Plugin'); goog.require('goog.editor.range'); goog.require('goog.events'); goog.require('goog.ui.editor.AbstractDialog.EventType'); // *** 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) { goog.editor.Plugin.call(this); this.command_ = command; }; goog.inherits(goog.editor.plugins.AbstractDialogPlugin, goog.editor.Plugin); /** @override */ goog.editor.plugins.AbstractDialogPlugin.prototype.isSupportedCommand = function(command) { 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) { 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. * @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() { 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) { 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) { // 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.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) { this.getFieldObject().setModalMode(false); this.restoreOriginalSelection(); 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() { 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() { if (this.savedRange_) { this.savedRange_.dispose(); this.savedRange_ = null; } }; /** @override */ goog.editor.plugins.AbstractDialogPlugin.prototype.disposeInternal = function() { this.disposeDialog_(); goog.base(this, 'disposeInternal'); }; // *** Private implementation *********************************************** // /** * The command that this plugin handles. * @type {string} * @private */ goog.editor.plugins.AbstractDialogPlugin.prototype.command_; /** * The current dialog that was created and opened by this plugin. * @type {goog.ui.editor.AbstractDialog} * @private */ goog.editor.plugins.AbstractDialogPlugin.prototype.dialog_; /** * Whether this plugin should reuse the same instance of the dialog each time * execCommand is called or create a new one. * @type {boolean} * @private */ goog.editor.plugins.AbstractDialogPlugin.prototype.reuseDialog_ = false; /** * Mutex to prevent recursive calls to disposeDialog_. * @type {boolean} * @private */ goog.editor.plugins.AbstractDialogPlugin.prototype.isDisposingDialog_ = false; /** * SavedRange representing the selection before the dialog was opened. * @type {goog.dom.SavedRange} * @private */ goog.editor.plugins.AbstractDialogPlugin.prototype.savedRange_; /** * 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() { // 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; } };