toolbarcontroller.js

// 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.

// Copyright 2008 Google Inc. All Rights Reserved.

/**
 * @fileoverview A class for managing the editor toolbar.
 *
 * @see ../../demos/editor/editor.html
 */

goog.provide('goog.ui.editor.ToolbarController');

goog.require('goog.editor.Field.EventType');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.ui.Component.EventType');


/**
 * A class for managing the editor toolbar.  Acts as a bridge between
 * a {@link goog.editor.Field} and a {@link goog.ui.Toolbar}.
 *
 * The {@code toolbar} argument must be an instance of {@link goog.ui.Toolbar}
 * or a subclass.  This class doesn't care how the toolbar was created.  As
 * long as one or more controls hosted  in the toolbar have IDs that match
 * built-in {@link goog.editor.Command}s, they will function as expected.  It is
 * the caller's responsibility to ensure that the toolbar is already rendered
 * or that it decorates an existing element.
 *
 *
 * @param {!goog.editor.Field} field Editable field to be controlled by the
 *     toolbar.
 * @param {!goog.ui.Toolbar} toolbar Toolbar to control the editable field.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.ui.editor.ToolbarController = function(field, toolbar) {
  goog.events.EventTarget.call(this);

  /**
   * Event handler to listen for field events and user actions.
   * @type {!goog.events.EventHandler}
   * @private
   */
  this.handler_ = new goog.events.EventHandler(this);

  /**
   * The field instance controlled by the toolbar.
   * @type {!goog.editor.Field}
   * @private
   */
  this.field_ = field;

  /**
   * The toolbar that controls the field.
   * @type {!goog.ui.Toolbar}
   * @private
   */
  this.toolbar_ = toolbar;

  /**
   * Editing commands whose state is to be queried when updating the toolbar.
   * @type {!Array.<string>}
   * @private
   */
  this.queryCommands_ = [];

  // Iterate over all buttons, and find those which correspond to
  // queryable commands. Add them to the list of commands to query on
  // each COMMAND_VALUE_CHANGE event.
  this.toolbar_.forEachChild(function(button) {
    if (button.queryable) {
      this.queryCommands_.push(this.getComponentId(button.getId()));
    }
  }, this);

  // Make sure the toolbar doesn't steal keyboard focus.
  this.toolbar_.setFocusable(false);

  // Hook up handlers that update the toolbar in response to field events,
  // and to execute editor commands in response to toolbar events.
  this.handler_.
      listen(this.field_, goog.editor.Field.EventType.COMMAND_VALUE_CHANGE,
          this.updateToolbar).
      listen(this.toolbar_, goog.ui.Component.EventType.ACTION,
          this.handleAction);
};
goog.inherits(goog.ui.editor.ToolbarController, goog.events.EventTarget);


/**
 * Returns the Closure component ID of the control that corresponds to the
 * given {@link goog.editor.Command} constant.
 * Subclasses may override this method if they want to use a custom mapping
 * scheme from commands to controls.
 * @param {string} command Editor command.
 * @return {string} Closure component ID of the corresponding toolbar
 *     control, if any.
 * @protected
 */
goog.ui.editor.ToolbarController.prototype.getComponentId = function(command) {
  // The default implementation assumes that the component ID is the same as
  // the command constant.
  return command;
};


/**
 * Returns the {@link goog.editor.Command} constant
 * that corresponds to the given Closure component ID.  Subclasses may override
 * this method if they want to use a custom mapping scheme from controls to
 * commands.
 * @param {string} id Closure component ID of a toolbar control.
 * @return {string} Editor command or dialog constant corresponding to the
 *     toolbar control, if any.
 * @protected
 */
goog.ui.editor.ToolbarController.prototype.getCommand = function(id) {
  // The default implementation assumes that the component ID is the same as
  // the command constant.
  return id;
};


/**
 * Returns the event handler object for the editor toolbar.  Useful for classes
 * that extend {@code goog.ui.editor.ToolbarController}.
 * @return {!goog.events.EventHandler} The event handler object.
 * @protected
 */
goog.ui.editor.ToolbarController.prototype.getHandler = function() {
  return this.handler_;
};


