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

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


/**
 * @fileoverview Code for handling edit history (undo/redo).
 */


goog.provide('goog.editor.plugins.UndoRedo');

goog.require('goog.dom');
goog.require('goog.dom.NodeOffset');
goog.require('goog.dom.Range');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.Command');
goog.require('goog.editor.Field');
goog.require('goog.editor.Plugin');
goog.require('goog.editor.node');
goog.require('goog.editor.plugins.UndoRedoManager');
goog.require('goog.editor.plugins.UndoRedoState');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.log');
goog.require('goog.object');
goog.requireType('goog.dom.AbstractRange');
goog.requireType('goog.events.Event');



/**
 * Encapsulates undo/redo logic using a custom undo stack (i.e. not browser
 * built-in). Browser built-in undo stacks are too flaky (e.g. IE's gets
 * clobbered on DOM modifications). Also, this allows interleaving non-editing
 * commands into the undo stack via the UndoRedoManager.
 *
 * @param {goog.editor.plugins.UndoRedoManager=} opt_manager An undo redo
 *    manager to be used by this plugin. If none is provided one is created.
 * @constructor
 * @extends {goog.editor.Plugin}
 */
goog.editor.plugins.UndoRedo = function(opt_manager) {
  'use strict';
  goog.editor.Plugin.call(this);

  this.setUndoRedoManager(
      opt_manager || new goog.editor.plugins.UndoRedoManager());

  // Map of goog.editor.Field hashcode to goog.events.EventHandler
  this.eventHandlers_ = {};

  this.currentStates_ = {};

  /**
   * @type {?string}
   * @private
   */
  this.initialFieldChange_ = null;

  /**
   * A copy of `goog.editor.plugins.UndoRedo.restoreState` bound to this,
   * used by undo-redo state objects to restore the state of an editable field.
   * @type {Function}
   * @see goog.editor.plugins.UndoRedo#restoreState
   * @private
   */
  this.boundRestoreState_ = goog.bind(this.restoreState, this);
};
goog.inherits(goog.editor.plugins.UndoRedo, goog.editor.Plugin);


/**
 * The logger for this class.
 * @type {goog.log.Logger}
 * @protected
 * @override
 */
goog.editor.plugins.UndoRedo.prototype.logger =
    goog.log.getLogger('goog.editor.plugins.UndoRedo');


/**
 * The `UndoState_` whose change is in progress, null if an undo or redo
 * is not in progress.
 *
 * @type {goog.editor.plugins.UndoRedo.UndoState_?}
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.inProgressUndo_ = null;


/**
 * The undo-redo stack manager used by this plugin.
 * @type {goog.editor.plugins.UndoRedoManager}
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.undoManager_;


/**
 * The key for the event listener handling state change events from the
 * undo-redo manager.
 * @type {goog.events.Key}
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.managerStateChangeKey_;


/**
 * Commands implemented by this plugin.
 * @enum {string}
 */
goog.editor.plugins.UndoRedo.COMMAND = {
  UNDO: '+undo',
  REDO: '+redo'
};


/**
 * Inverse map of execCommand strings to
 * {@link goog.editor.plugins.UndoRedo.COMMAND} constants. Used to determine
 * whether a string corresponds to a command this plugin handles in O(1) time.
 * @type {Object}
 * @private
 */
goog.editor.plugins.UndoRedo.SUPPORTED_COMMANDS_ =
    goog.object.transpose(goog.editor.plugins.UndoRedo.COMMAND);


/**
 * Set the max undo stack depth (not the real memory usage).
 * @param {number} depth Depth of the stack.
 */
goog.editor.plugins.UndoRedo.prototype.setMaxUndoDepth = function(depth) {
  'use strict';
  this.undoManager_.setMaxUndoDepth(depth);
};


/**
 * Set the undo-redo manager used by this plugin. Any state on a previous
 * undo-redo manager is lost.
 * @param {goog.editor.plugins.UndoRedoManager} manager The undo-redo manager.
 */
