// 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 Wrapper around {@link goog.ui.Dialog}, to provide * dialogs that are smarter about interacting with a rich text editor. * * @author nicksantos@google.com (Nick Santos) */ goog.provide('goog.ui.editor.AbstractDialog'); goog.provide('goog.ui.editor.AbstractDialog.Builder'); goog.provide('goog.ui.editor.AbstractDialog.EventType'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.classlist'); goog.require('goog.events.EventTarget'); goog.require('goog.string'); goog.require('goog.ui.Dialog'); goog.require('goog.ui.PopupBase'); // *** Public interface ***************************************************** // /** * Creates an object that represents a dialog box. * @param {goog.dom.DomHelper} domHelper DomHelper to be used to create the * dialog's dom structure. * @constructor * @extends {goog.events.EventTarget} */ goog.ui.editor.AbstractDialog = function(domHelper) { goog.ui.editor.AbstractDialog.base(this, 'constructor'); this.dom = domHelper; /** @private {?goog.ui.Dialog} */ this.dialogInternal_ = null; }; goog.inherits(goog.ui.editor.AbstractDialog, goog.events.EventTarget); /** * Causes the dialog box to appear, centered on the screen. Lazily creates the * dialog if needed. */ goog.ui.editor.AbstractDialog.prototype.show = function() { // Lazily create the wrapped dialog to be shown. if (!this.dialogInternal_) { this.dialogInternal_ = this.createDialogControl(); this.dialogInternal_.listen( goog.ui.PopupBase.EventType.HIDE, this.handleAfterHide_, false, this); } this.dialogInternal_.setVisible(true); }; /** * Hides the dialog, causing AFTER_HIDE to fire. */ goog.ui.editor.AbstractDialog.prototype.hide = function() { if (this.dialogInternal_) { // This eventually fires the wrapped dialog's AFTER_HIDE event, calling our // handleAfterHide_(). this.dialogInternal_.setVisible(false); } }; /** * @return {boolean} Whether the dialog is open. */ goog.ui.editor.AbstractDialog.prototype.isOpen = function() { return !!this.dialogInternal_ && this.dialogInternal_.isVisible(); }; /** * Runs the handler registered on the OK button event and closes the dialog if * that handler succeeds. * This is useful in cases such as double-clicking an item in the dialog is * equivalent to selecting it and clicking the default button. * @protected */ goog.ui.editor.AbstractDialog.prototype.processOkAndClose = function() { // Fake an OK event from the wrapped dialog control. var evt = new goog.ui.Dialog.Event(goog.ui.Dialog.DefaultButtonKeys.OK, null); if (this.handleOk(evt)) { // handleOk calls dispatchEvent, so if any listener calls preventDefault it // will return false and we won't hide the dialog. this.hide(); } }; // *** Dialog events ******************************************************** // /** * Event type constants for events the dialog fires. * @enum {string} */ goog.ui.editor.AbstractDialog.EventType = { // This event is fired after the dialog is hidden, no matter if it was closed // via OK or Cancel or is being disposed without being hidden first. AFTER_HIDE: 'afterhide', // Either the cancel or OK events can be canceled via preventDefault or by // returning false from their handlers to stop the dialog from closing. CANCEL: 'cancel', OK: 'ok' }; // *** Inner helper class *************************************************** // /** * A builder class for the dialog control. All methods except build return this. * @param {goog.ui.editor.AbstractDialog} editorDialog Editor dialog object * that will wrap the wrapped dialog object this builder will create. * @constructor */ goog.ui.editor.AbstractDialog.Builder = function(editorDialog) { // We require the editor dialog to be passed in so that the builder can set up // ok/cancel listeners by default, making it easier for most dialogs. this.editorDialog_ = editorDialog; this.wrappedDialog_ = new goog.ui.Dialog('', true, this.editorDialog_.dom); this.buttonSet_ = new goog.ui.Dialog.ButtonSet(this.editorDialog_.dom); this.buttonHandlers_ = {}; this.addClassName(goog.getCssName('tr-dialog')); }; /** * Sets the title of the dialog. * @param {string} title Title HTML (escaped). * @return {!goog.ui.editor.AbstractDialog.Builder} This. */ goog.ui.editor.AbstractDialog.Builder.prototype.setTitle = function(title) { this.wrappedDialog_.setTitle(title); return this; }; /** * Adds an OK button to the dialog. Clicking this button will cause {@link * handleOk} to run, subsequently dispatching an OK event. * @param {string=} opt_label The caption for the button, if not "OK". * @return {!goog.ui.editor.AbstractDialog.Builder} This. */ goog.ui.editor.AbstractDialog.Builder.prototype.addOkButton = function( opt_label) { var key = goog.ui.Dialog.DefaultButtonKeys.OK; /** @desc Label for an OK button in an editor dialog. */ var MSG_TR_DIALOG_OK = goog.getMsg('OK'); // True means this is the default/OK button. this.buttonSet_.set(key, opt_label || MSG_TR_DIALOG_OK, true); this.buttonHandlers_[key] = goog.bind(this.editorDialog_.handleOk, this.editorDialog_); return this; }; /** * Adds a Cancel button to the dialog. Clicking this button will cause {@link * handleCancel} to run, subsequently dispatching a CANCEL event. * @param {string=} opt_label The caption for the button, if not "Cancel". * @return {!goog.ui.editor.AbstractDialog.Builder} This. */ goog.ui.editor.AbstractDialog.Builder.prototype.addCancelButton = function( opt_label) { var key = goog.ui.Dialog.DefaultButtonKeys.CANCEL; /** @desc Label for a cancel button in an editor dialog. */ var MSG_TR_DIALOG_CANCEL = goog.getMsg('Cancel'); // False means it's not the OK button, true means it's the Cancel button. this.buttonSet_.set(key, opt_label || MSG_TR_DIALOG_CANCEL, false, true); this.buttonHandlers_[key] = goog.bind(this.editorDialog_.handleCancel, this.editorDialog_); return this; }; /** * Adds a custom button to the dialog. * @param {string} label The caption for the button. * @param {function(goog.ui.Dialog.EventType):*} handler Function called when * the button is clicked. It is recommended that this function be a method * in the concrete subclass of AbstractDialog using this Builder, and that * it dispatch an event (see {@link handleOk}). * @param {string=} opt_buttonId Identifier to be used to access the button when * calling AbstractDialog.getButtonElement(). * @return {!goog.ui.editor.AbstractDialog.Builder} This. */ goog.ui.editor.AbstractDialog.Builder.prototype.addButton = function( label, handler, opt_buttonId) { // We don't care what the key is, just that we can match the button with the // handler function later. var key = opt_buttonId || goog.string.createUniqueString(); this.buttonSet_.set(key, label); this.buttonHandlers_[key] = handler; return this; }; /** * Puts a CSS class on the dialog's main element. * @param {string} className The class to add. * @return {!goog.ui.editor.AbstractDialog.Builder} This. */ goog.ui.editor.AbstractDialog.Builder.prototype.addClassName = function( className) { goog.dom.classlist.add( goog.asserts.assert(this.wrappedDialog_.getDialogElement()), className); return this; }; /** * Sets the content element of the dialog. * @param {Element} contentElem An element for the main body. * @return {!goog.ui.editor.AbstractDialog.Builder} This. */ goog.ui.editor.AbstractDialog.Builder.prototype.setContent = function( contentElem) { goog.dom.appendChild(this.wrappedDialog_.getContentElement(), contentElem); return this; }; /** * Builds the wrapped dialog control. May only be called once, after which * no more methods may be called on this builder. * @return {!goog.ui.Dialog} The wrapped dialog control. */ goog.ui.editor.AbstractDialog.Builder.prototype.build = function() { if (this.buttonSet_.isEmpty()) { // If caller didn't set any buttons, add an OK and Cancel button by default. this.addOkButton(); this.addCancelButton(); } this.wrappedDialog_.setButtonSet(this.buttonSet_); var handlers = this.buttonHandlers_; this.buttonHandlers_ = null; this.wrappedDialog_.listen( goog.ui.Dialog.EventType.SELECT, // Listen for the SELECT event, which means a button was clicked, and // call the handler associated with that button via the key property. function(e) { if (handlers[e.key]) { return handlers[e.key](e); } }); // All editor dialogs are modal. this.wrappedDialog_.setModal(true); var dialog = this.wrappedDialog_; this.wrappedDialog_ = null; return dialog; }; /** * Editor dialog that will wrap the wrapped dialog this builder will create. * @type {goog.ui.editor.AbstractDialog} * @private */ goog.ui.editor.AbstractDialog.Builder.prototype.editorDialog_; /** * wrapped dialog control being built by this builder. * @type {goog.ui.Dialog} * @private */ goog.ui.editor.AbstractDialog.Builder.prototype.wrappedDialog_; /** * Set of buttons to be added to the wrapped dialog control. * @type {goog.ui.Dialog.ButtonSet} * @private */ goog.ui.editor.AbstractDialog.Builder.prototype.buttonSet_; /** * Map from keys that will be returned in the wrapped dialog SELECT events to * handler functions to be called to handle those events. * @type {Object} * @private */ goog.ui.editor.AbstractDialog.Builder.prototype.buttonHandlers_; // *** Protected interface ************************************************** // /** * The DOM helper for the parent document. * @type {goog.dom.DomHelper} * @protected */ goog.ui.editor.AbstractDialog.prototype.dom; /** * Creates and returns the goog.ui.Dialog control that is being wrapped * by this object. * @return {!goog.ui.Dialog} Created Dialog control. * @protected */ goog.ui.editor.AbstractDialog.prototype.createDialogControl = goog.abstractMethod; /** * Returns the HTML Button element for the OK button in this dialog. * @return {Element} The button element if found, else null. * @protected */ goog.ui.editor.AbstractDialog.prototype.getOkButtonElement = function() { return this.getButtonElement(goog.ui.Dialog.DefaultButtonKeys.OK); }; /** * Returns the HTML Button element for the Cancel button in this dialog. * @return {Element} The button element if found, else null. * @protected */ goog.ui.editor.AbstractDialog.prototype.getCancelButtonElement = function() { return this.getButtonElement(goog.ui.Dialog.DefaultButtonKeys.CANCEL); }; /** * Returns the HTML Button element for the button added to this dialog with * the given button id. * @param {string} buttonId The id of the button to get. * @return {Element} The button element if found, else null. * @protected */ goog.ui.editor.AbstractDialog.prototype.getButtonElement = function(buttonId) { return this.dialogInternal_.getButtonSet().getButton(buttonId); }; /** * Creates and returns the event object to be used when dispatching the OK * event to listeners, or returns null to prevent the dialog from closing. * Subclasses should override this to return their own subclass of * goog.events.Event that includes all data a plugin would need from the dialog. * @param {goog.events.Event} e The event object dispatched by the wrapped * dialog. * @return {goog.events.Event} The event object to be used when dispatching the * OK event to listeners. * @protected */ goog.ui.editor.AbstractDialog.prototype.createOkEvent = goog.abstractMethod; /** * Handles the event dispatched by the wrapped dialog control when the user * clicks the OK button. Attempts to create the OK event object and dispatches * it if successful. * @param {goog.ui.Dialog.Event} e wrapped dialog OK event object. * @return {boolean} Whether the default action (closing the dialog) should * still be executed. This will be false if the OK event could not be * created to be dispatched, or if any listener to that event returs false * or calls preventDefault. * @protected */ goog.ui.editor.AbstractDialog.prototype.handleOk = function(e) { var eventObj = this.createOkEvent(e); if (eventObj) { return this.dispatchEvent(eventObj); } else { return false; } }; /** * Handles the event dispatched by the wrapped dialog control when the user * clicks the Cancel button. Simply dispatches a CANCEL event. * @return {boolean} Returns false if any of the handlers called prefentDefault * on the event or returned false themselves. * @protected */ goog.ui.editor.AbstractDialog.prototype.handleCancel = function() { return this.dispatchEvent(goog.ui.editor.AbstractDialog.EventType.CANCEL); }; /** * Disposes of the dialog. If the dialog is open, it will be hidden and * AFTER_HIDE will be dispatched. * @override * @protected */ goog.ui.editor.AbstractDialog.prototype.disposeInternal = function() { if (this.dialogInternal_) { this.hide(); this.dialogInternal_.dispose(); this.dialogInternal_ = null; } goog.ui.editor.AbstractDialog.superClass_.disposeInternal.call(this); }; // *** Private implementation *********************************************** // /** * The wrapped dialog widget. * @type {goog.ui.Dialog} * @private */ goog.ui.editor.AbstractDialog.prototype.dialogInternal_; /** * Cleans up after the dialog is hidden and fires the AFTER_HIDE event. Should * be a listener for the wrapped dialog's AFTER_HIDE event. * @private */ goog.ui.editor.AbstractDialog.prototype.handleAfterHide_ = function() { this.dispatchEvent(goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE); };