http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/field.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/field.js b/externs/GCL/externs/goog/editor/field.js new file mode 100644 index 0000000..349aca8 --- /dev/null +++ b/externs/GCL/externs/goog/editor/field.js @@ -0,0 +1,2750 @@ +// Copyright 2006 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. +// All Rights Reserved. + +/** + * @fileoverview Class to encapsulate an editable field. Always uses an + * iframe to contain the editable area, never inherits the style of the + * surrounding page, and is always a fixed height. + * + * @author [email protected] (Nick Santos) + * @see ../demos/editor/editor.html + * @see ../demos/editor/field_basic.html + */ + +goog.provide('goog.editor.Field'); +goog.provide('goog.editor.Field.EventType'); + +goog.require('goog.a11y.aria'); +goog.require('goog.a11y.aria.Role'); +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.async.Delay'); +goog.require('goog.dom'); +goog.require('goog.dom.Range'); +goog.require('goog.dom.TagName'); +goog.require('goog.editor.BrowserFeature'); +goog.require('goog.editor.Command'); +goog.require('goog.editor.Plugin'); +goog.require('goog.editor.icontent'); +goog.require('goog.editor.icontent.FieldFormatInfo'); +goog.require('goog.editor.icontent.FieldStyleInfo'); +goog.require('goog.editor.node'); +goog.require('goog.editor.range'); +goog.require('goog.events'); +goog.require('goog.events.EventHandler'); +goog.require('goog.events.EventTarget'); +goog.require('goog.events.EventType'); +goog.require('goog.events.KeyCodes'); +goog.require('goog.functions'); +goog.require('goog.log'); +goog.require('goog.log.Level'); +goog.require('goog.string'); +goog.require('goog.string.Unicode'); +goog.require('goog.style'); +goog.require('goog.userAgent'); +goog.require('goog.userAgent.product'); + + + +/** + * This class encapsulates an editable field. + * + * event: load Fires when the field is loaded + * event: unload Fires when the field is unloaded (made not editable) + * + * event: beforechange Fires before the content of the field might change + * + * event: delayedchange Fires a short time after field has changed. If multiple + * change events happen really close to each other only + * the last one will trigger the delayedchange event. + * + * event: beforefocus Fires before the field becomes active + * event: focus Fires when the field becomes active. Fires after the blur event + * event: blur Fires when the field becomes inactive + * + * TODO: figure out if blur or beforefocus fires first in IE and make FF match + * + * @param {string} id An identifer for the field. This is used to find the + * field and the element associated with this field. + * @param {Document=} opt_doc The document that the element with the given + * id can be found in. If not provided, the default document is used. + * @constructor + * @extends {goog.events.EventTarget} + */ +goog.editor.Field = function(id, opt_doc) { + goog.events.EventTarget.call(this); + + /** + * The id for this editable field, which must match the id of the element + * associated with this field. + * @type {string} + */ + this.id = id; + + /** + * The hash code for this field. Should be equal to the id. + * @type {string} + * @private + */ + this.hashCode_ = id; + + /** + * Dom helper for the editable node. + * @type {goog.dom.DomHelper} + * @protected + */ + this.editableDomHelper = null; + + /** + * Map of class id to registered plugin. + * @type {Object} + * @private + */ + this.plugins_ = {}; + + + /** + * Plugins registered on this field, indexed by the goog.editor.Plugin.Op + * that they support. + * @type {Object<Array<goog.editor.Plugin>>} + * @private + */ + this.indexedPlugins_ = {}; + + for (var op in goog.editor.Plugin.OPCODE) { + this.indexedPlugins_[op] = []; + } + + + /** + * Additional styles to install for the editable field. + * @type {string} + * @protected + */ + this.cssStyles = ''; + + // The field will not listen to change events until it has finished loading + this.stoppedEvents_ = {}; + this.stopEvent(goog.editor.Field.EventType.CHANGE); + this.stopEvent(goog.editor.Field.EventType.DELAYEDCHANGE); + this.isModified_ = false; + this.isEverModified_ = false; + this.delayedChangeTimer_ = new goog.async.Delay(this.dispatchDelayedChange_, + goog.editor.Field.DELAYED_CHANGE_FREQUENCY, this); + + this.debouncedEvents_ = {}; + for (var key in goog.editor.Field.EventType) { + this.debouncedEvents_[goog.editor.Field.EventType[key]] = 0; + } + + if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) { + this.changeTimerGecko_ = new goog.async.Delay(this.handleChange, + goog.editor.Field.CHANGE_FREQUENCY, this); + } + + /** + * @type {goog.events.EventHandler<!goog.editor.Field>} + * @protected + */ + this.eventRegister = new goog.events.EventHandler(this); + + // Wrappers around this field, to be disposed when the field is disposed. + this.wrappers_ = []; + + this.loadState_ = goog.editor.Field.LoadState_.UNEDITABLE; + + var doc = opt_doc || document; + + /** + * @type {!goog.dom.DomHelper} + * @protected + */ + this.originalDomHelper = goog.dom.getDomHelper(doc); + + /** + * @type {Element} + * @protected + */ + this.originalElement = this.originalDomHelper.getElement(this.id); + + /** + * @private {boolean} + */ + this.followLinkInNewWindow_ = + goog.editor.BrowserFeature.FOLLOWS_EDITABLE_LINKS; + + // Default to the same window as the field is in. + this.appWindow_ = this.originalDomHelper.getWindow(); +}; +goog.inherits(goog.editor.Field, goog.events.EventTarget); + + +/** + * The editable dom node. + * @type {Element} + * TODO(user): Make this private! + */ +goog.editor.Field.prototype.field = null; + + +/** + * The original node that is being made editable, or null if it has + * not yet been found. + * @type {Element} + * @protected + */ +goog.editor.Field.prototype.originalElement = null; + + +/** + * Logging object. + * @type {goog.log.Logger} + * @protected + */ +goog.editor.Field.prototype.logger = + goog.log.getLogger('goog.editor.Field'); + + +/** + * Event types that can be stopped/started. + * @enum {string} + */ +goog.editor.Field.EventType = { + /** + * Dispatched when the command state of the selection may have changed. This + * event should be listened to for updating toolbar state. + */ + COMMAND_VALUE_CHANGE: 'cvc', + /** + * Dispatched when the field is loaded and ready to use. + */ + LOAD: 'load', + /** + * Dispatched when the field is fully unloaded and uneditable. + */ + UNLOAD: 'unload', + /** + * Dispatched before the field contents are changed. + */ + BEFORECHANGE: 'beforechange', + /** + * Dispatched when the field contents change, in FF only. + * Used for internal resizing, please do not use. + */ + CHANGE: 'change', + /** + * Dispatched on a slight delay after changes are made. + * Use for autosave, or other times your app needs to know + * that the field contents changed. + */ + DELAYEDCHANGE: 'delayedchange', + /** + * Dispatched before focus in moved into the field. + */ + BEFOREFOCUS: 'beforefocus', + /** + * Dispatched when focus is moved into the field. + */ + FOCUS: 'focus', + /** + * Dispatched when the field is blurred. + */ + BLUR: 'blur', + /** + * Dispatched before tab is handled by the field. This is a legacy way + * of controlling tab behavior. Use trog.plugins.AbstractTabHandler now. + */ + BEFORETAB: 'beforetab', + /** + * Dispatched after the iframe containing the field is resized, so that UI + * components which contain it can respond. + */ + IFRAME_RESIZED: 'ifrsz', + /** + * Dispatched when the selection changes. + * Use handleSelectionChange from plugin API instead of listening + * directly to this event. + */ + SELECTIONCHANGE: 'selectionchange' +}; + + +/** + * The load state of the field. + * @enum {number} + * @private + */ +goog.editor.Field.LoadState_ = { + UNEDITABLE: 0, + LOADING: 1, + EDITABLE: 2 +}; + + +/** + * The amount of time that a debounce blocks an event. + * TODO(nicksantos): As of 9/30/07, this is only used for blocking + * a keyup event after a keydown. We might need to tweak this for other + * types of events. Maybe have a per-event debounce time? + * @type {number} + * @private + */ +goog.editor.Field.DEBOUNCE_TIME_MS_ = 500; + + +/** + * There is at most one "active" field at a time. By "active" field, we mean + * a field that has focus and is being used. + * @type {?string} + * @private + */ +goog.editor.Field.activeFieldId_ = null; + + +/** + * Whether this field is in "modal interaction" mode. This usually + * means that it's being edited by a dialog. + * @type {boolean} + * @private + */ +goog.editor.Field.prototype.inModalMode_ = false; + + +/** + * The window where dialogs and bubbles should be rendered. + * @type {!Window} + * @private + */ +goog.editor.Field.prototype.appWindow_; + + +/** + * The dom helper for the node to be made editable. + * @type {goog.dom.DomHelper} + * @protected + */ +goog.editor.Field.prototype.originalDomHelper; + + +/** + * Target node to be used when dispatching SELECTIONCHANGE asynchronously on + * mouseup (to avoid IE quirk). Should be set just before starting the timer and + * nulled right after consuming. + * @type {Node} + * @private + */ +goog.editor.Field.prototype.selectionChangeTarget_; + + +/** + * Flag controlling wether to capture mouse up events on the window or not. + * @type {boolean} + * @private + */ +goog.editor.Field.prototype.useWindowMouseUp_ = false; + + +/** + * FLag indicating the handling of a mouse event sequence. + * @type {boolean} + * @private + */ +goog.editor.Field.prototype.waitingForMouseUp_ = false; + + +/** + * Sets the active field id. + * @param {?string} fieldId The active field id. + */ +goog.editor.Field.setActiveFieldId = function(fieldId) { + goog.editor.Field.activeFieldId_ = fieldId; +}; + + +/** + * @return {?string} The id of the active field. + */ +goog.editor.Field.getActiveFieldId = function() { + return goog.editor.Field.activeFieldId_; +}; + + +/** + * Sets flag to control whether to use window mouse up after seeing + * a mouse down operation on the field. + * @param {boolean} flag True to track window mouse up. + */ +goog.editor.Field.prototype.setUseWindowMouseUp = function(flag) { + goog.asserts.assert(!flag || !this.usesIframe(), + 'procssing window mouse up should only be enabled when not using iframe'); + this.useWindowMouseUp_ = flag; +}; + + +/** + * @return {boolean} Whether we're in modal interaction mode. When this + * returns true, another plugin is interacting with the field contents + * in a synchronous way, and expects you not to make changes to + * the field's DOM structure or selection. + */ +goog.editor.Field.prototype.inModalMode = function() { + return this.inModalMode_; +}; + + +/** + * @param {boolean} inModalMode Sets whether we're in modal interaction mode. + */ +goog.editor.Field.prototype.setModalMode = function(inModalMode) { + this.inModalMode_ = inModalMode; +}; + + +/** + * Returns a string usable as a hash code for this field. For field's + * that were created with an id, the hash code is guaranteed to be the id. + * TODO(user): I think we can get rid of this. Seems only used from editor. + * @return {string} The hash code for this editable field. + */ +goog.editor.Field.prototype.getHashCode = function() { + return this.hashCode_; +}; + + +/** + * Returns the editable DOM element or null if this field + * is not editable. + * <p>On IE or Safari this is the element with contentEditable=true + * (in whitebox mode, the iFrame body). + * <p>On Gecko this is the iFrame body + * TODO(user): How do we word this for subclass version? + * @return {Element} The editable DOM element, defined as above. + */ +goog.editor.Field.prototype.getElement = function() { + return this.field; +}; + + +/** + * Returns original DOM element that is being made editable by Trogedit or + * null if that element has not yet been found in the appropriate document. + * @return {Element} The original element. + */ +goog.editor.Field.prototype.getOriginalElement = function() { + return this.originalElement; +}; + + +/** + * Registers a keyboard event listener on the field. This is necessary for + * Gecko since the fields are contained in an iFrame and there is no way to + * auto-propagate key events up to the main window. + * @param {string|Array<string>} type Event type to listen for or array of + * event types, for example goog.events.EventType.KEYDOWN. + * @param {Function} listener Function to be used as the listener. + * @param {boolean=} opt_capture Whether to use capture phase (optional, + * defaults to false). + * @param {Object=} opt_handler Object in whose scope to call the listener. + */ +goog.editor.Field.prototype.addListener = function(type, listener, opt_capture, + opt_handler) { + var elem = this.getElement(); + // On Gecko, keyboard events only reliably fire on the document element when + // using an iframe. + if (goog.editor.BrowserFeature.USE_DOCUMENT_FOR_KEY_EVENTS && elem && + this.usesIframe()) { + elem = elem.ownerDocument; + } + if (opt_handler) { + this.eventRegister.listenWithScope( + elem, type, listener, opt_capture, opt_handler); + } else { + this.eventRegister.listen(elem, type, listener, opt_capture); + } +}; + + +/** + * Returns the registered plugin with the given classId. + * @param {string} classId classId of the plugin. + * @return {goog.editor.Plugin} Registered plugin with the given classId. + */ +goog.editor.Field.prototype.getPluginByClassId = function(classId) { + return this.plugins_[classId]; +}; + + +/** + * Registers the plugin with the editable field. + * @param {goog.editor.Plugin} plugin The plugin to register. + */ +goog.editor.Field.prototype.registerPlugin = function(plugin) { + var classId = plugin.getTrogClassId(); + if (this.plugins_[classId]) { + goog.log.error(this.logger, + 'Cannot register the same class of plugin twice.'); + } + this.plugins_[classId] = plugin; + + // Only key events and execute should have these has* functions with a custom + // handler array since they need to be very careful about performance. + // The rest of the plugin hooks should be event-based. + for (var op in goog.editor.Plugin.OPCODE) { + var opcode = goog.editor.Plugin.OPCODE[op]; + if (plugin[opcode]) { + this.indexedPlugins_[op].push(plugin); + } + } + plugin.registerFieldObject(this); + + // By default we enable all plugins for fields that are currently loaded. + if (this.isLoaded()) { + plugin.enable(this); + } +}; + + +/** + * Unregisters the plugin with this field. + * @param {goog.editor.Plugin} plugin The plugin to unregister. + */ +goog.editor.Field.prototype.unregisterPlugin = function(plugin) { + var classId = plugin.getTrogClassId(); + if (!this.plugins_[classId]) { + goog.log.error(this.logger, + 'Cannot unregister a plugin that isn\'t registered.'); + } + delete this.plugins_[classId]; + + for (var op in goog.editor.Plugin.OPCODE) { + var opcode = goog.editor.Plugin.OPCODE[op]; + if (plugin[opcode]) { + goog.array.remove(this.indexedPlugins_[op], plugin); + } + } + + plugin.unregisterFieldObject(this); +}; + + +/** + * Sets the value that will replace the style attribute of this field's + * element when the field is made non-editable. This method is called with the + * current value of the style attribute when the field is made editable. + * @param {string} cssText The value of the style attribute. + */ +goog.editor.Field.prototype.setInitialStyle = function(cssText) { + this.cssText = cssText; +}; + + +/** + * Reset the properties on the original field element to how it was before + * it was made editable. + */ +goog.editor.Field.prototype.resetOriginalElemProperties = function() { + var field = this.getOriginalElement(); + field.removeAttribute('contentEditable'); + field.removeAttribute('g_editable'); + field.removeAttribute('role'); + + if (!this.id) { + field.removeAttribute('id'); + } else { + field.id = this.id; + } + + field.className = this.savedClassName_ || ''; + + var cssText = this.cssText; + if (!cssText) { + field.removeAttribute('style'); + } else { + goog.dom.setProperties(field, {'style' : cssText}); + } + + if (goog.isString(this.originalFieldLineHeight_)) { + goog.style.setStyle(field, 'lineHeight', this.originalFieldLineHeight_); + this.originalFieldLineHeight_ = null; + } +}; + + +/** + * Checks the modified state of the field. + * Note: Changes that take place while the goog.editor.Field.EventType.CHANGE + * event is stopped do not effect the modified state. + * @param {boolean=} opt_useIsEverModified Set to true to check if the field + * has ever been modified since it was created, otherwise checks if the field + * has been modified since the last goog.editor.Field.EventType.DELAYEDCHANGE + * event was dispatched. + * @return {boolean} Whether the field has been modified. + */ +goog.editor.Field.prototype.isModified = function(opt_useIsEverModified) { + return opt_useIsEverModified ? this.isEverModified_ : this.isModified_; +}; + + +/** + * Number of milliseconds after a change when the change event should be fired. + * @type {number} + */ +goog.editor.Field.CHANGE_FREQUENCY = 15; + + +/** + * Number of milliseconds between delayed change events. + * @type {number} + */ +goog.editor.Field.DELAYED_CHANGE_FREQUENCY = 250; + + +/** + * @return {boolean} Whether the field is implemented as an iframe. + */ +goog.editor.Field.prototype.usesIframe = goog.functions.TRUE; + + +/** + * @return {boolean} Whether the field should be rendered with a fixed + * height, or should expand to fit its contents. + */ +goog.editor.Field.prototype.isFixedHeight = goog.functions.TRUE; + + +/** + * @return {boolean} Whether the field should be refocused on input. + * This is a workaround for the iOS bug that text input doesn't work + * when the main window listens touch events. + */ +goog.editor.Field.prototype.shouldRefocusOnInputMobileSafari = + goog.functions.FALSE; + + +/** + * Map of keyCodes (not charCodes) that cause changes in the field contents. + * @type {Object} + * @private + */ +goog.editor.Field.KEYS_CAUSING_CHANGES_ = { + 46: true, // DEL + 8: true // BACKSPACE +}; + +if (!goog.userAgent.IE) { + // Only IE doesn't change the field by default upon tab. + // TODO(user): This really isn't right now that we have tab plugins. + goog.editor.Field.KEYS_CAUSING_CHANGES_[9] = true; // TAB +} + + +/** + * Map of keyCodes (not charCodes) that when used in conjunction with the + * Ctrl key cause changes in the field contents. These are the keys that are + * not handled by basic formatting trogedit plugins. + * @type {Object} + * @private + */ +goog.editor.Field.CTRL_KEYS_CAUSING_CHANGES_ = { + 86: true, // V + 88: true // X +}; + +if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) { + // In IE and Webkit, input from IME (Input Method Editor) does not generate a + // keypress event so we have to rely on the keydown event. This way we have + // false positives while the user is using keyboard to select the + // character to input, but it is still better than the false negatives + // that ignores user's final input at all. + goog.editor.Field.KEYS_CAUSING_CHANGES_[229] = true; // from IME; +} + + +/** + * Returns true if the keypress generates a change in contents. + * @param {goog.events.BrowserEvent} e The event. + * @param {boolean} testAllKeys True to test for all types of generating keys. + * False to test for only the keys found in + * goog.editor.Field.KEYS_CAUSING_CHANGES_. + * @return {boolean} Whether the keypress generates a change in contents. + * @private + */ +goog.editor.Field.isGeneratingKey_ = function(e, testAllKeys) { + if (goog.editor.Field.isSpecialGeneratingKey_(e)) { + return true; + } + + return !!(testAllKeys && !(e.ctrlKey || e.metaKey) && + (!goog.userAgent.GECKO || e.charCode)); +}; + + +/** + * Returns true if the keypress generates a change in the contents. + * due to a special key listed in goog.editor.Field.KEYS_CAUSING_CHANGES_ + * @param {goog.events.BrowserEvent} e The event. + * @return {boolean} Whether the keypress generated a change in the contents. + * @private + */ +goog.editor.Field.isSpecialGeneratingKey_ = function(e) { + var testCtrlKeys = (e.ctrlKey || e.metaKey) && + e.keyCode in goog.editor.Field.CTRL_KEYS_CAUSING_CHANGES_; + var testRegularKeys = !(e.ctrlKey || e.metaKey) && + e.keyCode in goog.editor.Field.KEYS_CAUSING_CHANGES_; + + return testCtrlKeys || testRegularKeys; +}; + + +/** + * Sets the application window. + * @param {!Window} appWindow The window where dialogs and bubbles should be + * rendered. + */ +goog.editor.Field.prototype.setAppWindow = function(appWindow) { + this.appWindow_ = appWindow; +}; + + +/** + * Returns the "application" window, where dialogs and bubbles + * should be rendered. + * @return {!Window} The window. + */ +goog.editor.Field.prototype.getAppWindow = function() { + return this.appWindow_; +}; + + +/** + * Sets the zIndex that the field should be based off of. + * TODO(user): Get rid of this completely. Here for Sites. + * Should this be set directly on UI plugins? + * + * @param {number} zindex The base zIndex of the editor. + */ +goog.editor.Field.prototype.setBaseZindex = function(zindex) { + this.baseZindex_ = zindex; +}; + + +/** + * Returns the zindex of the base level of the field. + * + * @return {number} The base zindex of the editor. + */ +goog.editor.Field.prototype.getBaseZindex = function() { + return this.baseZindex_ || 0; +}; + + +/** + * Sets up the field object and window util of this field, and enables this + * editable field with all registered plugins. + * This is essential to the initialization of the field. + * It must be called when the field becomes fully loaded and editable. + * @param {Element} field The field property. + * @protected + */ +goog.editor.Field.prototype.setupFieldObject = function(field) { + this.loadState_ = goog.editor.Field.LoadState_.EDITABLE; + this.field = field; + this.editableDomHelper = goog.dom.getDomHelper(field); + this.isModified_ = false; + this.isEverModified_ = false; + field.setAttribute('g_editable', 'true'); + goog.a11y.aria.setRole(field, goog.a11y.aria.Role.TEXTBOX); +}; + + +/** + * Help make the field not editable by setting internal data structures to null, + * and disabling this field with all registered plugins. + * @private + */ +goog.editor.Field.prototype.tearDownFieldObject_ = function() { + this.loadState_ = goog.editor.Field.LoadState_.UNEDITABLE; + + for (var classId in this.plugins_) { + var plugin = this.plugins_[classId]; + if (!plugin.activeOnUneditableFields()) { + plugin.disable(this); + } + } + + this.field = null; + this.editableDomHelper = null; +}; + + +/** + * Initialize listeners on the field. + * @private + */ +goog.editor.Field.prototype.setupChangeListeners_ = function() { + if ((goog.userAgent.product.IPHONE || goog.userAgent.product.IPAD) && + this.usesIframe() && this.shouldRefocusOnInputMobileSafari()) { + // This is a workaround for the iOS bug that text input doesn't work + // when the main window listens touch events. + var editWindow = this.getEditableDomHelper().getWindow(); + this.boundRefocusListenerMobileSafari_ = + goog.bind(editWindow.focus, editWindow); + editWindow.addEventListener(goog.events.EventType.KEYDOWN, + this.boundRefocusListenerMobileSafari_, false); + editWindow.addEventListener(goog.events.EventType.TOUCHEND, + this.boundRefocusListenerMobileSafari_, false); + } + if (goog.userAgent.OPERA && this.usesIframe()) { + // We can't use addListener here because we need to listen on the window, + // and removing listeners on window objects from the event register throws + // an exception if the window is closed. + this.boundFocusListenerOpera_ = + goog.bind(this.dispatchFocusAndBeforeFocus_, this); + this.boundBlurListenerOpera_ = + goog.bind(this.dispatchBlur, this); + var editWindow = this.getEditableDomHelper().getWindow(); + editWindow.addEventListener(goog.events.EventType.FOCUS, + this.boundFocusListenerOpera_, false); + editWindow.addEventListener(goog.events.EventType.BLUR, + this.boundBlurListenerOpera_, false); + } else { + if (goog.editor.BrowserFeature.SUPPORTS_FOCUSIN) { + this.addListener(goog.events.EventType.FOCUS, this.dispatchFocus_); + this.addListener(goog.events.EventType.FOCUSIN, + this.dispatchBeforeFocus_); + } else { + this.addListener(goog.events.EventType.FOCUS, + this.dispatchFocusAndBeforeFocus_); + } + this.addListener(goog.events.EventType.BLUR, this.dispatchBlur, + goog.editor.BrowserFeature.USE_MUTATION_EVENTS); + } + + if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) { + // Ways to detect changes in Mozilla: + // + // keypress - check event.charCode (only typable characters has a + // charCode), but also keyboard commands lile Ctrl+C will + // return a charCode. + // dragdrop - fires when the user drops something. This does not necessary + // lead to a change but we cannot detect if it will or not + // + // Known Issues: We cannot detect cut and paste using menus + // We cannot detect when someone moves something out of the + // field using drag and drop. + // + this.setupMutationEventHandlersGecko(); + } else { + // Ways to detect that a change is about to happen in other browsers. + // (IE and Safari have these events. Opera appears to work, but we haven't + // researched it.) + // + // onbeforepaste + // onbeforecut + // ondrop - happens when the user drops something on the editable text + // field the value at this time does not contain the dropped text + // ondragleave - when the user drags something from the current document. + // This might not cause a change if the action was copy + // instead of move + // onkeypress - IE only fires keypress events if the key will generate + // output. It will not trigger for delete and backspace + // onkeydown - For delete and backspace + // + // known issues: IE triggers beforepaste just by opening the edit menu + // delete at the end should not cause beforechange + // backspace at the beginning should not cause beforechange + // see above in ondragleave + // TODO(user): Why don't we dispatchBeforeChange from the + // handleDrop event for all browsers? + this.addListener(['beforecut', 'beforepaste', 'drop', 'dragend'], + this.dispatchBeforeChange); + this.addListener(['cut', 'paste'], + goog.functions.lock(this.dispatchChange)); + this.addListener('drop', this.handleDrop_); + } + + // TODO(user): Figure out why we use dragend vs dragdrop and + // document this better. + var dropEventName = goog.userAgent.WEBKIT ? 'dragend' : 'dragdrop'; + this.addListener(dropEventName, this.handleDrop_); + + this.addListener(goog.events.EventType.KEYDOWN, this.handleKeyDown_); + this.addListener(goog.events.EventType.KEYPRESS, this.handleKeyPress_); + this.addListener(goog.events.EventType.KEYUP, this.handleKeyUp_); + + this.selectionChangeTimer_ = + new goog.async.Delay(this.handleSelectionChangeTimer_, + goog.editor.Field.SELECTION_CHANGE_FREQUENCY_, this); + + if (this.followLinkInNewWindow_) { + this.addListener( + goog.events.EventType.CLICK, goog.editor.Field.cancelLinkClick_); + } + + this.addListener(goog.events.EventType.MOUSEDOWN, this.handleMouseDown_); + if (this.useWindowMouseUp_) { + this.eventRegister.listen(this.editableDomHelper.getDocument(), + goog.events.EventType.MOUSEUP, this.handleMouseUp_); + this.addListener(goog.events.EventType.DRAGSTART, this.handleDragStart_); + } else { + this.addListener(goog.events.EventType.MOUSEUP, this.handleMouseUp_); + } +}; + + +/** + * Frequency to check for selection changes. + * @type {number} + * @private + */ +goog.editor.Field.SELECTION_CHANGE_FREQUENCY_ = 250; + + +/** + * Stops all listeners and timers. + * @protected + */ +goog.editor.Field.prototype.clearListeners = function() { + if (this.eventRegister) { + this.eventRegister.removeAll(); + } + + if ((goog.userAgent.product.IPHONE || goog.userAgent.product.IPAD) && + this.usesIframe() && this.shouldRefocusOnInputMobileSafari()) { + try { + var editWindow = this.getEditableDomHelper().getWindow(); + editWindow.removeEventListener(goog.events.EventType.KEYDOWN, + this.boundRefocusListenerMobileSafari_, false); + editWindow.removeEventListener(goog.events.EventType.TOUCHEND, + this.boundRefocusListenerMobileSafari_, false); + } catch (e) { + // The editWindow no longer exists, or has been navigated to a different- + // origin URL. Either way, the event listeners have already been removed + // for us. + } + delete this.boundRefocusListenerMobileSafari_; + } + if (goog.userAgent.OPERA && this.usesIframe()) { + try { + var editWindow = this.getEditableDomHelper().getWindow(); + editWindow.removeEventListener(goog.events.EventType.FOCUS, + this.boundFocusListenerOpera_, false); + editWindow.removeEventListener(goog.events.EventType.BLUR, + this.boundBlurListenerOpera_, false); + } catch (e) { + // The editWindow no longer exists, or has been navigated to a different- + // origin URL. Either way, the event listeners have already been removed + // for us. + } + delete this.boundFocusListenerOpera_; + delete this.boundBlurListenerOpera_; + } + + if (this.changeTimerGecko_) { + this.changeTimerGecko_.stop(); + } + this.delayedChangeTimer_.stop(); +}; + + +/** @override */ +goog.editor.Field.prototype.disposeInternal = function() { + if (this.isLoading() || this.isLoaded()) { + goog.log.warning(this.logger, 'Disposing a field that is in use.'); + } + + if (this.getOriginalElement()) { + this.execCommand(goog.editor.Command.CLEAR_LOREM); + } + + this.tearDownFieldObject_(); + this.clearListeners(); + this.clearFieldLoadListener_(); + this.originalDomHelper = null; + + if (this.eventRegister) { + this.eventRegister.dispose(); + this.eventRegister = null; + } + + this.removeAllWrappers(); + + if (goog.editor.Field.getActiveFieldId() == this.id) { + goog.editor.Field.setActiveFieldId(null); + } + + for (var classId in this.plugins_) { + var plugin = this.plugins_[classId]; + if (plugin.isAutoDispose()) { + plugin.dispose(); + } + } + delete(this.plugins_); + + goog.editor.Field.superClass_.disposeInternal.call(this); +}; + + +/** + * Attach an wrapper to this field, to be thrown out when the field + * is disposed. + * @param {goog.Disposable} wrapper The wrapper to attach. + */ +goog.editor.Field.prototype.attachWrapper = function(wrapper) { + this.wrappers_.push(wrapper); +}; + + +/** + * Removes all wrappers and destroys them. + */ +goog.editor.Field.prototype.removeAllWrappers = function() { + var wrapper; + while (wrapper = this.wrappers_.pop()) { + wrapper.dispose(); + } +}; + + +/** + * Sets whether activating a hyperlink in this editable field will open a new + * window or not. + * @param {boolean} followLinkInNewWindow + */ +goog.editor.Field.prototype.setFollowLinkInNewWindow = + function(followLinkInNewWindow) { + this.followLinkInNewWindow_ = followLinkInNewWindow; +}; + + +/** + * List of mutation events in Gecko browsers. + * @type {Array<string>} + * @protected + */ +goog.editor.Field.MUTATION_EVENTS_GECKO = [ + 'DOMNodeInserted', + 'DOMNodeRemoved', + 'DOMNodeRemovedFromDocument', + 'DOMNodeInsertedIntoDocument', + 'DOMCharacterDataModified' +]; + + +/** + * Mutation events tell us when something has changed for mozilla. + * @protected + */ +goog.editor.Field.prototype.setupMutationEventHandlersGecko = function() { + // Always use DOMSubtreeModified on Gecko when not using an iframe so that + // DOM mutations outside the Field do not trigger handleMutationEventGecko_. + if (goog.editor.BrowserFeature.HAS_DOM_SUBTREE_MODIFIED_EVENT || + !this.usesIframe()) { + this.eventRegister.listen(this.getElement(), 'DOMSubtreeModified', + this.handleMutationEventGecko_); + } else { + var doc = this.getEditableDomHelper().getDocument(); + this.eventRegister.listen(doc, goog.editor.Field.MUTATION_EVENTS_GECKO, + this.handleMutationEventGecko_, true); + + // DOMAttrModified fires for a lot of events we want to ignore. This goes + // through a different handler so that we can ignore many of these. + this.eventRegister.listen(doc, 'DOMAttrModified', + goog.bind(this.handleDomAttrChange, this, + this.handleMutationEventGecko_), + true); + } +}; + + +/** + * Handle before change key events and fire the beforetab event if appropriate. + * This needs to happen on keydown in IE and keypress in FF. + * @param {goog.events.BrowserEvent} e The browser event. + * @return {boolean} Whether to still perform the default key action. Only set + * to true if the actual event has already been canceled. + * @private + */ +goog.editor.Field.prototype.handleBeforeChangeKeyEvent_ = function(e) { + // There are two reasons to block a key: + var block = + // #1: to intercept a tab + // TODO: possibly don't allow clients to intercept tabs outside of LIs and + // maybe tables as well? + (e.keyCode == goog.events.KeyCodes.TAB && !this.dispatchBeforeTab_(e)) || + // #2: to block a Firefox-specific bug where Macs try to navigate + // back a page when you hit command+left arrow or comamnd-right arrow. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=341886 + // This was fixed in Firefox 29, but still exists in older versions. + (goog.userAgent.GECKO && e.metaKey && + !goog.userAgent.isVersionOrHigher(29) && + (e.keyCode == goog.events.KeyCodes.LEFT || + e.keyCode == goog.events.KeyCodes.RIGHT)); + + if (block) { + e.preventDefault(); + return false; + } else { + // In Gecko we have both keyCode and charCode. charCode is for human + // readable characters like a, b and c. However pressing ctrl+c and so on + // also causes charCode to be set. + + // TODO(arv): Del at end of field or backspace at beginning should be + // ignored. + this.gotGeneratingKey_ = e.charCode || + goog.editor.Field.isGeneratingKey_(e, goog.userAgent.GECKO); + if (this.gotGeneratingKey_) { + this.dispatchBeforeChange(); + // TODO(robbyw): Should we return the value of the above? + } + } + + return true; +}; + + +/** + * Keycodes that result in a selectionchange event (e.g. the cursor moving). + * @type {!Object<number, number>} + */ +goog.editor.Field.SELECTION_CHANGE_KEYCODES = { + 8: 1, // backspace + 9: 1, // tab + 13: 1, // enter + 33: 1, // page up + 34: 1, // page down + 35: 1, // end + 36: 1, // home + 37: 1, // left + 38: 1, // up + 39: 1, // right + 40: 1, // down + 46: 1 // delete +}; + + +/** + * Map of keyCodes (not charCodes) that when used in conjunction with the + * Ctrl key cause selection changes in the field contents. These are the keys + * that are not handled by the basic formatting trogedit plugins. Note that + * combinations like Ctrl-left etc are already handled in + * SELECTION_CHANGE_KEYCODES + * @type {Object} + * @private + */ +goog.editor.Field.CTRL_KEYS_CAUSING_SELECTION_CHANGES_ = { + 65: true, // A + 86: true, // V + 88: true // X +}; + + +/** + * Map of keyCodes (not charCodes) that might need to be handled as a keyboard + * shortcut (even when ctrl/meta key is not pressed) by some plugin. Currently + * it is a small list. If it grows too big we can optimize it by using ranges + * or extending it from SELECTION_CHANGE_KEYCODES + * @type {Object} + * @private + */ +goog.editor.Field.POTENTIAL_SHORTCUT_KEYCODES_ = { + 8: 1, // backspace + 9: 1, // tab + 13: 1, // enter + 27: 1, // esc + 33: 1, // page up + 34: 1, // page down + 37: 1, // left + 38: 1, // up + 39: 1, // right + 40: 1 // down +}; + + +/** + * Calls all the plugins of the given operation, in sequence, with the + * given arguments. This is short-circuiting: once one plugin cancels + * the event, no more plugins will be invoked. + * @param {goog.editor.Plugin.Op} op A plugin op. + * @param {...*} var_args The arguments to the plugin. + * @return {boolean} True if one of the plugins cancel the event, false + * otherwise. + * @private + */ +goog.editor.Field.prototype.invokeShortCircuitingOp_ = function(op, var_args) { + var plugins = this.indexedPlugins_[op]; + var argList = goog.array.slice(arguments, 1); + for (var i = 0; i < plugins.length; ++i) { + // If the plugin returns true, that means it handled the event and + // we shouldn't propagate to the other plugins. + var plugin = plugins[i]; + if ((plugin.isEnabled(this) || + goog.editor.Plugin.IRREPRESSIBLE_OPS[op]) && + plugin[goog.editor.Plugin.OPCODE[op]].apply(plugin, argList)) { + // Only one plugin is allowed to handle the event. If for some reason + // a plugin wants to handle it and still allow other plugins to handle + // it, it shouldn't return true. + return true; + } + } + + return false; +}; + + +/** + * Invoke this operation on all plugins with the given arguments. + * @param {goog.editor.Plugin.Op} op A plugin op. + * @param {...*} var_args The arguments to the plugin. + * @private + */ +goog.editor.Field.prototype.invokeOp_ = function(op, var_args) { + var plugins = this.indexedPlugins_[op]; + var argList = goog.array.slice(arguments, 1); + for (var i = 0; i < plugins.length; ++i) { + var plugin = plugins[i]; + if (plugin.isEnabled(this) || + goog.editor.Plugin.IRREPRESSIBLE_OPS[op]) { + plugin[goog.editor.Plugin.OPCODE[op]].apply(plugin, argList); + } + } +}; + + +/** + * Reduce this argument over all plugins. The result of each plugin invocation + * will be passed to the next plugin invocation. See goog.array.reduce. + * @param {goog.editor.Plugin.Op} op A plugin op. + * @param {string} arg The argument to reduce. For now, we assume it's a + * string, but we should widen this later if there are reducing + * plugins that don't operate on strings. + * @param {...*} var_args Any extra arguments to pass to the plugin. These args + * will not be reduced. + * @return {string} The reduced argument. + * @private + */ +goog.editor.Field.prototype.reduceOp_ = function(op, arg, var_args) { + var plugins = this.indexedPlugins_[op]; + var argList = goog.array.slice(arguments, 1); + for (var i = 0; i < plugins.length; ++i) { + var plugin = plugins[i]; + if (plugin.isEnabled(this) || + goog.editor.Plugin.IRREPRESSIBLE_OPS[op]) { + argList[0] = plugin[goog.editor.Plugin.OPCODE[op]].apply( + plugin, argList); + } + } + return argList[0]; +}; + + +/** + * Prepare the given contents, then inject them into the editable field. + * @param {?string} contents The contents to prepare. + * @param {Element} field The field element. + * @protected + */ +goog.editor.Field.prototype.injectContents = function(contents, field) { + var styles = {}; + var newHtml = this.getInjectableContents(contents, styles); + goog.style.setStyle(field, styles); + goog.editor.node.replaceInnerHtml(field, newHtml); +}; + + +/** + * Returns prepared contents that can be injected into the editable field. + * @param {?string} contents The contents to prepare. + * @param {Object} styles A map that will be populated with styles that should + * be applied to the field element together with the contents. + * @return {string} The prepared contents. + */ +goog.editor.Field.prototype.getInjectableContents = function(contents, styles) { + return this.reduceOp_( + goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, contents || '', styles); +}; + + +/** + * Handles keydown on the field. + * @param {goog.events.BrowserEvent} e The browser event. + * @private + */ +goog.editor.Field.prototype.handleKeyDown_ = function(e) { + if (!goog.editor.BrowserFeature.USE_MUTATION_EVENTS) { + if (!this.handleBeforeChangeKeyEvent_(e)) { + return; + } + } + + if (!this.invokeShortCircuitingOp_(goog.editor.Plugin.Op.KEYDOWN, e) && + goog.editor.BrowserFeature.USES_KEYDOWN) { + this.handleKeyboardShortcut_(e); + } +}; + + +/** + * Handles keypress on the field. + * @param {goog.events.BrowserEvent} e The browser event. + * @private + */ +goog.editor.Field.prototype.handleKeyPress_ = function(e) { + if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) { + if (!this.handleBeforeChangeKeyEvent_(e)) { + return; + } + } else { + // In IE only keys that generate output trigger keypress + // In Mozilla charCode is set for keys generating content. + this.gotGeneratingKey_ = true; + this.dispatchBeforeChange(); + } + + if (!this.invokeShortCircuitingOp_(goog.editor.Plugin.Op.KEYPRESS, e) && + !goog.editor.BrowserFeature.USES_KEYDOWN) { + this.handleKeyboardShortcut_(e); + } +}; + + +/** + * Handles keyup on the field. + * @param {goog.events.BrowserEvent} e The browser event. + * @private + */ +goog.editor.Field.prototype.handleKeyUp_ = function(e) { + if (!goog.editor.BrowserFeature.USE_MUTATION_EVENTS && + (this.gotGeneratingKey_ || + goog.editor.Field.isSpecialGeneratingKey_(e))) { + // The special keys won't have set the gotGeneratingKey flag, so we check + // for them explicitly + this.handleChange(); + } + + this.invokeShortCircuitingOp_(goog.editor.Plugin.Op.KEYUP, e); + + if (this.isEventStopped(goog.editor.Field.EventType.SELECTIONCHANGE)) { + return; + } + + if (goog.editor.Field.SELECTION_CHANGE_KEYCODES[e.keyCode] || + ((e.ctrlKey || e.metaKey) && + goog.editor.Field.CTRL_KEYS_CAUSING_SELECTION_CHANGES_[e.keyCode])) { + this.selectionChangeTimer_.start(); + } +}; + + +/** + * Handles keyboard shortcuts on the field. Note that we bake this into our + * handleKeyPress/handleKeyDown rather than using goog.events.KeyHandler or + * goog.ui.KeyboardShortcutHandler for performance reasons. Since these + * are handled on every key stroke, we do not want to be going out to the + * event system every time. + * @param {goog.events.BrowserEvent} e The browser event. + * @private + */ +goog.editor.Field.prototype.handleKeyboardShortcut_ = function(e) { + // Alt key is used for i18n languages to enter certain characters. like + // control + alt + z (used for IMEs) and control + alt + s for Polish. + // So we don't invoke handleKeyboardShortcut at all for alt keys. + if (e.altKey) { + return; + } + + var isModifierPressed = goog.userAgent.MAC ? e.metaKey : e.ctrlKey; + if (isModifierPressed || + goog.editor.Field.POTENTIAL_SHORTCUT_KEYCODES_[e.keyCode]) { + // TODO(user): goog.events.KeyHandler uses much more complicated logic + // to determine key. Consider changing to what they do. + var key = e.charCode || e.keyCode; + + if (key == 17) { // Ctrl key + // In IE and Webkit pressing Ctrl key itself results in this event. + return; + } + + var stringKey = String.fromCharCode(key).toLowerCase(); + if (this.invokeShortCircuitingOp_(goog.editor.Plugin.Op.SHORTCUT, + e, stringKey, isModifierPressed)) { + e.preventDefault(); + // We don't call stopPropagation as some other handler outside of + // trogedit might need it. + } + } +}; + + +/** + * Executes an editing command as per the registered plugins. + * @param {string} command The command to execute. + * @param {...*} var_args Any additional parameters needed to execute the + * command. + * @return {*} False if the command wasn't handled, otherwise, the result of + * the command. + */ +goog.editor.Field.prototype.execCommand = function(command, var_args) { + var args = arguments; + var result; + + var plugins = this.indexedPlugins_[goog.editor.Plugin.Op.EXEC_COMMAND]; + for (var i = 0; i < plugins.length; ++i) { + // If the plugin supports the command, that means it handled the + // event and we shouldn't propagate to the other plugins. + var plugin = plugins[i]; + if (plugin.isEnabled(this) && plugin.isSupportedCommand(command)) { + result = plugin.execCommand.apply(plugin, args); + break; + } + } + + return result; +}; + + +/** + * Gets the value of command(s). + * @param {string|Array<string>} commands String name(s) of the command. + * @return {*} Value of each command. Returns false (or array of falses) + * if designMode is off or the field is otherwise uneditable, and + * there are no activeOnUneditable plugins for the command. + */ +goog.editor.Field.prototype.queryCommandValue = function(commands) { + var isEditable = this.isLoaded() && this.isSelectionEditable(); + if (goog.isString(commands)) { + return this.queryCommandValueInternal_(commands, isEditable); + } else { + var state = {}; + for (var i = 0; i < commands.length; i++) { + state[commands[i]] = this.queryCommandValueInternal_(commands[i], + isEditable); + } + return state; + } +}; + + +/** + * Gets the value of this command. + * @param {string} command The command to check. + * @param {boolean} isEditable Whether the field is currently editable. + * @return {*} The state of this command. Null if not handled. + * False if the field is uneditable and there are no handlers for + * uneditable commands. + * @private + */ +goog.editor.Field.prototype.queryCommandValueInternal_ = function(command, + isEditable) { + var plugins = this.indexedPlugins_[goog.editor.Plugin.Op.QUERY_COMMAND]; + for (var i = 0; i < plugins.length; ++i) { + var plugin = plugins[i]; + if (plugin.isEnabled(this) && plugin.isSupportedCommand(command) && + (isEditable || plugin.activeOnUneditableFields())) { + return plugin.queryCommandValue(command); + } + } + return isEditable ? null : false; +}; + + +/** + * Fires a change event only if the attribute change effects the editiable + * field. We ignore events that are internal browser events (ie scrollbar + * state change) + * @param {Function} handler The function to call if this is not an internal + * browser event. + * @param {goog.events.BrowserEvent} browserEvent The browser event. + * @protected + */ +goog.editor.Field.prototype.handleDomAttrChange = + function(handler, browserEvent) { + if (this.isEventStopped(goog.editor.Field.EventType.CHANGE)) { + return; + } + + var e = browserEvent.getBrowserEvent(); + + // For XUL elements, since we don't care what they are doing + try { + if (e.originalTarget.prefix || e.originalTarget.nodeName == 'scrollbar') { + return; + } + } catch (ex1) { + // Some XUL nodes don't like you reading their properties. If we got + // the exception, this implies a XUL node so we can return. + return; + } + + // Check if prev and new values are different, sometimes this fires when + // nothing has really changed. + if (e.prevValue == e.newValue) { + return; + } + handler.call(this, e); +}; + + +/** + * Handle a mutation event. + * @param {goog.events.BrowserEvent|Event} e The browser event. + * @private + */ +goog.editor.Field.prototype.handleMutationEventGecko_ = function(e) { + if (this.isEventStopped(goog.editor.Field.EventType.CHANGE)) { + return; + } + + e = e.getBrowserEvent ? e.getBrowserEvent() : e; + // For people with firebug, firebug sets this property on elements it is + // inserting into the dom. + if (e.target.firebugIgnore) { + return; + } + + this.isModified_ = true; + this.isEverModified_ = true; + this.changeTimerGecko_.start(); +}; + + +/** + * Handle drop events. Deal with focus/selection issues and set the document + * as changed. + * @param {goog.events.BrowserEvent} e The browser event. + * @private + */ +goog.editor.Field.prototype.handleDrop_ = function(e) { + if (goog.userAgent.IE) { + // TODO(user): This should really be done in the loremipsum plugin. + this.execCommand(goog.editor.Command.CLEAR_LOREM, true); + } + + // TODO(user): I just moved this code to this location, but I wonder why + // it is only done for this case. Investigate. + if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) { + this.dispatchFocusAndBeforeFocus_(); + } + + this.dispatchChange(); +}; + + +/** + * @return {HTMLIFrameElement} The iframe that's body is editable. + * @protected + */ +goog.editor.Field.prototype.getEditableIframe = function() { + var dh; + if (this.usesIframe() && (dh = this.getEditableDomHelper())) { + // If the iframe has been destroyed, the dh could still exist since the + // node may not be gc'ed, but fetching the window can fail. + var win = dh.getWindow(); + return /** @type {HTMLIFrameElement} */ (win && win.frameElement); + } + return null; +}; + + +/** + * @return {goog.dom.DomHelper?} The dom helper for the editable node. + */ +goog.editor.Field.prototype.getEditableDomHelper = function() { + return this.editableDomHelper; +}; + + +/** + * @return {goog.dom.AbstractRange?} Closure range object wrapping the selection + * in this field or null if this field is not currently editable. + */ +goog.editor.Field.prototype.getRange = function() { + var win = this.editableDomHelper && this.editableDomHelper.getWindow(); + return win && goog.dom.Range.createFromWindow(win); +}; + + +/** + * Dispatch a selection change event, optionally caused by the given browser + * event or selecting the given target. + * @param {goog.events.BrowserEvent=} opt_e Optional browser event causing this + * event. + * @param {Node=} opt_target The node the selection changed to. + */ +goog.editor.Field.prototype.dispatchSelectionChangeEvent = function( + opt_e, opt_target) { + if (this.isEventStopped(goog.editor.Field.EventType.SELECTIONCHANGE)) { + return; + } + + // The selection is editable only if the selection is inside the + // editable field. + var range = this.getRange(); + var rangeContainer = range && range.getContainerElement(); + this.isSelectionEditable_ = !!rangeContainer && + goog.dom.contains(this.getElement(), rangeContainer); + + this.dispatchCommandValueChange(); + this.dispatchEvent({ + type: goog.editor.Field.EventType.SELECTIONCHANGE, + originalType: opt_e && opt_e.type + }); + + this.invokeShortCircuitingOp_(goog.editor.Plugin.Op.SELECTION, + opt_e, opt_target); +}; + + +/** + * Dispatch a selection change event using a browser event that was + * asynchronously saved earlier. + * @private + */ +goog.editor.Field.prototype.handleSelectionChangeTimer_ = function() { + var t = this.selectionChangeTarget_; + this.selectionChangeTarget_ = null; + this.dispatchSelectionChangeEvent(undefined, t); +}; + + +/** + * This dispatches the beforechange event on the editable field + */ +goog.editor.Field.prototype.dispatchBeforeChange = function() { + if (this.isEventStopped(goog.editor.Field.EventType.BEFORECHANGE)) { + return; + } + + this.dispatchEvent(goog.editor.Field.EventType.BEFORECHANGE); +}; + + +/** + * This dispatches the beforetab event on the editable field. If this event is + * cancelled, then the default tab behavior is prevented. + * @param {goog.events.BrowserEvent} e The tab event. + * @private + * @return {boolean} The result of dispatchEvent. + */ +goog.editor.Field.prototype.dispatchBeforeTab_ = function(e) { + return this.dispatchEvent({ + type: goog.editor.Field.EventType.BEFORETAB, + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.ctrlKey + }); +}; + + +/** + * Temporarily ignore change events. If the time has already been set, it will + * fire immediately now. Further setting of the timer is stopped and + * dispatching of events is stopped until startChangeEvents is called. + * @param {boolean=} opt_stopChange Whether to ignore base change events. + * @param {boolean=} opt_stopDelayedChange Whether to ignore delayed change + * events. + */ +goog.editor.Field.prototype.stopChangeEvents = function(opt_stopChange, + opt_stopDelayedChange) { + if (opt_stopChange) { + if (this.changeTimerGecko_) { + this.changeTimerGecko_.fireIfActive(); + } + + this.stopEvent(goog.editor.Field.EventType.CHANGE); + } + if (opt_stopDelayedChange) { + this.clearDelayedChange(); + this.stopEvent(goog.editor.Field.EventType.DELAYEDCHANGE); + } +}; + + +/** + * Start change events again and fire once if desired. + * @param {boolean=} opt_fireChange Whether to fire the change event + * immediately. + * @param {boolean=} opt_fireDelayedChange Whether to fire the delayed change + * event immediately. + */ +goog.editor.Field.prototype.startChangeEvents = function(opt_fireChange, + opt_fireDelayedChange) { + + if (!opt_fireChange && this.changeTimerGecko_) { + // In the case where change events were stopped and we're not firing + // them on start, the user was trying to suppress all change or delayed + // change events. Clear the change timer now while the events are still + // stopped so that its firing doesn't fire a stopped change event, or + // queue up a delayed change event that we were trying to stop. + this.changeTimerGecko_.fireIfActive(); + } + + this.startEvent(goog.editor.Field.EventType.CHANGE); + this.startEvent(goog.editor.Field.EventType.DELAYEDCHANGE); + if (opt_fireChange) { + this.handleChange(); + } + + if (opt_fireDelayedChange) { + this.dispatchDelayedChange_(); + } +}; + + +/** + * Stops the event of the given type from being dispatched. + * @param {goog.editor.Field.EventType} eventType type of event to stop. + */ +goog.editor.Field.prototype.stopEvent = function(eventType) { + this.stoppedEvents_[eventType] = 1; +}; + + +/** + * Re-starts the event of the given type being dispatched, if it had + * previously been stopped with stopEvent(). + * @param {goog.editor.Field.EventType} eventType type of event to start. + */ +goog.editor.Field.prototype.startEvent = function(eventType) { + // Toggling this bit on/off instead of deleting it/re-adding it + // saves array allocations. + this.stoppedEvents_[eventType] = 0; +}; + + +/** + * Block an event for a short amount of time. Intended + * for the situation where an event pair fires in quick succession + * (e.g., mousedown/mouseup, keydown/keyup, focus/blur), + * and we want the second event in the pair to get "debounced." + * + * WARNING: This should never be used to solve race conditions or for + * mission-critical actions. It should only be used for UI improvements, + * where it's okay if the behavior is non-deterministic. + * + * @param {goog.editor.Field.EventType} eventType type of event to debounce. + */ +goog.editor.Field.prototype.debounceEvent = function(eventType) { + this.debouncedEvents_[eventType] = goog.now(); +}; + + +/** + * Checks if the event of the given type has stopped being dispatched + * @param {goog.editor.Field.EventType} eventType type of event to check. + * @return {boolean} true if the event has been stopped with stopEvent(). + * @protected + */ +goog.editor.Field.prototype.isEventStopped = function(eventType) { + return !!this.stoppedEvents_[eventType] || + (this.debouncedEvents_[eventType] && + (goog.now() - this.debouncedEvents_[eventType] <= + goog.editor.Field.DEBOUNCE_TIME_MS_)); +}; + + +/** + * Calls a function to manipulate the dom of this field. This method should be + * used whenever Trogedit clients need to modify the dom of the field, so that + * delayed change events are handled appropriately. Extra delayed change events + * will cause undesired states to be added to the undo-redo stack. This method + * will always fire at most one delayed change event, depending on the value of + * {@code opt_preventDelayedChange}. + * + * @param {function()} func The function to call that will manipulate the dom. + * @param {boolean=} opt_preventDelayedChange Whether delayed change should be + * prevented after calling {@code func}. Defaults to always firing + * delayed change. + * @param {Object=} opt_handler Object in whose scope to call the listener. + */ +goog.editor.Field.prototype.manipulateDom = function(func, + opt_preventDelayedChange, opt_handler) { + + this.stopChangeEvents(true, true); + // We don't want any problems with the passed in function permanently + // stopping change events. That would break Trogedit. + try { + func.call(opt_handler); + } finally { + // If the field isn't loaded then change and delayed change events will be + // started as part of the onload behavior. + if (this.isLoaded()) { + // We assume that func always modified the dom and so fire a single change + // event. Delayed change is only fired if not prevented by the user. + if (opt_preventDelayedChange) { + this.startEvent(goog.editor.Field.EventType.CHANGE); + this.handleChange(); + this.startEvent(goog.editor.Field.EventType.DELAYEDCHANGE); + } else { + this.dispatchChange(); + } + } + } +}; + + +/** + * Dispatches a command value change event. + * @param {Array<string>=} opt_commands Commands whose state has + * changed. + */ +goog.editor.Field.prototype.dispatchCommandValueChange = + function(opt_commands) { + if (opt_commands) { + this.dispatchEvent({ + type: goog.editor.Field.EventType.COMMAND_VALUE_CHANGE, + commands: opt_commands + }); + } else { + this.dispatchEvent(goog.editor.Field.EventType.COMMAND_VALUE_CHANGE); + } +}; + + +/** + * Dispatches the appropriate set of change events. This only fires + * synchronous change events in blended-mode, iframe-using mozilla. It just + * starts the appropriate timer for goog.editor.Field.EventType.DELAYEDCHANGE. + * This also starts up change events again if they were stopped. + * + * @param {boolean=} opt_noDelay True if + * goog.editor.Field.EventType.DELAYEDCHANGE should be fired syncronously. + */ +goog.editor.Field.prototype.dispatchChange = function(opt_noDelay) { + this.startChangeEvents(true, opt_noDelay); +}; + + +/** + * Handle a change in the Editable Field. Marks the field has modified, + * dispatches the change event on the editable field (moz only), starts the + * timer for the delayed change event. Note that these actions only occur if + * the proper events are not stopped. + */ +goog.editor.Field.prototype.handleChange = function() { + if (this.isEventStopped(goog.editor.Field.EventType.CHANGE)) { + return; + } + + // Clear the changeTimerGecko_ if it's active, since any manual call to + // handle change is equiavlent to changeTimerGecko_.fire(). + if (this.changeTimerGecko_) { + this.changeTimerGecko_.stop(); + } + + this.isModified_ = true; + this.isEverModified_ = true; + + if (this.isEventStopped(goog.editor.Field.EventType.DELAYEDCHANGE)) { + return; + } + + this.delayedChangeTimer_.start(); +}; + + +/** + * Dispatch a delayed change event. + * @private + */ +goog.editor.Field.prototype.dispatchDelayedChange_ = function() { + if (this.isEventStopped(goog.editor.Field.EventType.DELAYEDCHANGE)) { + return; + } + // Clear the delayedChangeTimer_ if it's active, since any manual call to + // dispatchDelayedChange_ is equivalent to delayedChangeTimer_.fire(). + this.delayedChangeTimer_.stop(); + this.isModified_ = false; + this.dispatchEvent(goog.editor.Field.EventType.DELAYEDCHANGE); +}; + + +/** + * Don't wait for the timer and just fire the delayed change event if it's + * pending. + */ +goog.editor.Field.prototype.clearDelayedChange = function() { + // The changeTimerGecko_ will queue up a delayed change so to fully clear + // delayed change we must also clear this timer. + if (this.changeTimerGecko_) { + this.changeTimerGecko_.fireIfActive(); + } + this.delayedChangeTimer_.fireIfActive(); +}; + + +/** + * Dispatch beforefocus and focus for FF. Note that both of these actually + * happen in the document's "focus" event. Unfortunately, we don't actually + * have a way of getting in before the focus event in FF (boo! hiss!). + * In IE, we use onfocusin for before focus and onfocus for focus. + * @private + */ +goog.editor.Field.prototype.dispatchFocusAndBeforeFocus_ = function() { + this.dispatchBeforeFocus_(); + this.dispatchFocus_(); +}; + + +/** + * Dispatches a before focus event. + * @private + */ +goog.editor.Field.prototype.dispatchBeforeFocus_ = function() { + if (this.isEventStopped(goog.editor.Field.EventType.BEFOREFOCUS)) { + return; + } + + this.execCommand(goog.editor.Command.CLEAR_LOREM, true); + this.dispatchEvent(goog.editor.Field.EventType.BEFOREFOCUS); +}; + + +/** + * Dispatches a focus event. + * @private + */ +goog.editor.Field.prototype.dispatchFocus_ = function() { + if (this.isEventStopped(goog.editor.Field.EventType.FOCUS)) { + return; + } + goog.editor.Field.setActiveFieldId(this.id); + + this.isSelectionEditable_ = true; + + this.dispatchEvent(goog.editor.Field.EventType.FOCUS); + + if (goog.editor.BrowserFeature. + PUTS_CURSOR_BEFORE_FIRST_BLOCK_ELEMENT_ON_FOCUS) { + // If the cursor is at the beginning of the field, make sure that it is + // in the first user-visible line break, e.g., + // no selection: <div><p>...</p></div> --> <div><p>|cursor|...</p></div> + // <div>|cursor|<p>...</p></div> --> <div><p>|cursor|...</p></div> + // <body>|cursor|<p>...</p></body> --> <body><p>|cursor|...</p></body> + var field = this.getElement(); + var range = this.getRange(); + + if (range) { + var focusNode = range.getFocusNode(); + if (range.getFocusOffset() == 0 && (!focusNode || focusNode == field || + focusNode.tagName == goog.dom.TagName.BODY)) { + goog.editor.range.selectNodeStart(field); + } + } + } + + if (!goog.editor.BrowserFeature.CLEARS_SELECTION_WHEN_FOCUS_LEAVES && + this.usesIframe()) { + var parent = this.getEditableDomHelper().getWindow().parent; + parent.getSelection().removeAllRanges(); + } +}; + + +/** + * Dispatches a blur event. + * @protected + */ +goog.editor.Field.prototype.dispatchBlur = function() { + if (this.isEventStopped(goog.editor.Field.EventType.BLUR)) { + return; + } + + // Another field may have already been registered as active, so only + // clear out the active field id if we still think this field is active. + if (goog.editor.Field.getActiveFieldId() == this.id) { + goog.editor.Field.setActiveFieldId(null); + } + + this.isSelectionEditable_ = false; + this.dispatchEvent(goog.editor.Field.EventType.BLUR); +}; + + +/** + * @return {boolean} Whether the selection is editable. + */ +goog.editor.Field.prototype.isSelectionEditable = function() { + return this.isSelectionEditable_; +}; + + +/** + * Event handler for clicks in browsers that will follow a link when the user + * clicks, even if it's editable. We stop the click manually + * @param {goog.events.BrowserEvent} e The event. + * @private + */ +goog.editor.Field.cancelLinkClick_ = function(e) { + if (goog.dom.getAncestorByTagNameAndClass( + /** @type {Node} */ (e.target), goog.dom.TagName.A)) { + e.preventDefault(); + } +}; + + +/** + * Handle mouse down inside the editable field. + * @param {goog.events.BrowserEvent} e The event. + * @private + */ +goog.editor.Field.prototype.handleMouseDown_ = function(e) { + goog.editor.Field.setActiveFieldId(this.id); + + // Open links in a new window if the user control + clicks. + if (goog.userAgent.IE) { + var targetElement = e.target; + if (targetElement && + targetElement.tagName == goog.dom.TagName.A && e.ctrlKey) { + this.originalDomHelper.getWindow().open(targetElement.href); + } + } + this.waitingForMouseUp_ = true; +}; + + +/** + * Handle drag start. Needs to cancel listening for the mouse up event on the + * window. + * @param {goog.events.BrowserEvent} e The event. + * @private + */ +goog.editor.Field.prototype.handleDragStart_ = function(e) { + this.waitingForMouseUp_ = false; +}; + + +/** + * Handle mouse up inside the editable field. + * @param {goog.events.BrowserEvent} e The event. + * @private + */ +goog.editor.Field.prototype.handleMouseUp_ = function(e) { + if (this.useWindowMouseUp_ && !this.waitingForMouseUp_) { + return; + } + this.waitingForMouseUp_ = false; + + /* + * We fire a selection change event immediately for listeners that depend on + * the native browser event object (e). On IE, a listener that tries to + * retrieve the selection with goog.dom.Range may see an out-of-date + * selection range. + */ + this.dispatchSelectionChangeEvent(e); + if (goog.userAgent.IE) { + /* + * Fire a second selection change event for listeners that need an + * up-to-date selection range. Save the event's target to be sent with it + * (it's safer than saving a copy of the event itself). + */ + this.selectionChangeTarget_ = /** @type {Node} */ (e.target); + this.selectionChangeTimer_.start(); + } +}; + + +/** + * Retrieve the HTML contents of a field. + * + * Do NOT just get the innerHTML of a field directly--there's a lot of + * processing that needs to happen. + * @return {string} The scrubbed contents of the field. + */ +goog.editor.Field.prototype.getCleanContents = function() { + if (this.queryCommandValue(goog.editor.Command.USING_LOREM)) { + return goog.string.Unicode.NBSP; + } + + if (!this.isLoaded()) { + // The field is uneditable, so it's ok to read contents directly. + var elem = this.getOriginalElement(); + if (!elem) { + goog.log.log(this.logger, goog.log.Level.SHOUT, + "Couldn't get the field element to read the contents"); + } + return elem.innerHTML; + } + + var fieldCopy = this.getFieldCopy(); + + // Allow the plugins to handle their cleanup. + this.invokeOp_(goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM, fieldCopy); + return this.reduceOp_( + goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML, fieldCopy.innerHTML); +}; + + +/** + * Get the copy of the editable field element, which has the innerHTML set + * correctly. + * @return {!Element} The copy of the editable field. + * @protected + */ +goog.editor.Field.prototype.getFieldCopy = function() { + var field = this.getElement(); + // Deep cloneNode strips some script tag contents in IE, so we do this. + var fieldCopy = /** @type {Element} */(field.cloneNode(false)); + + // For some reason, when IE sets innerHtml of the cloned node, it strips + // script tags that fall at the beginning of an element. Appending a + // non-breaking space prevents this. + var html = field.innerHTML; + if (goog.userAgent.IE && html.match(/^\s*<script/i)) { + html = goog.string.Unicode.NBSP + html; + } + fieldCopy.innerHTML = html; + return fieldCopy; +}; + + +/** + * Sets the contents of the field. + * @param {boolean} addParas Boolean to specify whether to add paragraphs + * to long fields. + * @param {?string} html html to insert. If html=null, then this defaults + * to a nsbp for mozilla and an empty string for IE. + * @param {boolean=} opt_dontFireDelayedChange True to make this content change + * not fire a delayed change event. + * @param {boolean=} opt_applyLorem Whether to apply lorem ipsum styles. + */ +goog.editor.Field.prototype.setHtml = function( + addParas, html, opt_dontFireDelayedChange, opt_applyLorem) { + if (this.isLoading()) { + goog.log.error(this.logger, "Can't set html while loading Trogedit"); + return; + } + + // Clear the lorem ipsum style, always. + if (opt_applyLorem) { + this.execCommand(goog.editor.Command.CLEAR_LOREM); + } + + if (html && addParas) { + html = '<p>' + html + '</p>'; + } + + // If we don't want change events to fire, we have to turn off change events + // before setting the field contents, since that causes mutation events. + if (opt_dontFireDelayedChange) { + this.stopChangeEvents(false, true); + } + + this.setInnerHtml_(html); + + // Set the lorem ipsum style, if the element is empty. + if (opt_applyLorem) { + this.execCommand(goog.editor.Command.UPDATE_LOREM); + } + + // TODO(user): This check should probably be moved to isEventStopped and + // startEvent. + if (this.isLoaded()) { + if (opt_dontFireDelayedChange) { // Turn back on change events + // We must fire change timer if necessary before restarting change events! + // Otherwise, the change timer firing after we restart events will cause + // the delayed change we were trying to stop. Flow: + // Stop delayed change + // setInnerHtml_, this starts the change timer + // start delayed change + // change timer fires + // starts delayed change timer since event was not stopped + // delayed change fires for the delayed change we tried to stop. + if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) { + this.changeTimerGecko_.fireIfActive(); + } + this.startChangeEvents(); + } else { // Mark the document as changed and fire change events. + this.dispatchChange(); + } + } +}; + + +/** + * Sets the inner HTML of the field. Works on both editable and + * uneditable fields. + * @param {?string} html The new inner HTML of the field. + * @private + */ +goog.editor.Field.prototype.setInnerHtml_ = function(html) { + var field = this.getElement(); + if (field) { + // Safari will put <style> tags into *new* <head> elements. When setting + // HTML, we need to remove these spare <head>s to make sure there's a + // clean slate, but keep the first <head>. + // Note: We punt on this issue for the non iframe case since + // we don't want to screw with the main document. + if (this.usesIframe() && goog.editor.BrowserFeature.MOVES_STYLE_TO_HEAD) { + var heads = field.ownerDocument.getElementsByTagName( + goog.dom.TagName.HEAD); + for (var i = heads.length - 1; i >= 1; --i) { + heads[i].parentNode.removeChild(heads[i]); + } + } + } else { + field = this.getOriginalElement(); + } + + if (field) { + this.injectContents(html, field); + } +}; + + +/** + * Attemps to turn on designMode for a document. This function can fail under + * certain circumstances related to the load event, and will throw an exception. + * @protected + */ +goog.editor.Field.prototype.turnOnDesignModeGecko = function() { + var doc = this.getEditableDomHelper().getDocument(); + + // NOTE(nicksantos): This will fail under certain conditions, like + // when the node has display: none. It's up to clients to ensure that + // their fields are valid when they try to make them editable. + doc.designMode = 'on'; + + if (goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) { + doc.execCommand('styleWithCSS', false, false); + } +}; + + +/** + * Installs styles if needed. Only writes styles when they can't be written + * inline directly into the field. + * @protected + */ +goog.editor.Field.prototype.installStyles = function() { + if (this.cssStyles && this.shouldLoadAsynchronously()) { + goog.style.installStyles(this.cssStyles, this.getElement()); + } +}; + + +/** + * Signal that the field is loaded and ready to use. Change events now are + * in effect. + * @private + */ +goog.editor.Field.prototype.dispatchLoadEvent_ = function() { + var field = this.getElement(); + + this.installStyles(); + this.startChangeEvents(); + goog.log.info(this.logger, 'Dispatching load ' + this.id); + this.dispatchEvent(goog.editor.Field.EventType.LOAD); +}; + + +/** + * @return {boolean} Whether the field is uneditable. + */ +goog.editor.Field.prototype.isUneditable = function() { + return this.loadState_ == goog.editor.Field.LoadState_.UNEDITABLE; +}; + + +/** + * @return {boolean} Whether the field has finished loading. + */ +goog.editor.Field.prototype.isLoaded = function() { + return this.loadState_ == goog.editor.Field.LoadState_.EDITABLE; +}; + + +/** + * @return {boolean} Whether the field is in the process of loading. + */ +goog.editor.Field.prototype.isLoading = function() { + return this.loadState_ == goog.editor.Field.LoadState_.LOADING; +}; + + +/** + * Gives the field focus. + */ +goog.editor.Field.prototype.focus = function() { + if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE && + this.usesIframe()) { + // In designMode, only the window itself can be focused; not the element. + this.getEditableDomHelper().getWindow().focus(); + } else { + if (goog.userAgent.OPERA) { + // Opera will scroll to the bottom of the focused document, even + // if it is contained in an iframe that is scrolled to the top and + // the bottom flows past the end of it. To prevent this, + // save the scroll position of the document containing the editor + // iframe, then restore it after the focus. + var scrollX = this.appWindow_.pageXOffset; + var scrollY = this.appWindow_.pageYOffset; + } + this.getElement().focus(); + if (goog.userAgent.OPERA) { + this.appWindow_.scrollTo( + /** @type {number} */ (scrollX), /** @type {number} */ (scrollY)); + } + } +}; + + +/** + * Gives the field focus and places the cursor at the start of the field. + */ +goog.editor.Field.prototype.focusAndPlaceCursorAtStart = function() { + // NOTE(user): Excluding Gecko to maintain existing behavior post refactoring + // placeCursorAtStart into its own method. In Gecko browsers that currently + // have a selection the existing selection will be restored, otherwise it + // will go to the start. + // TODO(user): Refactor the code using this and related methods. We should + // only mess with the selection in the case where there is not an existing + // selection in the field. + if (goog.editor.BrowserFeature.HAS_IE_RANGES || !goog.userAgent.GECKO) { + this.placeCursorAtStart(); + } + this.focus(); +}; + + +/** + * Place the cursor at the start of this field. It's recommended that you only + * use this method (and manipulate the selection in general) when there is not + * an existing selection in the field. + */ +goog.editor.Field.prototype.placeCursorAtStart = function() { + this.placeCursorAtStartOrEnd_(true); +}; + + +/** + * Place the cursor at the start of this field. It's recommended that you only + * use this method (and manipulate the selection in general) when there is not + * an existing selection in the field. + */ +goog.editor.Field.prototype.placeCursorAtEnd = function() { + this.placeCursorAtStartOrEnd_(false); +}; + + +/** + * Helper method to place the cursor at the start or end of this field. + * @param {boolean} isStart True for start, false for end. + * @private + */ +goog.editor.Field.prototype.placeCursorAtStartOrEnd_ = function(isStart) { + var field = this.getElement(); + if (field) { + var cursorPosition = isStart ? goog.editor.node.getLeftMostLeaf(field) : + goog.editor.node.getRightMostLeaf(field); + if (field == cursorPosition) { + // The rightmost leaf we found was the field element itself (which likely + // means the field element is empty). We can't place the cursor next to + // the field element, so just place it at the beginning. + goog.dom.Range.createCaret(field, 0).select(); + } else { + goog.editor.range.placeCursorNextTo(cursorPosition, isStart); + } + this.dispatchSelectionChangeEvent(); + } +}; + + +/** + * Restore a saved range, and set the focus on the field. + * If no range is specified, we simply set the focus. + * @param {goog.dom.SavedRange=} opt_range A previously saved selected range. + */ +goog.editor.Field.prototype.restoreSavedRange = function(opt_range) { + if (goog.userAgent.IE) { + this.focus(); + } + if (opt_range) { + opt_range.restore(); + } + if (!goog.userAgent.IE) { + this.focus(); + } +}; + + +/** + * Makes a field editable. + * + * @param {string=} opt_iframeSrc URL to set the iframe src to if necessary. + */ +goog.editor.Field.prototype.makeEditable = function(opt_iframeSrc) { + this.loadState_ = goog.editor.Field.LoadState_.LOADING; + + var field = this.getOriginalElement(); + + // TODO: In the fieldObj, save the field's id, className, cssText + // in order to reset it on closeField. That way, we can muck with the field's + // css, id, class and restore to how it was at the end. + this.nodeName = field.nodeName; + this.savedClassName_ = field.className; + this.setInitialStyle(field.style.cssText); + + field.className += ' editable'; + + this.makeEditableInternal(opt_iframeSrc); +}; + + +/** + * Handles actually making something editable - creating necessary nodes, + * injecting content, etc. + * @param {string=} opt_iframeSrc URL to set the iframe src to if necessary. + * @protected + */ +goog.editor.Field.prototype.makeEditableInternal = function(opt_iframeSrc) { + this.makeIframeField_(opt_iframeSrc); +}; + + +/** + * Handle the loading of the field (e.g. once the field is ready to setup). + * TODO(user): this should probably just be moved into dispatchLoadEvent_. + * @protected + */ +goog.editor.Field.prototype.handleFieldLoad = function() { + if (goog.userAgent.IE) { + // This sometimes fails if the selection is invalid. This can happen, for + // example, if you attach a CLICK handler to the field that causes the + // field to be removed from the DOM and replaced with an editor + // -- however, listening to another event like MOUSEDOWN does not have this + // issue since no mouse selection has happened at that time. + goog.dom.Range.clearSelection(this.editableDomHelper.getWindow()); + } + + if (goog.editor.Field.getActiveFieldId() != this.id) { + this.execCommand(goog.editor.Command.UPDATE_LOREM); + } + + this.setupChangeListeners_(); + this.dispatchLoadEvent_(); + + // Enabling plugins after we fire the load event so that clients have a + // chance to set initial field contents before we start mucking with + // everything. + for (var classId in this.plugins_) { + this.plugins_[classId].enable(this); + } +}; + + +/** + * Closes the field and cancels all pending change timers. Note that this + * means that if a change event has not fired yet, it will not fire. Clients + * should check fieldOj.isModified() if they depend on the final change event. + * Throws an error if the field is already uneditable. + * + * @param {boolean=} opt_skipRestore True to prevent copying of editable field + * contents back into the original node. + */ +goog.editor.Field.prototype.makeUneditable = function(opt_skipRestore) { + if (this.isUneditable()) { + throw Error('makeUneditable: Field is already uneditable'); + } + + // Fire any events waiting on a timeout. + // Clearing delayed change also clears changeTimerGecko_. + this.clearDelayedChange(); + this.selectionChangeTimer_.fireIfActive(); + this.execCommand(goog.editor.Command.CLEAR_LOREM); + + var html = null; + if (!opt_skipRestore && this.getElement()) { + // Rest of cleanup is simpler if field was never initialized. + html = this.getCleanContents(); + } + + // First clean up anything that happens in makeFieldEditable + // (i.e. anything that needs cleanup even if field has not loaded). + this.clearFieldLoadListener_(); + + var field = this.getOriginalElement(); + if (goog.editor.Field.getActiveFieldId() == field.id) { + goog.editor.Field.setActiveFieldId(null); + } + + // Clear all listeners before removing the nodes from the dom - if + // there are listeners on the iframe window, Firefox throws errors trying + // to unlisten once the iframe is no longer in the dom. + this.clearListeners(); + + // For fields that have loaded, clean up anything that happened in + // handleFieldOpen or later. + // If html is provided, copy it back and reset the properties on the field + // so that the original node will have the same properties as it did before + // it was made editable. + if (goog.isString(html)) { + goog.editor.node.replaceInnerHtml(field, html); + this.resetOriginalElemProperties(); + } + + this.restoreDom(); + this.tearDownFieldObject_(); + + // On Safari, make sure to un-focus the field so that the + // native "current field" highlight style gets removed. + if (goog.userAgent.WEBKIT) { + field.blur(); + } + + this.execCommand(goog.editor.Command.UPDATE_LOREM); + this.dispatchEvent(goog.editor.Field.EventType.UNLOAD); +}; + + +/** + * Restores the dom to how it was before being made editable. + * @protected + */ +goog.editor.Field.prototype.restoreDom = function() { + // TODO(user): Consider only removing the iframe if we are + // restoring the original node, aka, if opt_html. + var field = this.getOriginalElement(); + // TODO(robbyw): Consider throwing an error if !field. + if (field) { + // If the field is in the process of loading when it starts getting torn + // up, the iframe will not exist. + var iframe = this.getEditableIframe(); + if (iframe) { + goog.dom.replaceNode(field, iframe); + } + } +}; + + +/** + * Returns true if the field needs to be loaded asynchrnously. + * @return {boolean} True if loads are async. + * @protected + */ +goog.editor.Field.prototype.shouldLoadAsynchronously = function() { + if (!goog.isDef(this.isHttps_)) { + this.isHttps_ = false; + + if (goog.userAgent.IE && this.usesIframe()) { + // IE iframes need to load asynchronously if they are in https as we need + // to set an actual src on the iframe and wait for it to load. + + // Find the top-most window we have access to and see if it's https. + // Technically this could fail if we have an http frame in an https frame + // on the same domain (or vice versa), but walking up the window heirarchy + // to find the first window that has an http* protocol seems like + // overkill. + var win = this.originalDomHelper.getWindow(); + while (win != win.parent) { + try { + win = win.parent; + } catch (e) { + break; + } + } + var loc = win.location; + this.isHttps_ = loc.protocol == 'https:' && + loc.search.indexOf('nocheckhttps') == -1; + } + } + return this.isHttps_; +}; + + +/** + * Start the editable iframe creation process for Mozilla or IE whitebox. + * The iframes load asynchronously. + * + * @param {string=} opt_iframeSrc URL to set the iframe src to if necessary. + * @private + */ +goog.editor.Field.prototype.makeIframeField_ = function(opt_iframeSrc) { + var field = this.getOriginalElement(); + // TODO(robbyw): Consider throwing an error if !field. + if (field) { + var html = field.innerHTML; + + // Invoke prepareContentsHtml on all plugins to prepare html for editing. + // Make sure this is done before calling this.attachFrame which removes the + // original element from DOM tree. Plugins may assume that the original + // element is still in its original position in DOM. + var styles = {}; + html = this.reduceOp_(goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, + html, styles); + + var iframe = /** @type {!HTMLIFrameElement} */( + this.originalDomHelper.createDom(goog.dom.TagName.IFRAME, + this.getIframeAttributes())); + + // TODO(nicksantos): Figure out if this is ever needed in SAFARI? + // In IE over HTTPS we need to wait for a load event before we set up the + // iframe, this is to prevent a security prompt or access is denied + // errors. + // NOTE(user): This hasn't been confirmed. isHttps_ allows a query + // param, nocheckhttps, which we can use to ascertain if this is actually + // needed. It was originally thought to be needed for IE6 SP1, but + // errors have been seen in IE7 as well. + if (this.shouldLoadAsynchronously()) { + // onLoad is the function to call once the iframe is ready to continue + // loading. + var onLoad = goog.bind(this.iframeFieldLoadHandler, this, iframe, + html, styles); + + this.fieldLoadListenerKey_ = goog.events.listen(iframe, + goog.events.EventType.LOAD, onLoad, true); + + if (opt_iframeSrc) { + iframe.src = opt_iframeSrc; + } + } + + this.attachIframe(iframe); + + // Only continue if its not IE HTTPS in which case we're waiting for load. + if (!this.shouldLoadAsynchronously()) { + this.iframeFieldLoadHandler(iframe, html, styles); + } + } +}; + + +/** + * Given the original field element, and the iframe that is destined to + * become the editable field, styles them appropriately and add the iframe + * to the dom. + * + * @param {HTMLIFrameElement} iframe The iframe element. + * @protected + */ +goog.editor.Field.prototype.attachIframe = function(iframe) { + var field = this.getOriginalElement(); + // TODO(user): Why do we do these two lines .. and why whitebox only? + iframe.className = field.className; + iframe.id = field.id; + goog.dom.replaceNode(iframe, field); +}; + + +/** + * @param {Object} extraStyles A map of extra styles. + * @return {!goog.editor.icontent.FieldFormatInfo} The FieldFormatInfo + * object for this field's configuration. + * @protected + */ +goog.editor.Field.prototype.getFieldFormatInfo = function(extraStyles) { + var originalElement = this.getOriginalElement(); + var isStandardsMode = goog.editor.node.isStandardsMode(originalElement); + + return new goog.editor.icontent.FieldFormatInfo( + this.id, + isStandardsMode, + false, + false, + extraStyles); +}; + + +/** + * Writes the html content into the iframe. Handles writing any aditional + * styling as well. + * @param {HTMLIFrameElement} iframe Iframe to write contents into. + * @param {string} innerHtml The html content to write into the iframe. + * @param {Object} extraStyles A map of extra style attributes. + * @protected + */ +goog.editor.Field.prototype.writeIframeContent = function( + iframe, innerHtml, extraStyles) { + var formatInfo = this.getFieldFormatInfo(extraStyles); + + if (this.shouldLoadAsynchronously()) { + var doc = goog.dom.getFrameContentDocument(iframe); + goog.editor.icontent.writeHttpsInitialIframe(formatInfo, doc, innerHtml); + } else { + var styleInfo = new goog.editor.icontent.FieldStyleInfo( + this.getElement(), this.cssStyles); + goog.editor.icontent.writeNormalInitialIframe(formatInfo, innerHtml, + styleInfo, iframe); + } +}; + + +/** + * The function to call when the editable iframe loads. + * + * @param {HTMLIFrameElement} iframe Iframe that just loaded. + * @param {string} innerHtml Html to put inside the body of the iframe. + * @param {Object} styles Property-value map of CSS styles to install on + * editable field. + * @protected + */ +goog.editor.Field.prototype.iframeFieldLoadHandler = function(iframe, + innerHtml, styles) { + this.clearFieldLoadListener_(); + + iframe.allowTransparency = 'true'; + this.writeIframeContent(iframe, innerHtml, styles); + var doc = goog.dom.getFrameContentDocument(iframe); + + // Make sure to get this pointer after the doc.write as the doc.write + // clobbers all the document contents. + var body = doc.body; + this.setupFieldObject(body); + + if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE && + this.usesIframe()) { + this.turnOnDesignModeGecko(); + } + + this.handleFieldLoad(); +}; + + +/** + * Clears fieldLoadListener for a field. Must be called even (especially?) if + * the field is not yet loaded and therefore not in this.fieldMap_ + * @private + */ +goog.editor.Field.prototype.clearFieldLoadListener_ = function() { + if (this.fieldLoadListenerKey_) { + goog.events.unlistenByKey(this.fieldLoadListenerKey_); + this.fieldLoadListenerKey_ = null; + } +}; + + +/** + * @return {!Object} Get the HTML attributes for this field's iframe. + * @protected + */ +goog.editor.Field.
<TRUNCATED>