goog.editor.plugins.UndoRedo.prototype.setUndoRedoManager = function(manager) {
  'use strict';
  if (this.managerStateChangeKey_) {
    goog.events.unlistenByKey(this.managerStateChangeKey_);
  }

  this.undoManager_ = manager;
  this.managerStateChangeKey_ = goog.events.listen(
      this.undoManager_,
      goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE,
      this.dispatchCommandValueChange_, false, this);
};


/**
 * Whether the string corresponds to a command this plugin handles.
 * @param {string} command Command string to check.
 * @return {boolean} Whether the string corresponds to a command
 *     this plugin handles.
 * @override
 */
goog.editor.plugins.UndoRedo.prototype.isSupportedCommand = function(command) {
  'use strict';
  return command in goog.editor.plugins.UndoRedo.SUPPORTED_COMMANDS_;
};


/**
 * Unregisters and disables the fieldObject with this plugin. Thie does *not*
 * clobber the undo stack for the fieldObject though.
 * TODO(user): For the multifield version, we really should add a way to
 * ignore undo actions on field's that have been made uneditable.
 * This is probably as simple as skipping over entries in the undo stack
 * that have a hashcode of an uneditable field.
 * @param {goog.editor.Field} fieldObject The field to register with the plugin.
 * @override
 */
goog.editor.plugins.UndoRedo.prototype.unregisterFieldObject = function(
    fieldObject) {
  'use strict';
  this.disable(fieldObject);
  this.setFieldObject(null);
};


/**
 * This is so subclasses can deal with multifield undo-redo.
 * @return {goog.editor.Field} The active field object for this field. This is
 *     the one registered field object for the single-plugin case and the
 *     focused field for the multi-field plugin case.
 */
goog.editor.plugins.UndoRedo.prototype.getCurrentFieldObject = function() {
  'use strict';
  return this.getFieldObject();
};


/**
 * This is so subclasses can deal with multifield undo-redo.
 * @param {string} fieldHashCode The Field's hashcode.
 * @return {goog.editor.Field} The field object with the hashcode.
 */
goog.editor.plugins.UndoRedo.prototype.getFieldObjectForHash = function(
    fieldHashCode) {
  'use strict';
  // With single field undoredo, there's only one Field involved.
  return this.getFieldObject();
};


/**
 * This is so subclasses can deal with multifield undo-redo.
 * @return {goog.editor.Field} Target for COMMAND_VALUE_CHANGE events.
 */
goog.editor.plugins.UndoRedo.prototype.getCurrentEventTarget = function() {
  'use strict';
  return this.getFieldObject();
};


/** @override */
goog.editor.plugins.UndoRedo.prototype.enable = function(fieldObject) {
  'use strict';
  if (this.isEnabled(fieldObject)) {
    return;
  }

  // Don't want pending delayed changes from when undo-redo was disabled
  // firing after undo-redo is enabled since they might cause undo-redo stack
  // updates.
  fieldObject.clearDelayedChange();

  var eventHandler = new goog.events.EventHandler(this);

  // TODO(user): From ojan during a code review:
  // The beforechange handler is meant to be there so you can grab the cursor
  // position *before* the change is made as that's where you want the cursor to
  // be after an undo.
  //
  // It kinda looks like updateCurrentState_ doesn't do that correctly right
  // now, but it really should be fixed to do so. The cursor position stored in
  // the state should be the cursor position before any changes are made, not
  // the cursor position when the change finishes.
  //
  // It also seems like the if check below is just a bad one. We should do this
  // for browsers that use mutation events as well even though the beforechange
  // happens too late...maybe not. I don't know about this.
  if (!goog.editor.BrowserFeature.USE_MUTATION_EVENTS) {
    // We don't listen to beforechange in mutation-event browsers because
    // there we fire beforechange, then syncronously file change. The point
    // of before change is to capture before the user has changed anything.
    eventHandler.listen(
        fieldObject, goog.editor.Field.EventType.BEFORECHANGE,
        this.handleBeforeChange_);
  }
  eventHandler.listen(
      fieldObject, goog.editor.Field.EventType.DELAYEDCHANGE,
      this.handleDelayedChange_);
  eventHandler.listen(
      fieldObject, goog.editor.Field.EventType.BLUR, this.handleBlur_);

  this.eventHandlers_[fieldObject.getHashCode()] = eventHandler;

  // We want to capture the initial state of a Trogedit field before any
  // editing has happened. This is necessary so that we can undo the first
  // change to a field, even if we don't handle beforeChange.
  this.updateCurrentState_(fieldObject);
};


