Path: blob/trunk/third_party/closure/goog/editor/plugins/undoredo.js
2868 views
// Copyright 2005 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.131415/**16* @fileoverview Code for handling edit history (undo/redo).17*18*/192021goog.provide('goog.editor.plugins.UndoRedo');2223goog.require('goog.dom');24goog.require('goog.dom.NodeOffset');25goog.require('goog.dom.Range');26goog.require('goog.editor.BrowserFeature');27goog.require('goog.editor.Command');28goog.require('goog.editor.Field');29goog.require('goog.editor.Plugin');30goog.require('goog.editor.node');31goog.require('goog.editor.plugins.UndoRedoManager');32goog.require('goog.editor.plugins.UndoRedoState');33goog.require('goog.events');34goog.require('goog.events.EventHandler');35goog.require('goog.log');36goog.require('goog.object');37383940/**41* Encapsulates undo/redo logic using a custom undo stack (i.e. not browser42* built-in). Browser built-in undo stacks are too flaky (e.g. IE's gets43* clobbered on DOM modifications). Also, this allows interleaving non-editing44* commands into the undo stack via the UndoRedoManager.45*46* @param {goog.editor.plugins.UndoRedoManager=} opt_manager An undo redo47* manager to be used by this plugin. If none is provided one is created.48* @constructor49* @extends {goog.editor.Plugin}50*/51goog.editor.plugins.UndoRedo = function(opt_manager) {52goog.editor.Plugin.call(this);5354this.setUndoRedoManager(55opt_manager || new goog.editor.plugins.UndoRedoManager());5657// Map of goog.editor.Field hashcode to goog.events.EventHandler58this.eventHandlers_ = {};5960this.currentStates_ = {};6162/**63* @type {?string}64* @private65*/66this.initialFieldChange_ = null;6768/**69* A copy of {@code goog.editor.plugins.UndoRedo.restoreState} bound to this,70* used by undo-redo state objects to restore the state of an editable field.71* @type {Function}72* @see goog.editor.plugins.UndoRedo#restoreState73* @private74*/75this.boundRestoreState_ = goog.bind(this.restoreState, this);76};77goog.inherits(goog.editor.plugins.UndoRedo, goog.editor.Plugin);787980/**81* The logger for this class.82* @type {goog.log.Logger}83* @protected84* @override85*/86goog.editor.plugins.UndoRedo.prototype.logger =87goog.log.getLogger('goog.editor.plugins.UndoRedo');888990/**91* The {@code UndoState_} whose change is in progress, null if an undo or redo92* is not in progress.93*94* @type {goog.editor.plugins.UndoRedo.UndoState_?}95* @private96*/97goog.editor.plugins.UndoRedo.prototype.inProgressUndo_ = null;9899100/**101* The undo-redo stack manager used by this plugin.102* @type {goog.editor.plugins.UndoRedoManager}103* @private104*/105goog.editor.plugins.UndoRedo.prototype.undoManager_;106107108/**109* The key for the event listener handling state change events from the110* undo-redo manager.111* @type {goog.events.Key}112* @private113*/114goog.editor.plugins.UndoRedo.prototype.managerStateChangeKey_;115116117/**118* Commands implemented by this plugin.119* @enum {string}120*/121goog.editor.plugins.UndoRedo.COMMAND = {122UNDO: '+undo',123REDO: '+redo'124};125126127/**128* Inverse map of execCommand strings to129* {@link goog.editor.plugins.UndoRedo.COMMAND} constants. Used to determine130* whether a string corresponds to a command this plugin handles in O(1) time.131* @type {Object}132* @private133*/134goog.editor.plugins.UndoRedo.SUPPORTED_COMMANDS_ =135goog.object.transpose(goog.editor.plugins.UndoRedo.COMMAND);136137138/**139* Set the max undo stack depth (not the real memory usage).140* @param {number} depth Depth of the stack.141*/142goog.editor.plugins.UndoRedo.prototype.setMaxUndoDepth = function(depth) {143this.undoManager_.setMaxUndoDepth(depth);144};145146147/**148* Set the undo-redo manager used by this plugin. Any state on a previous149* undo-redo manager is lost.150* @param {goog.editor.plugins.UndoRedoManager} manager The undo-redo manager.151*/152goog.editor.plugins.UndoRedo.prototype.setUndoRedoManager = function(manager) {153if (this.managerStateChangeKey_) {154goog.events.unlistenByKey(this.managerStateChangeKey_);155}156157this.undoManager_ = manager;158this.managerStateChangeKey_ = goog.events.listen(159this.undoManager_,160goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE,161this.dispatchCommandValueChange_, false, this);162};163164165/**166* Whether the string corresponds to a command this plugin handles.167* @param {string} command Command string to check.168* @return {boolean} Whether the string corresponds to a command169* this plugin handles.170* @override171*/172goog.editor.plugins.UndoRedo.prototype.isSupportedCommand = function(command) {173return command in goog.editor.plugins.UndoRedo.SUPPORTED_COMMANDS_;174};175176177/**178* Unregisters and disables the fieldObject with this plugin. Thie does *not*179* clobber the undo stack for the fieldObject though.180* TODO(user): For the multifield version, we really should add a way to181* ignore undo actions on field's that have been made uneditable.182* This is probably as simple as skipping over entries in the undo stack183* that have a hashcode of an uneditable field.184* @param {goog.editor.Field} fieldObject The field to register with the plugin.185* @override186*/187goog.editor.plugins.UndoRedo.prototype.unregisterFieldObject = function(188fieldObject) {189this.disable(fieldObject);190this.setFieldObject(null);191};192193194/**195* This is so subclasses can deal with multifield undo-redo.196* @return {goog.editor.Field} The active field object for this field. This is197* the one registered field object for the single-plugin case and the198* focused field for the multi-field plugin case.199*/200goog.editor.plugins.UndoRedo.prototype.getCurrentFieldObject = function() {201return this.getFieldObject();202};203204205/**206* This is so subclasses can deal with multifield undo-redo.207* @param {string} fieldHashCode The Field's hashcode.208* @return {goog.editor.Field} The field object with the hashcode.209*/210goog.editor.plugins.UndoRedo.prototype.getFieldObjectForHash = function(211fieldHashCode) {212// With single field undoredo, there's only one Field involved.213return this.getFieldObject();214};215216217/**218* This is so subclasses can deal with multifield undo-redo.219* @return {goog.editor.Field} Target for COMMAND_VALUE_CHANGE events.220*/221goog.editor.plugins.UndoRedo.prototype.getCurrentEventTarget = function() {222return this.getFieldObject();223};224225226/** @override */227goog.editor.plugins.UndoRedo.prototype.enable = function(fieldObject) {228if (this.isEnabled(fieldObject)) {229return;230}231232// Don't want pending delayed changes from when undo-redo was disabled233// firing after undo-redo is enabled since they might cause undo-redo stack234// updates.235fieldObject.clearDelayedChange();236237var eventHandler = new goog.events.EventHandler(this);238239// TODO(user): From ojan during a code review:240// The beforechange handler is meant to be there so you can grab the cursor241// position *before* the change is made as that's where you want the cursor to242// be after an undo.243//244// It kinda looks like updateCurrentState_ doesn't do that correctly right245// now, but it really should be fixed to do so. The cursor position stored in246// the state should be the cursor position before any changes are made, not247// the cursor position when the change finishes.248//249// It also seems like the if check below is just a bad one. We should do this250// for browsers that use mutation events as well even though the beforechange251// happens too late...maybe not. I don't know about this.252if (!goog.editor.BrowserFeature.USE_MUTATION_EVENTS) {253// We don't listen to beforechange in mutation-event browsers because254// there we fire beforechange, then syncronously file change. The point255// of before change is to capture before the user has changed anything.256eventHandler.listen(257fieldObject, goog.editor.Field.EventType.BEFORECHANGE,258this.handleBeforeChange_);259}260eventHandler.listen(261fieldObject, goog.editor.Field.EventType.DELAYEDCHANGE,262this.handleDelayedChange_);263eventHandler.listen(264fieldObject, goog.editor.Field.EventType.BLUR, this.handleBlur_);265266this.eventHandlers_[fieldObject.getHashCode()] = eventHandler;267268// We want to capture the initial state of a Trogedit field before any269// editing has happened. This is necessary so that we can undo the first270// change to a field, even if we don't handle beforeChange.271this.updateCurrentState_(fieldObject);272};273274275/** @override */276goog.editor.plugins.UndoRedo.prototype.disable = function(fieldObject) {277// Process any pending changes so we don't lose any undo-redo states that we278// want prior to disabling undo-redo.279fieldObject.clearDelayedChange();280281var eventHandler = this.eventHandlers_[fieldObject.getHashCode()];282if (eventHandler) {283eventHandler.dispose();284delete this.eventHandlers_[fieldObject.getHashCode()];285}286287// We delete the current state of the field on disable. When we re-enable288// the state will be re-fetched. In most cases the content will be the same,289// but this allows us to pick up changes while not editable. That way, when290// undoing after starting an editable session, you can always undo to the291// state you started in. Given this sequence of events:292// Make editable293// Type 'anakin'294// Make not editable295// Set HTML to be 'padme'296// Make editable297// Type 'dark side'298// Undo299// Without re-snapshoting current state on enable, the undo would go from300// 'dark-side' -> 'anakin', rather than 'dark-side' -> 'padme'. You couldn't301// undo the field to the state that existed immediately after it was made302// editable for the second time.303if (this.currentStates_[fieldObject.getHashCode()]) {304delete this.currentStates_[fieldObject.getHashCode()];305}306};307308309/** @override */310goog.editor.plugins.UndoRedo.prototype.isEnabled = function(fieldObject) {311// All enabled plugins have a eventHandler so reuse that map rather than312// storing additional enabled state.313return !!this.eventHandlers_[fieldObject.getHashCode()];314};315316317/** @override */318goog.editor.plugins.UndoRedo.prototype.disposeInternal = function() {319goog.editor.plugins.UndoRedo.superClass_.disposeInternal.call(this);320321for (var hashcode in this.eventHandlers_) {322this.eventHandlers_[hashcode].dispose();323delete this.eventHandlers_[hashcode];324}325this.setFieldObject(null);326327if (this.undoManager_) {328this.undoManager_.dispose();329delete this.undoManager_;330}331};332333334/** @override */335goog.editor.plugins.UndoRedo.prototype.getTrogClassId = function() {336return 'UndoRedo';337};338339340/** @override */341goog.editor.plugins.UndoRedo.prototype.execCommand = function(342command, var_args) {343if (command == goog.editor.plugins.UndoRedo.COMMAND.UNDO) {344this.undoManager_.undo();345} else if (command == goog.editor.plugins.UndoRedo.COMMAND.REDO) {346this.undoManager_.redo();347}348};349350351/** @override */352goog.editor.plugins.UndoRedo.prototype.queryCommandValue = function(command) {353var state = null;354if (command == goog.editor.plugins.UndoRedo.COMMAND.UNDO) {355state = this.undoManager_.hasUndoState();356} else if (command == goog.editor.plugins.UndoRedo.COMMAND.REDO) {357state = this.undoManager_.hasRedoState();358}359return state;360};361362363/**364* Dispatches the COMMAND_VALUE_CHANGE event on the editable field or the field365* manager, as appropriate.366* Note: Really, people using multi field mode should be listening directly367* to the undo-redo manager for events.368* @private369*/370goog.editor.plugins.UndoRedo.prototype.dispatchCommandValueChange_ =371function() {372var eventTarget = this.getCurrentEventTarget();373eventTarget.dispatchEvent({374type: goog.editor.Field.EventType.COMMAND_VALUE_CHANGE,375commands: [376goog.editor.plugins.UndoRedo.COMMAND.REDO,377goog.editor.plugins.UndoRedo.COMMAND.UNDO378]379});380};381382383/**384* Restores the state of the editable field.385* @param {goog.editor.plugins.UndoRedo.UndoState_} state The state initiating386* the restore.387* @param {string} content The content to restore.388* @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition389* The cursor position within the content.390*/391goog.editor.plugins.UndoRedo.prototype.restoreState = function(392state, content, cursorPosition) {393// Fire any pending changes to get the current field state up to date and394// then stop listening to changes while doing the undo/redo.395var fieldObj = this.getFieldObjectForHash(state.fieldHashCode);396if (!fieldObj) {397return;398}399400// Fires any pending changes, and stops the change events. Still want to401// dispatch before change, as a change is being made and the change event402// will be manually dispatched below after the new content has been restored403// (also restarting change events).404fieldObj.stopChangeEvents(true, true);405406// To prevent the situation where we stop change events and then an exception407// happens before we can restart change events, the following code must be in408// a try-finally block.409try {410fieldObj.dispatchBeforeChange();411412// Restore the state413fieldObj.execCommand(goog.editor.Command.CLEAR_LOREM, true);414415// We specifically set the raw innerHTML of the field here as that's what416// we get from the field when we save an undo/redo state. There's417// no need to clean/unclean the contents in either direction.418goog.editor.node.replaceInnerHtml(fieldObj.getElement(), content);419420if (cursorPosition) {421cursorPosition.select();422}423424var previousFieldObject = this.getCurrentFieldObject();425fieldObj.focus();426427// Apps that integrate their undo-redo with Trogedit may be428// in a state where there is no previous field object (no field focused at429// the time of undo), so check for existence first.430if (previousFieldObject &&431previousFieldObject.getHashCode() != state.fieldHashCode) {432previousFieldObject.execCommand(goog.editor.Command.UPDATE_LOREM);433}434435// We need to update currentState_ to reflect the change.436this.currentStates_[state.fieldHashCode].setUndoState(437content, cursorPosition);438} catch (e) {439goog.log.error(this.logger, 'Error while restoring undo state', e);440} finally {441// Clear the delayed change event, set flag so we know not to act on it.442this.inProgressUndo_ = state;443// Notify the editor that we've changed (fire autosave).444// Note that this starts up change events again, so we don't have to445// manually do so even though we stopped change events above.446fieldObj.dispatchChange();447fieldObj.dispatchSelectionChangeEvent();448}449};450451452/**453* @override454*/455goog.editor.plugins.UndoRedo.prototype.handleKeyboardShortcut = function(456e, key, isModifierPressed) {457if (isModifierPressed) {458var command;459if (key == 'z') {460command = e.shiftKey ? goog.editor.plugins.UndoRedo.COMMAND.REDO :461goog.editor.plugins.UndoRedo.COMMAND.UNDO;462} else if (key == 'y') {463command = goog.editor.plugins.UndoRedo.COMMAND.REDO;464}465466if (command) {467// In the case where Trogedit shares its undo redo stack with another468// application it's possible that an undo or redo will not be for an469// goog.editor.Field. In this case we don't want to go through the470// goog.editor.Field execCommand flow which stops and restarts events on471// the current field. Only Trogedit UndoState's have a fieldHashCode so472// use that to distinguish between Trogedit and other states.473var state = command == goog.editor.plugins.UndoRedo.COMMAND.UNDO ?474this.undoManager_.undoPeek() :475this.undoManager_.redoPeek();476if (state && state.fieldHashCode) {477this.getCurrentFieldObject().execCommand(command);478} else {479this.execCommand(command);480}481482return true;483}484}485486return false;487};488489490/**491* Clear the undo/redo stack.492*/493goog.editor.plugins.UndoRedo.prototype.clearHistory = function() {494// Fire all pending change events, so that they don't come back495// asynchronously to fill the queue.496this.getFieldObject().stopChangeEvents(true, true);497this.undoManager_.clearHistory();498this.getFieldObject().startChangeEvents();499};500501502/**503* Refreshes the current state of the editable field as maintained by undo-redo,504* without adding any undo-redo states to the stack.505* @param {goog.editor.Field} fieldObject The editable field.506*/507goog.editor.plugins.UndoRedo.prototype.refreshCurrentState = function(508fieldObject) {509if (this.isEnabled(fieldObject)) {510if (this.currentStates_[fieldObject.getHashCode()]) {511delete this.currentStates_[fieldObject.getHashCode()];512}513this.updateCurrentState_(fieldObject);514}515};516517518/**519* Before the field changes, we want to save the state.520* @param {goog.events.Event} e The event.521* @private522*/523goog.editor.plugins.UndoRedo.prototype.handleBeforeChange_ = function(e) {524if (this.inProgressUndo_) {525// We are in between a previous undo and its delayed change event.526// Continuing here clobbers the redo stack.527// This does mean that if you are trying to undo/redo really quickly, it528// will be gated by the speed of delayed change events.529return;530}531532var fieldObj = /** @type {goog.editor.Field} */ (e.target);533var fieldHashCode = fieldObj.getHashCode();534535if (this.initialFieldChange_ != fieldHashCode) {536this.initialFieldChange_ = fieldHashCode;537this.updateCurrentState_(fieldObj);538}539};540541542/**543* After some idle time, we want to save the state.544* @param {goog.events.Event} e The event.545* @private546*/547goog.editor.plugins.UndoRedo.prototype.handleDelayedChange_ = function(e) {548// This was undo making a change, don't add it BACK into the history549if (this.inProgressUndo_) {550// Must clear this.inProgressUndo_ before dispatching event because the551// dispatch can cause another, queued undo that should be allowed to go552// through.553var state = this.inProgressUndo_;554this.inProgressUndo_ = null;555state.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED);556return;557}558559this.updateCurrentState_(/** @type {goog.editor.Field} */ (e.target));560};561562563/**564* When the user blurs away, we need to save the state on that field.565* @param {goog.events.Event} e The event.566* @private567*/568goog.editor.plugins.UndoRedo.prototype.handleBlur_ = function(e) {569var fieldObj = /** @type {goog.editor.Field} */ (e.target);570if (fieldObj) {571fieldObj.clearDelayedChange();572}573};574575576/**577* Returns the goog.editor.plugins.UndoRedo.CursorPosition_ for the current578* selection in the given Field.579* @param {goog.editor.Field} fieldObj The field object.580* @return {goog.editor.plugins.UndoRedo.CursorPosition_} The CursorPosition_ or581* null if there is no valid selection.582* @private583*/584goog.editor.plugins.UndoRedo.prototype.getCursorPosition_ = function(fieldObj) {585var cursorPos = new goog.editor.plugins.UndoRedo.CursorPosition_(fieldObj);586if (!cursorPos.isValid()) {587return null;588}589return cursorPos;590};591592593/**594* Helper method for saving state.595* @param {goog.editor.Field} fieldObj The field object.596* @private597*/598goog.editor.plugins.UndoRedo.prototype.updateCurrentState_ = function(599fieldObj) {600var fieldHashCode = fieldObj.getHashCode();601// We specifically grab the raw innerHTML of the field here as that's what602// we would set on the field in the case of an undo/redo operation. There's603// no need to clean/unclean the contents in either direction. In the case of604// lorem ipsum being used, we want to capture the effective state (empty, no605// cursor position) rather than capturing the lorem html.606var content, cursorPos;607if (fieldObj.queryCommandValue(goog.editor.Command.USING_LOREM)) {608content = '';609cursorPos = null;610} else {611content = fieldObj.getElement().innerHTML;612cursorPos = this.getCursorPosition_(fieldObj);613}614615var currentState = this.currentStates_[fieldHashCode];616if (currentState) {617// Don't create states if the content hasn't changed (spurious618// delayed change). This can happen when lorem is cleared, for example.619if (currentState.undoContent_ == content) {620return;621} else if (content == '' || currentState.undoContent_ == '') {622// If lorem ipsum is on we say the contents are the empty string. However,623// for an empty text shape with focus, the empty contents might not be624// the same, depending on plugins. We want these two empty states to be625// considered identical because to the user they are indistinguishable,626// so we use fieldObj.getInjectableContents to map between them.627// We cannot use getInjectableContents when first creating the undo628// content for a field with lorem, because on enable when this is first629// called we can't guarantee plugin registration order, so the630// injectableContents at that time might not match the final631// injectableContents.632var emptyContents = fieldObj.getInjectableContents('', {});633if (content == emptyContents && currentState.undoContent_ == '' ||634currentState.undoContent_ == emptyContents && content == '') {635return;636}637}638639currentState.setRedoState(content, cursorPos);640this.undoManager_.addState(currentState);641}642643this.currentStates_[fieldHashCode] =644new goog.editor.plugins.UndoRedo.UndoState_(645fieldHashCode, content, cursorPos, this.boundRestoreState_);646};647648649650/**651* This object encapsulates the state of an editable field.652*653* @param {string} fieldHashCode String the id of the field we're saving the654* content of.655* @param {string} content String the actual text we're saving.656* @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition657* CursorPosLite object for the cursor position in the field.658* @param {Function} restore The function used to restore editable field state.659* @private660* @constructor661* @extends {goog.editor.plugins.UndoRedoState}662*/663goog.editor.plugins.UndoRedo.UndoState_ = function(664fieldHashCode, content, cursorPosition, restore) {665goog.editor.plugins.UndoRedoState.call(this, true);666667/**668* The hash code for the field whose content is being saved.669* @type {string}670*/671this.fieldHashCode = fieldHashCode;672673/**674* The bound copy of {@code goog.editor.plugins.UndoRedo.restoreState} used by675* this state.676* @type {Function}677* @private678*/679this.restore_ = restore;680681this.setUndoState(content, cursorPosition);682};683goog.inherits(684goog.editor.plugins.UndoRedo.UndoState_, goog.editor.plugins.UndoRedoState);685686687/**688* The content to restore on undo.689* @type {string}690* @private691*/692goog.editor.plugins.UndoRedo.UndoState_.prototype.undoContent_;693694695/**696* The cursor position to restore on undo.697* @type {goog.editor.plugins.UndoRedo.CursorPosition_?}698* @private699*/700goog.editor.plugins.UndoRedo.UndoState_.prototype.undoCursorPosition_;701702703/**704* The content to restore on redo, undefined until the state is pushed onto the705* undo stack.706* @type {string|undefined}707* @private708*/709goog.editor.plugins.UndoRedo.UndoState_.prototype.redoContent_;710711712/**713* The cursor position to restore on redo, undefined until the state is pushed714* onto the undo stack.715* @type {goog.editor.plugins.UndoRedo.CursorPosition_|null|undefined}716* @private717*/718goog.editor.plugins.UndoRedo.UndoState_.prototype.redoCursorPosition_;719720721/**722* Performs the undo operation represented by this state.723* @override724*/725goog.editor.plugins.UndoRedo.UndoState_.prototype.undo = function() {726this.restore_(this, this.undoContent_, this.undoCursorPosition_);727};728729730/**731* Performs the redo operation represented by this state.732* @override733*/734goog.editor.plugins.UndoRedo.UndoState_.prototype.redo = function() {735this.restore_(this, this.redoContent_, this.redoCursorPosition_);736};737738739/**740* Updates the undo portion of this state. Should only be used to update the741* current state of an editable field, which is not yet on the undo stack after742* an undo or redo operation. You should never be modifying states on the stack!743* @param {string} content The current content.744* @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition745* The current cursor position.746*/747goog.editor.plugins.UndoRedo.UndoState_.prototype.setUndoState = function(748content, cursorPosition) {749this.undoContent_ = content;750this.undoCursorPosition_ = cursorPosition;751};752753754/**755* Adds redo information to this state. This method should be called before the756* state is added onto the undo stack.757*758* @param {string} content The content to restore on a redo.759* @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition760* The cursor position to restore on a redo.761*/762goog.editor.plugins.UndoRedo.UndoState_.prototype.setRedoState = function(763content, cursorPosition) {764this.redoContent_ = content;765this.redoCursorPosition_ = cursorPosition;766};767768769/**770* Checks if the *contents* of two771* {@code goog.editor.plugins.UndoRedo.UndoState_}s are the same. We don't772* bother checking the cursor position (that's not something we'd want to save773* anyway).774* @param {goog.editor.plugins.UndoRedoState} rhs The state to compare.775* @return {boolean} Whether the contents are the same.776* @override777*/778goog.editor.plugins.UndoRedo.UndoState_.prototype.equals = function(rhs) {779return this.fieldHashCode == rhs.fieldHashCode &&780this.undoContent_ == rhs.undoContent_ &&781this.redoContent_ == rhs.redoContent_;782};783784785786/**787* Stores the state of the selection in a way the survives DOM modifications788* that don't modify the user-interactable content (e.g. making something bold789* vs. typing a character).790*791* TODO(user): Completely get rid of this and use goog.dom.SavedCaretRange.792*793* @param {goog.editor.Field} field The field the selection is in.794* @private795* @constructor796*/797goog.editor.plugins.UndoRedo.CursorPosition_ = function(field) {798this.field_ = field;799800var win = field.getEditableDomHelper().getWindow();801var range = field.getRange();802var isValidRange =803!!range && range.isRangeInDocument() && range.getWindow() == win;804range = isValidRange ? range : null;805806if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {807this.initW3C_(range);808} else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {809this.initIE_(range);810}811};812813814/**815* The standards compliant version keeps a list of childNode offsets.816* @param {goog.dom.AbstractRange?} range The range to save.817* @private818*/819goog.editor.plugins.UndoRedo.CursorPosition_.prototype.initW3C_ = function(820range) {821this.isValid_ = false;822823// TODO: Check if the range is in the field before trying to save it824// for FF 3 contentEditable.825if (!range) {826return;827}828829var anchorNode = range.getAnchorNode();830var focusNode = range.getFocusNode();831if (!anchorNode || !focusNode) {832return;833}834835var anchorOffset = range.getAnchorOffset();836var anchor = new goog.dom.NodeOffset(anchorNode, this.field_.getElement());837838var focusOffset = range.getFocusOffset();839var focus = new goog.dom.NodeOffset(focusNode, this.field_.getElement());840841// Test range direction.842if (range.isReversed()) {843this.startOffset_ = focus;844this.startChildOffset_ = focusOffset;845this.endOffset_ = anchor;846this.endChildOffset_ = anchorOffset;847} else {848this.startOffset_ = anchor;849this.startChildOffset_ = anchorOffset;850this.endOffset_ = focus;851this.endChildOffset_ = focusOffset;852}853854this.isValid_ = true;855};856857858/**859* In IE, we just keep track of the text offset (number of characters).860* @param {goog.dom.AbstractRange?} range The range to save.861* @private862*/863goog.editor.plugins.UndoRedo.CursorPosition_.prototype.initIE_ = function(864range) {865this.isValid_ = false;866867if (!range) {868return;869}870871var ieRange = range.getTextRange(0).getBrowserRangeObject();872873if (!goog.dom.contains(this.field_.getElement(), ieRange.parentElement())) {874return;875}876877// Create a range that encompasses the contentEditable region to serve878// as a reference to form ranges below.879var contentEditableRange =880this.field_.getEditableDomHelper().getDocument().body.createTextRange();881contentEditableRange.moveToElementText(this.field_.getElement());882883// startMarker is a range from the start of the contentEditable node to the884// start of the current selection.885var startMarker = ieRange.duplicate();886startMarker.collapse(true);887startMarker.setEndPoint('StartToStart', contentEditableRange);888this.startOffset_ =889goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_(890startMarker);891892// endMarker is a range from the start of the contentEditable node to the893// end of the current selection.894var endMarker = ieRange.duplicate();895endMarker.setEndPoint('StartToStart', contentEditableRange);896this.endOffset_ =897goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_(898endMarker);899900this.isValid_ = true;901};902903904/**905* @return {boolean} Whether this object is valid.906*/907goog.editor.plugins.UndoRedo.CursorPosition_.prototype.isValid = function() {908return this.isValid_;909};910911912/**913* @return {string} A string representation of this object.914* @override915*/916goog.editor.plugins.UndoRedo.CursorPosition_.prototype.toString = function() {917if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {918return 'W3C:' + this.startOffset_.toString() + '\n' +919this.startChildOffset_ + ':' + this.endOffset_.toString() + '\n' +920this.endChildOffset_;921}922return 'IE:' + this.startOffset_ + ',' + this.endOffset_;923};924925926/**927* Makes the browser's selection match the cursor position.928*/929goog.editor.plugins.UndoRedo.CursorPosition_.prototype.select = function() {930var range = this.getRange_(this.field_.getElement());931if (range) {932if (goog.editor.BrowserFeature.HAS_IE_RANGES) {933this.field_.getElement().focus();934}935goog.dom.Range.createFromBrowserRange(range).select();936}937};938939940/**941* Get the range that encompases the the cursor position relative to a given942* base node.943* @param {Element} baseNode The node to get the cursor position relative to.944* @return {Range|TextRange|null} The browser range for this position.945* @private946*/947goog.editor.plugins.UndoRedo.CursorPosition_.prototype.getRange_ = function(948baseNode) {949if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {950var startNode = this.startOffset_.findTargetNode(baseNode);951var endNode = this.endOffset_.findTargetNode(baseNode);952if (!startNode || !endNode) {953return null;954}955956// Create range.957return /** @type {Range} */ (958goog.dom.Range959.createFromNodes(960startNode, this.startChildOffset_, endNode,961this.endChildOffset_)962.getBrowserRangeObject());963}964965// Create a collapsed selection at the start of the contentEditable region,966// which the offsets were calculated relative to before. Note that we force967// a text range here so we can use moveToElementText.968var sel = baseNode.ownerDocument.body.createTextRange();969sel.moveToElementText(baseNode);970sel.collapse(true);971sel.moveEnd('character', this.endOffset_);972sel.moveStart('character', this.startOffset_);973return sel;974};975976977/**978* Compute the number of characters to the end of the range in IE.979* @param {TextRange} range The range to compute an offset for.980* @return {number} The number of characters to the end of the range.981* @private982*/983goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_ = function(984range) {985var testRange = range.duplicate();986987// The number of offset characters is a little off depending on988// what type of block elements happen to be between the start of the989// textedit and the cursor position. We fudge the offset until the990// two ranges match.991var text = range.text;992var guess = text.length;993994testRange.collapse(true);995testRange.moveEnd('character', guess);996997// Adjust the range until the end points match. This doesn't quite998// work if we're at the end of the field so we give up after a few999// iterations.1000var diff;1001var numTries = 10;1002while (diff = testRange.compareEndPoints('EndToEnd', range)) {1003guess -= diff;1004testRange.moveEnd('character', -diff);1005--numTries;1006if (0 == numTries) {1007break;1008}1009}1010// When we set innerHTML, blank lines become a single space, causing1011// the cursor position to be off by one. So we accommodate for blank1012// lines.1013var offset = 0;1014var pos = text.indexOf('\n\r');1015while (pos != -1) {1016++offset;1017pos = text.indexOf('\n\r', pos + 1);1018}1019return guess + offset;1020};102110221023