/**
 * Returns the field instance managed by the toolbar.  Useful for
 * classes that extend {@code goog.ui.editor.ToolbarController}.
 * @return {!goog.editor.Field} The field managed by the toolbar.
 * @protected
 */
goog.ui.editor.ToolbarController.prototype.getField = function() {
  return this.field_;
};


/**
 * Returns the toolbar UI component that manages the editor.  Useful for
 * classes that extend {@code goog.ui.editor.ToolbarController}.
 * @return {!goog.ui.Toolbar} The toolbar UI component.
 */
goog.ui.editor.ToolbarController.prototype.getToolbar = function() {
  return this.toolbar_;
};


/**
 * @return {boolean} Whether the toolbar is visible.
 */
goog.ui.editor.ToolbarController.prototype.isVisible = function() {
  return this.toolbar_.isVisible();
};


/**
 * Shows or hides the toolbar.
 * @param {boolean} visible Whether to show or hide the toolbar.
 */
goog.ui.editor.ToolbarController.prototype.setVisible = function(visible) {
  this.toolbar_.setVisible(visible);
};


/**
 * @return {boolean} Whether the toolbar is enabled.
 */
goog.ui.editor.ToolbarController.prototype.isEnabled = function() {
  return this.toolbar_.isEnabled();
};


/**
 * Enables or disables the toolbar.
 * @param {boolean} enabled Whether to enable or disable the toolbar.
 */
goog.ui.editor.ToolbarController.prototype.setEnabled = function(enabled) {
  this.toolbar_.setEnabled(enabled);
};


/**
 * Programmatically blurs the editor toolbar, un-highlighting the currently
 * highlighted item, and closing the currently open menu (if any).
 */
goog.ui.editor.ToolbarController.prototype.blur = function() {
  // We can't just call this.toolbar_.getElement().blur(), because the toolbar
  // element itself isn't focusable, so goog.ui.Container#handleBlur isn't
  // registered to handle blur events.
  this.toolbar_.handleBlur(null);
};


/** @inheritDoc */
goog.ui.editor.ToolbarController.prototype.disposeInternal = function() {
  goog.ui.editor.ToolbarController.superClass_.disposeInternal.call(this);
  if (this.handler_) {
    this.handler_.dispose();
    delete this.handler_;
  }
  if (this.toolbar_) {
    this.toolbar_.dispose();
    delete this.toolbar_;
  }
  delete this.field_;
  delete this.queryCommands_;
};


/**
 * Updates the toolbar in response to editor events.  Specifically, updates
 * button states based on {@code COMMAND_VALUE_CHANGE} events, reflecting the
 * effective formatting of the selection.
 * @param {goog.events.Event} e Editor event to handle.
 * @protected
 */
goog.ui.editor.ToolbarController.prototype.updateToolbar = function(e) {
  if (!this.toolbar_.isEnabled() ||
      !this.dispatchEvent(goog.ui.Component.EventType.CHANGE)) {
    return;
  }

  var state;

  /** @preserveTry */
  try {
    /** @type {Array.<string>} */
    e.commands; // Added by dispatchEvent.

    // If the COMMAND_VALUE_CHANGE event specifies which commands changed
    // state, then we only need to update those ones, otherwise update all
    // commands.
    state = this.field_.queryCommandValue(e.commands || this.queryCommands_);
  } catch (ex) {
    // TODO: Find out when/why this happens.
    state = {};
  }

  this.updateToolbarFromState(state);
};


/**
 * Updates the toolbar to reflect a given state.
 * @param {Object} state Object mapping editor commands to values.
 */
goog.ui.editor.ToolbarController.prototype.updateToolbarFromState =
    function(state) {
  for (var command in state) {
    var button = this.toolbar_.getChild(this.getComponentId(command));
    if (button) {
      var value = state[command];
      if (button.updateFromValue) {
        button.updateFromValue(value);
      } else {
        button.setChecked(!!value);
      }
    }
  }
};


/**
 * Handles {@code ACTION} events dispatched by toolbar buttons in response to
 * user actions by executing the corresponding field command.
 * @param {goog.events.Event} e Action event to handle.
 * @protected
 */
goog.ui.editor.ToolbarController.prototype.handleAction = function(e) {
  var command = this.getCommand(e.target.getId());
  this.field_.execCommand(command, e.target.getValue());
};