/** @override */
goog.editor.plugins.UndoRedo.prototype.disable = function(fieldObject) {
  'use strict';
  // Process any pending changes so we don't lose any undo-redo states that we
  // want prior to disabling undo-redo.
  fieldObject.clearDelayedChange();

  var eventHandler = this.eventHandlers_[fieldObject.getHashCode()];
  if (eventHandler) {
    eventHandler.dispose();
    delete this.eventHandlers_[fieldObject.getHashCode()];
  }

  // We delete the current state of the field on disable. When we re-enable
  // the state will be re-fetched. In most cases the content will be the same,
  // but this allows us to pick up changes while not editable. That way, when
  // undoing after starting an editable session, you can always undo to the
  // state you started in. Given this sequence of events:
  // Make editable
  // Type 'anakin'
  // Make not editable
  // Set HTML to be 'padme'
  // Make editable
  // Type 'dark side'
  // Undo
  // Without re-snapshoting current state on enable, the undo would go from
  // 'dark-side' -> 'anakin', rather than 'dark-side' -> 'padme'. You couldn't
  // undo the field to the state that existed immediately after it was made
  // editable for the second time.
  if (this.currentStates_[fieldObject.getHashCode()]) {
    delete this.currentStates_[fieldObject.getHashCode()];
  }
};


/** @override */
goog.editor.plugins.UndoRedo.prototype.isEnabled = function(fieldObject) {
  'use strict';
  // All enabled plugins have a eventHandler so reuse that map rather than
  // storing additional enabled state.
  return !!this.eventHandlers_[fieldObject.getHashCode()];
};


/** @override */
goog.editor.plugins.UndoRedo.prototype.disposeInternal = function() {
  'use strict';
  goog.editor.plugins.UndoRedo.superClass_.disposeInternal.call(this);

  for (var hashcode in this.eventHandlers_) {
    this.eventHandlers_[hashcode].dispose();
    delete this.eventHandlers_[hashcode];
  }
  this.setFieldObject(null);

  if (this.undoManager_) {
    this.undoManager_.dispose();
    delete this.undoManager_;
  }
};


/** @override */
goog.editor.plugins.UndoRedo.prototype.getTrogClassId = function() {
  'use strict';
  return 'UndoRedo';
};


/** @override */
goog.editor.plugins.UndoRedo.prototype.execCommand = function(
    command, var_args) {
  'use strict';
  if (command == goog.editor.plugins.UndoRedo.COMMAND.UNDO) {
    this.undoManager_.undo();
  } else if (command == goog.editor.plugins.UndoRedo.COMMAND.REDO) {
    this.undoManager_.redo();
  }
};


/** @override */
goog.editor.plugins.UndoRedo.prototype.queryCommandValue = function(command) {
  'use strict';
  var state = null;
  if (command == goog.editor.plugins.UndoRedo.COMMAND.UNDO) {
    state = this.undoManager_.hasUndoState();
  } else if (command == goog.editor.plugins.UndoRedo.COMMAND.REDO) {
    state = this.undoManager_.hasRedoState();
  }
  return state;
};


/**
 * Dispatches the COMMAND_VALUE_CHANGE event on the editable field or the field
 * manager, as appropriate.
 * Note: Really, people using multi field mode should be listening directly
 * to the undo-redo manager for events.
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.dispatchCommandValueChange_ =
    function() {
  'use strict';
  var eventTarget = this.getCurrentEventTarget();
  eventTarget.dispatchEvent({
    type: goog.editor.Field.EventType.COMMAND_VALUE_CHANGE,
    commands: [
      goog.editor.plugins.UndoRedo.COMMAND.REDO,
      goog.editor.plugins.UndoRedo.COMMAND.UNDO
    ]
  });
};


/**
 * Restores the state of the editable field.
 * @param {goog.editor.plugins.UndoRedo.UndoState_} state The state initiating
 *    the restore.
 * @param {string} content The content to restore.
 * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition
 *     The cursor position within the content.
 */
goog.editor.plugins.UndoRedo.prototype.restoreState = function(
    state, content, cursorPosition) {
  'use strict';
  // Fire any pending changes to get the current field state up to date and
  // then stop listening to changes while doing the undo/redo.
  var fieldObj = this.getFieldObjectForHash(state.fieldHashCode);
  if (!fieldObj) {
    return;
  }

  // Fires any pending changes, and stops the change events. Still want to
  // dispatch before change, as a change is being made and the change event
  // will be manually dispatched below after the new content has been restored
  // (also restarting change events).
  fieldObj.stopChangeEvents(true, true);

  // To prevent the situation where we stop change events and then an exception
  // happens before we can restart change events, the following code must be in
  // a try-finally block.
  try {
    fieldObj.dispatchBeforeChange();

    // Restore the state
    fieldObj.execCommand(goog.editor.Command.CLEAR_LOREM, true);

    // We specifically set the raw innerHTML of the field here as that's what
    // we get from the field when we save an undo/redo state. There's
    // no need to clean/unclean the contents in either direction.
    goog.editor.node.replaceInnerHtml(fieldObj.getElement(), content);

    if (cursorPosition) {
      cursorPosition.select();
    }

    var previousFieldObject = this.getCurrentFieldObject();
    fieldObj.focus();

    // Apps that integrate their undo-redo with Trogedit may be
    // in a state where there is no previous field object (no field focused at
    // the time of undo), so check for existence first.
    if (previousFieldObject &&
        previousFieldObject.getHashCode() != state.fieldHashCode) {
      previousFieldObject.execCommand(goog.editor.Command.UPDATE_LOREM);
    }

    // We need to update currentState_ to reflect the change.
    this.currentStates_[state.fieldHashCode].setUndoState(
        content, cursorPosition);
  } catch (e) {
    goog.log.error(this.logger, 'Error while restoring undo state', e);
  } finally {
    // Clear the delayed change event, set flag so we know not to act on it.
    this.inProgressUndo_ = state;
    // Notify the editor that we've changed (fire autosave).
    // Note that this starts up change events again, so we don't have to
    // manually do so even though we stopped change events above.
    fieldObj.dispatchChange();
    fieldObj.dispatchSelectionChangeEvent();
  }
};


/**
 * @override
 */
goog.editor.plugins.UndoRedo.prototype.handleKeyboardShortcut = function(
    e, key, isModifierPressed) {
  'use strict';
  if (isModifierPressed) {
    var command;
    if (key == 'z') {
      command = e.shiftKey ? goog.editor.plugins.UndoRedo.COMMAND.REDO :
                             goog.editor.plugins.UndoRedo.COMMAND.UNDO;
    } else if (key == 'y') {
      command = goog.editor.plugins.UndoRedo.COMMAND.REDO;
    }

    if (command) {
      // In the case where Trogedit shares its undo redo stack with another
      // application it's possible that an undo or redo will not be for an
      // goog.editor.Field. In this case we don't want to go through the
      // goog.editor.Field execCommand flow which stops and restarts events on
      // the current field. Only Trogedit UndoState's have a fieldHashCode so
      // use that to distinguish between Trogedit and other states.
      var state = command == goog.editor.plugins.UndoRedo.COMMAND.UNDO ?
          this.undoManager_.undoPeek() :
          this.undoManager_.redoPeek();
      if (state && state.fieldHashCode) {
        this.getCurrentFieldObject().execCommand(command);
      } else {
        this.execCommand(command);
      }

      return true;
    }
  }

  return false;
};


/**
 * Clear the undo/redo stack.
 */
goog.editor.plugins.UndoRedo.prototype.clearHistory = function() {
  'use strict';
  // Fire all pending change events, so that they don't come back
  // asynchronously to fill the queue.
  this.getFieldObject().stopChangeEvents(true, true);
  this.undoManager_.clearHistory();
  this.getFieldObject().startChangeEvents();
};


/**
 * Refreshes the current state of the editable field as maintained by undo-redo,
 * without adding any undo-redo states to the stack.
 * @param {goog.editor.Field} fieldObject The editable field.
 */
goog.editor.plugins.UndoRedo.prototype.refreshCurrentState = function(
    fieldObject) {
  'use strict';
  if (this.isEnabled(fieldObject)) {
    if (this.currentStates_[fieldObject.getHashCode()]) {
      delete this.currentStates_[fieldObject.getHashCode()];
    }
    this.updateCurrentState_(fieldObject);
  }
};


/**
 * Before the field changes, we want to save the state.
 * @param {goog.events.Event} e The event.
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.handleBeforeChange_ = function(e) {
  'use strict';
  if (this.inProgressUndo_) {
    // We are in between a previous undo and its delayed change event.
    // Continuing here clobbers the redo stack.
    // This does mean that if you are trying to undo/redo really quickly, it
    // will be gated by the speed of delayed change events.
    return;
  }

  var fieldObj = /** @type {goog.editor.Field} */ (e.target);
  var fieldHashCode = fieldObj.getHashCode();

  if (this.initialFieldChange_ != fieldHashCode) {
    this.initialFieldChange_ = fieldHashCode;
    this.updateCurrentState_(fieldObj);
  }
};


/**
 * After some idle time, we want to save the state.
 * @param {goog.events.Event} e The event.
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.handleDelayedChange_ = function(e) {
  'use strict';
  // This was undo making a change, don't add it BACK into the history
  if (this.inProgressUndo_) {
    // Must clear this.inProgressUndo_ before dispatching event because the
    // dispatch can cause another, queued undo that should be allowed to go
    // through.
    var state = this.inProgressUndo_;
    this.inProgressUndo_ = null;
    state.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED);
    return;
  }

  this.updateCurrentState_(/** @type {goog.editor.Field} */ (e.target));
};


/**
 * When the user blurs away, we need to save the state on that field.
 * @param {goog.events.Event} e The event.
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.handleBlur_ = function(e) {
  'use strict';
  var fieldObj = /** @type {goog.editor.Field} */ (e.target);
  if (fieldObj) {
    fieldObj.clearDelayedChange();
  }
};


/**
 * Returns the goog.editor.plugins.UndoRedo.CursorPosition_ for the current
 * selection in the given Field.
 * @param {goog.editor.Field} fieldObj The field object.
 * @return {goog.editor.plugins.UndoRedo.CursorPosition_} The CursorPosition_ or
 *    null if there is no valid selection.
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.getCursorPosition_ = function(fieldObj) {
  'use strict';
  var cursorPos = new goog.editor.plugins.UndoRedo.CursorPosition_(fieldObj);
  if (!cursorPos.isValid()) {
    return null;
  }
  return cursorPos;
};


/**
 * Helper method for saving state.
 * @param {goog.editor.Field} fieldObj The field object.
 * @private
 */
goog.editor.plugins.UndoRedo.prototype.updateCurrentState_ = function(
    fieldObj) {
  'use strict';
  var fieldHashCode = fieldObj.getHashCode();
  // We specifically grab the raw innerHTML of the field here as that's what
  // we would set on the field in the case of an undo/redo operation. There's
  // no need to clean/unclean the contents in either direction. In the case of
  // lorem ipsum being used, we want to capture the effective state (empty, no
  // cursor position) rather than capturing the lorem html.
  var content, cursorPos;
  if (fieldObj.queryCommandValue(goog.editor.Command.USING_LOREM)) {
    content = '';
    cursorPos = null;
  } else {
    content = fieldObj.getElement().innerHTML;
    cursorPos = this.getCursorPosition_(fieldObj);
  }

  var currentState = this.currentStates_[fieldHashCode];
  if (currentState) {
    // Don't create states if the content hasn't changed (spurious
    // delayed change). This can happen when lorem is cleared, for example.
    if (currentState.undoContent_ == content) {
      return;
    } else if (content == '' || currentState.undoContent_ == '') {
      // If lorem ipsum is on we say the contents are the empty string. However,
      // for an empty text shape with focus, the empty contents might not be
      // the same, depending on plugins. We want these two empty states to be
      // considered identical because to the user they are indistinguishable,
      // so we use fieldObj.getInjectableContents to map between them.
      // We cannot use getInjectableContents when first creating the undo
      // content for a field with lorem, because on enable when this is first
      // called we can't guarantee plugin registration order, so the
      // injectableContents at that time might not match the final
      // injectableContents.
      var emptyContents = fieldObj.getInjectableContents('', {});
      if (content == emptyContents && currentState.undoContent_ == '' ||
          currentState.undoContent_ == emptyContents && content == '') {
        return;
      }
    }

    currentState.setRedoState(content, cursorPos);
    this.undoManager_.addState(currentState);
  }

  this.currentStates_[fieldHashCode] =
      new goog.editor.plugins.UndoRedo.UndoState_(
          fieldHashCode, content, cursorPos, this.boundRestoreState_);
};



/**
 * This object encapsulates the state of an editable field.
 *
 * @param {string} fieldHashCode String the id of the field we're saving the
 *     content of.
 * @param {string} content String the actual text we're saving.
 * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition
 *     CursorPosLite object for the cursor position in the field.
 * @param {Function} restore The function used to restore editable field state.
 * @private
 * @constructor
 * @extends {goog.editor.plugins.UndoRedoState}
 */
goog.editor.plugins.UndoRedo.UndoState_ = function(
    fieldHashCode, content, cursorPosition, restore) {
  'use strict';
  goog.editor.plugins.UndoRedoState.call(this, true);

  /**
   * The hash code for the field whose content is being saved.
   * @type {string}
   */
  this.fieldHashCode = fieldHashCode;

  /**
   * The bound copy of `goog.editor.plugins.UndoRedo.restoreState` used by
   * this state.
   * @type {Function}
   * @private
   */
  this.restore_ = restore;

  this.setUndoState(content, cursorPosition);
};
goog.inherits(
    goog.editor.plugins.UndoRedo.UndoState_, goog.editor.plugins.UndoRedoState);


/**
 * The content to restore on undo.
 * @type {string}
 * @private
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.undoContent_;


/**
 * The cursor position to restore on undo.
 * @type {goog.editor.plugins.UndoRedo.CursorPosition_?}
 * @private
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.undoCursorPosition_;


/**
 * The content to restore on redo, undefined until the state is pushed onto the
 * undo stack.
 * @type {string|undefined}
 * @private
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.redoContent_;


/**
 * The cursor position to restore on redo, undefined until the state is pushed
 * onto the undo stack.
 * @type {goog.editor.plugins.UndoRedo.CursorPosition_|null|undefined}
 * @private
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.redoCursorPosition_;


/**
 * Get the content to restore on undo.
 * @return {string}
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.getUndoContent = function() {
  'use strict';
  return this.undoContent_;
};


/**
 * Get the content to restore on redo.
 * @return {string|undefined}
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.getRedoContent = function() {
  'use strict';
  return this.redoContent_;
};


/**
 * Performs the undo operation represented by this state.
 * @override
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.undo = function() {
  'use strict';
  this.restore_(this, this.undoContent_, this.undoCursorPosition_);
};


/**
 * Performs the redo operation represented by this state.
 * @override
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.redo = function() {
  'use strict';
  this.restore_(this, this.redoContent_, this.redoCursorPosition_);
};


/**
 * Updates the undo portion of this state. Should only be used to update the
 * current state of an editable field, which is not yet on the undo stack after
 * an undo or redo operation. You should never be modifying states on the stack!
 * @param {string} content The current content.
 * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition
 *     The current cursor position.
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.setUndoState = function(
    content, cursorPosition) {
  'use strict';
  this.undoContent_ = content;
  this.undoCursorPosition_ = cursorPosition;
};


/**
 * Adds redo information to this state. This method should be called before the
 * state is added onto the undo stack.
 *
 * @param {string} content The content to restore on a redo.
 * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition
 *     The cursor position to restore on a redo.
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.setRedoState = function(
    content, cursorPosition) {
  'use strict';
  this.redoContent_ = content;
  this.redoCursorPosition_ = cursorPosition;
};


/**
 * Checks if the *contents* of two
 * `goog.editor.plugins.UndoRedo.UndoState_`s are the same.  We don't
 * bother checking the cursor position (that's not something we'd want to save
 * anyway).
 * @param {goog.editor.plugins.UndoRedoState} rhs The state to compare.
 * @return {boolean} Whether the contents are the same.
 * @override
 */
goog.editor.plugins.UndoRedo.UndoState_.prototype.equals = function(rhs) {
  'use strict';
  return this.fieldHashCode == rhs.fieldHashCode &&
      this.undoContent_ == rhs.undoContent_ &&
      this.redoContent_ == rhs.redoContent_;
};



/**
 * Stores the state of the selection in a way the survives DOM modifications
 * that don't modify the user-interactable content (e.g. making something bold
 * vs. typing a character).
 *
 * TODO(user): Completely get rid of this and use goog.dom.SavedCaretRange.
 *
 * @param {goog.editor.Field} field The field the selection is in.
 * @private
 * @constructor
 */
goog.editor.plugins.UndoRedo.CursorPosition_ = function(field) {
  'use strict';
  this.field_ = field;

  var win = field.getEditableDomHelper().getWindow();
  var range = field.getRange();
  var isValidRange =
      !!range && range.isRangeInDocument() && range.getWindow() == win;
  range = isValidRange ? range : null;

  if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
    this.initW3C_(range);
  } else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
    this.initIE_(range);
  }
};


/**
 * The standards compliant version keeps a list of childNode offsets.
 * @param {goog.dom.AbstractRange?} range The range to save.
 * @private
 */
goog.editor.plugins.UndoRedo.CursorPosition_.prototype.initW3C_ = function(
    range) {
  'use strict';
  this.isValid_ = false;

  // TODO: Check if the range is in the field before trying to save it
  // for FF 3 contentEditable.
  if (!range) {
    return;
  }

  var anchorNode = range.getAnchorNode();
  var focusNode = range.getFocusNode();
  if (!anchorNode || !focusNode) {
    return;
  }

  var anchorOffset = range.getAnchorOffset();
  var anchor = new goog.dom.NodeOffset(anchorNode, this.field_.getElement());

  var focusOffset = range.getFocusOffset();
  var focus = new goog.dom.NodeOffset(focusNode, this.field_.getElement());

  // Test range direction.
  if (range.isReversed()) {
    this.startOffset_ = focus;
    this.startChildOffset_ = focusOffset;
    this.endOffset_ = anchor;
    this.endChildOffset_ = anchorOffset;
  } else {
    this.startOffset_ = anchor;
    this.startChildOffset_ = anchorOffset;
    this.endOffset_ = focus;
    this.endChildOffset_ = focusOffset;
  }

  this.isValid_ = true;
};


/**
 * In IE, we just keep track of the text offset (number of characters).
 * @param {goog.dom.AbstractRange?} range The range to save.
 * @private
 */
goog.editor.plugins.UndoRedo.CursorPosition_.prototype.initIE_ = function(
    range) {
  'use strict';
  this.isValid_ = false;

  if (!range) {
    return;
  }

  var ieRange = range.getTextRange(0).getBrowserRangeObject();

  if (!goog.dom.contains(this.field_.getElement(), ieRange.parentElement())) {
    return;
  }

  // Create a range that encompasses the contentEditable region to serve
  // as a reference to form ranges below.
  var contentEditableRange =
      this.field_.getEditableDomHelper().getDocument().body.createTextRange();
  contentEditableRange.moveToElementText(this.field_.getElement());

  // startMarker is a range from the start of the contentEditable node to the
  // start of the current selection.
  var startMarker = ieRange.duplicate();
  startMarker.collapse(true);
  startMarker.setEndPoint('StartToStart', contentEditableRange);
  this.startOffset_ =
      goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_(
          startMarker);

  // endMarker is a range from the start of the contentEditable node to the
  // end of the current selection.
  var endMarker = ieRange.duplicate();
  endMarker.setEndPoint('StartToStart', contentEditableRange);
  this.endOffset_ =
      goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_(
          endMarker);

  this.isValid_ = true;
};


/**
 * @return {boolean} Whether this object is valid.
 */
goog.editor.plugins.UndoRedo.CursorPosition_.prototype.isValid = function() {
  'use strict';
  return this.isValid_;
};


/**
 * @return {string} A string representation of this object.
 * @override
 */
goog.editor.plugins.UndoRedo.CursorPosition_.prototype.toString = function() {
  'use strict';
  if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
    return 'W3C:' + this.startOffset_.toString() + '\n' +
        this.startChildOffset_ + ':' + this.endOffset_.toString() + '\n' +
        this.endChildOffset_;
  }
  return 'IE:' + this.startOffset_ + ',' + this.endOffset_;
};


/**
 * Makes the browser's selection match the cursor position.
 */
goog.editor.plugins.UndoRedo.CursorPosition_.prototype.select = function() {
  'use strict';
  var range = this.getRange_(this.field_.getElement());
  if (range) {
    if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
      this.field_.getElement().focus();
    }
    goog.dom.Range.createFromBrowserRange(range).select();
  }
};


/**
 * Get the range that encompases the cursor position relative to a given base
 * node.
 * @param {Element} baseNode The node to get the cursor position relative to.
 * @return {Range|TextRange|null} The browser range for this position.
 * @private
 */
goog.editor.plugins.UndoRedo.CursorPosition_.prototype.getRange_ = function(
    baseNode) {
  'use strict';
  if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
    var startNode = this.startOffset_.findTargetNode(baseNode);
    var endNode = this.endOffset_.findTargetNode(baseNode);
    if (!startNode || !endNode) {
      return null;
    }

    // Create range.
    return /** @type {Range} */ (
        goog.dom.Range
            .createFromNodes(
                startNode, this.startChildOffset_, endNode,
                this.endChildOffset_)
            .getBrowserRangeObject());
  }

  // Create a collapsed selection at the start of the contentEditable region,
  // which the offsets were calculated relative to before.  Note that we force
  // a text range here so we can use moveToElementText.
  var sel = baseNode.ownerDocument.body.createTextRange();
  sel.moveToElementText(baseNode);
  sel.collapse(true);
  sel.moveEnd('character', this.endOffset_);
  sel.moveStart('character', this.startOffset_);
  return sel;
};


/**
 * Compute the number of characters to the end of the range in IE.
 * @param {TextRange} range The range to compute an offset for.
 * @return {number} The number of characters to the end of the range.
 * @private
 */
goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_ = function(
    range) {
  'use strict';
  var testRange = range.duplicate();

  // The number of offset characters is a little off depending on
  // what type of block elements happen to be between the start of the
  // textedit and the cursor position.  We fudge the offset until the
  // two ranges match.
  var text = range.text;
  var guess = text.length;

  testRange.collapse(true);
  testRange.moveEnd('character', guess);

  // Adjust the range until the end points match.  This doesn't quite
  // work if we're at the end of the field so we give up after a few
  // iterations.
  var diff;
  var numTries = 10;
  while (diff = testRange.compareEndPoints('EndToEnd', range)) {
    guess -= diff;
    testRange.moveEnd('character', -diff);
    --numTries;
    if (0 == numTries) {
      break;
    }
  }
  // When we set innerHTML, blank lines become a single space, causing
  // the cursor position to be off by one.  So we accommodate for blank
  // lines.
  var offset = 0;
  var pos = text.indexOf('\n\r');
  while (pos != -1) {
    ++offset;
    pos = text.indexOf('\n\r', pos + 1);
  }
  return guess + offset;
};