// Copyright 2007 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview A wrapper around a goog.editor.Field * that listens to mouse events on the specified un-editable field, and makes * the field editable if the user clicks on it. Clients are still responsible * for determining when to make the field un-editable again. * * Clients can still determine when the field has loaded by listening to * field's load event. * * @author nicksantos@google.com (Nick Santos) */ goog.provide('goog.editor.ClickToEditWrapper'); goog.require('goog.Disposable'); 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.Field'); goog.require('goog.editor.range'); goog.require('goog.events.BrowserEvent'); goog.require('goog.events.EventHandler'); goog.require('goog.events.EventType'); /** * Initialize the wrapper, and begin listening to mouse events immediately. * @param {goog.editor.Field} fieldObj The editable field being wrapped. * @constructor * @extends {goog.Disposable} */ goog.editor.ClickToEditWrapper = function(fieldObj) { goog.Disposable.call(this); /** * The field this wrapper interacts with. * @type {goog.editor.Field} * @private */ this.fieldObj_ = fieldObj; /** * DOM helper for the field's original element. * @type {goog.dom.DomHelper} * @private */ this.originalDomHelper_ = goog.dom.getDomHelper(fieldObj.getOriginalElement()); /** * @type {goog.dom.SavedCaretRange} * @private */ this.savedCaretRange_ = null; /** * Event handler for field related events. * @type {!goog.events.EventHandler} * @private */ this.fieldEventHandler_ = new goog.events.EventHandler(this); /** * Bound version of the finishMouseUp method. * @type {Function} * @private */ this.finishMouseUpBound_ = goog.bind(this.finishMouseUp_, this); /** * Event handler for mouse events. * @type {!goog.events.EventHandler} * @private */ this.mouseEventHandler_ = new goog.events.EventHandler(this); // Start listening to mouse events immediately if necessary. if (!this.fieldObj_.isLoaded()) { this.enterDocument(); } this.fieldEventHandler_ . // Whenever the field is made editable, we need to check if there // are any carets in it, and if so, use them to render the selection. listen( this.fieldObj_, goog.editor.Field.EventType.LOAD, this.renderSelection_) . // Whenever the field is made uneditable, we need to set up // the click-to-edit listeners. listen( this.fieldObj_, goog.editor.Field.EventType.UNLOAD, this.enterDocument); }; goog.inherits(goog.editor.ClickToEditWrapper, goog.Disposable); /** @return {goog.editor.Field} The field. */ goog.editor.ClickToEditWrapper.prototype.getFieldObject = function() { return this.fieldObj_; }; /** @return {goog.dom.DomHelper} The dom helper of the uneditable element. */ goog.editor.ClickToEditWrapper.prototype.getOriginalDomHelper = function() { return this.originalDomHelper_; }; /** @override */ goog.editor.ClickToEditWrapper.prototype.disposeInternal = function() { goog.editor.ClickToEditWrapper.base(this, 'disposeInternal'); this.exitDocument(); if (this.savedCaretRange_) { this.savedCaretRange_.dispose(); } this.fieldEventHandler_.dispose(); this.mouseEventHandler_.dispose(); this.savedCaretRange_ = null; delete this.fieldEventHandler_; delete this.mouseEventHandler_; }; /** * Initialize listeners when the uneditable field is added to the document. * Also sets up lorem ipsum text. */ goog.editor.ClickToEditWrapper.prototype.enterDocument = function() { if (this.isInDocument_) { return; } this.isInDocument_ = true; this.mouseEventTriggeredLoad_ = false; var field = this.fieldObj_.getOriginalElement(); // To do artificial selection preservation, we have to listen to mouseup, // get the current selection, and re-select the same text in the iframe. // // NOTE(nicksantos): Artificial selection preservation is needed in all cases // where we set the field contents by setting innerHTML. There are a few // rare cases where we don't need it. But these cases are highly // implementation-specific, and computationally hard to detect (bidi // and ig modules both set innerHTML), so we just do it in all cases. this.savedAnchorClicked_ = null; this.mouseEventHandler_ .listen(field, goog.events.EventType.MOUSEUP, this.handleMouseUp_) .listen(field, goog.events.EventType.CLICK, this.handleClick_); // manage lorem ipsum text, if necessary this.fieldObj_.execCommand(goog.editor.Command.UPDATE_LOREM); }; /** * Destroy listeners when the field is removed from the document. */ goog.editor.ClickToEditWrapper.prototype.exitDocument = function() { this.mouseEventHandler_.removeAll(); this.isInDocument_ = false; }; /** * Returns the uneditable field element if the field is not yet editable * (equivalent to EditableField.getOriginalElement()), and the editable DOM * element if the field is currently editable (equivalent to * EditableField.getElement()). * @return {Element} The element containing the editable field contents. */ goog.editor.ClickToEditWrapper.prototype.getElement = function() { return this.fieldObj_.isLoaded() ? this.fieldObj_.getElement() : this.fieldObj_.getOriginalElement(); }; /** * True if a mouse event should be handled, false if it should be ignored. * @param {goog.events.BrowserEvent} e The mouse event. * @return {boolean} Wether or not this mouse event should be handled. * @private */ goog.editor.ClickToEditWrapper.prototype.shouldHandleMouseEvent_ = function(e) { return e.isButton(goog.events.BrowserEvent.MouseButton.LEFT) && !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey); }; /** * Handle mouse click events on the field. * @param {goog.events.BrowserEvent} e The click event. * @private */ goog.editor.ClickToEditWrapper.prototype.handleClick_ = function(e) { // If the user clicked on a link in an uneditable field, // we want to cancel the click. var anchorAncestor = goog.dom.getAncestorByTagNameAndClass( /** @type {Node} */ (e.target), goog.dom.TagName.A); if (anchorAncestor) { e.preventDefault(); if (!goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) { this.savedAnchorClicked_ = anchorAncestor; } } }; /** * Handle a mouse up event on the field. * @param {goog.events.BrowserEvent} e The mouseup event. * @private */ goog.editor.ClickToEditWrapper.prototype.handleMouseUp_ = function(e) { // Only respond to the left mouse button. if (this.shouldHandleMouseEvent_(e)) { // We need to get the selection when the user mouses up, but the // selection doesn't actually change until after the mouseup event has // propagated. So we need to do this asynchronously. this.originalDomHelper_.getWindow().setTimeout(this.finishMouseUpBound_, 0); } }; /** * A helper function for handleMouseUp_ -- does the actual work * when the event is finished propagating. * @private */ goog.editor.ClickToEditWrapper.prototype.finishMouseUp_ = function() { // Make sure that the field is still not editable. if (!this.fieldObj_.isLoaded()) { if (this.savedCaretRange_) { this.savedCaretRange_.dispose(); this.savedCaretRange_ = null; } if (!this.fieldObj_.queryCommandValue(goog.editor.Command.USING_LOREM)) { // We need carets (blank span nodes) to maintain the selection when // the html is copied into an iframe. However, because our code // clears the selection to make the behavior consistent, we need to do // this even when we're not using an iframe. this.insertCarets_(); } this.ensureFieldEditable_(); } this.exitDocument(); this.savedAnchorClicked_ = null; }; /** * Ensure that the field is editable. If the field is not editable, * make it so, and record the fact that it was done by a user mouse event. * @private */ goog.editor.ClickToEditWrapper.prototype.ensureFieldEditable_ = function() { if (!this.fieldObj_.isLoaded()) { this.mouseEventTriggeredLoad_ = true; this.makeFieldEditable(this.fieldObj_); } }; /** * Once the field has loaded in an iframe, re-create the selection * as marked by the carets. * @private */ goog.editor.ClickToEditWrapper.prototype.renderSelection_ = function() { if (this.savedCaretRange_) { // Make sure that the restoration document is inside the iframe // if we're using one. this.savedCaretRange_.setRestorationDocument( this.fieldObj_.getEditableDomHelper().getDocument()); var startCaret = this.savedCaretRange_.getCaret(true); var endCaret = this.savedCaretRange_.getCaret(false); var hasCarets = startCaret && endCaret; } // There are two reasons why we might want to focus the field: // 1) makeFieldEditable was triggered by the click-to-edit wrapper. // In this case, the mouse event should have triggered a focus, but // the editor might have taken the focus away to create lorem ipsum // text or create an iframe for the field. So we make sure the focus // is restored. // 2) somebody placed carets, and we need to select those carets. The field // needs focus to ensure that the selection appears. if (this.mouseEventTriggeredLoad_ || hasCarets) { this.focusOnFieldObj(this.fieldObj_); } if (hasCarets) { this.savedCaretRange_.restore(); this.fieldObj_.dispatchSelectionChangeEvent(); // NOTE(nicksantos): Bubbles aren't actually enabled until the end // if the load sequence, so if the user clicked on a link, the bubble // will not pop up. } if (this.savedCaretRange_) { this.savedCaretRange_.dispose(); this.savedCaretRange_ = null; } this.mouseEventTriggeredLoad_ = false; }; /** * Focus on the field object. * @param {goog.editor.Field} field The field to focus. * @protected */ goog.editor.ClickToEditWrapper.prototype.focusOnFieldObj = function(field) { field.focusAndPlaceCursorAtStart(); }; /** * Make the field object editable. * @param {goog.editor.Field} field The field to make editable. * @protected */ goog.editor.ClickToEditWrapper.prototype.makeFieldEditable = function(field) { field.makeEditable(); }; //================================================================ // Caret-handling methods /** * Gets a saved caret range for the given range. * @param {goog.dom.AbstractRange} range A range wrapper. * @return {goog.dom.SavedCaretRange} The range, saved with carets, or null * if the range wrapper was null. * @private */ goog.editor.ClickToEditWrapper.createCaretRange_ = function(range) { return range && goog.editor.range.saveUsingNormalizedCarets(range); }; /** * Inserts the carets, given the current selection. * * Note that for all practical purposes, a cursor position is just * a selection with the start and end at the same point. * @private */ goog.editor.ClickToEditWrapper.prototype.insertCarets_ = function() { var fieldElement = this.fieldObj_.getOriginalElement(); this.savedCaretRange_ = null; var originalWindow = this.originalDomHelper_.getWindow(); if (goog.dom.Range.hasSelection(originalWindow)) { var range = goog.dom.Range.createFromWindow(originalWindow); range = range && goog.editor.range.narrow(range, fieldElement); this.savedCaretRange_ = goog.editor.ClickToEditWrapper.createCaretRange_(range); } if (!this.savedCaretRange_) { // We couldn't figure out where to put the carets. // But in FF2/IE6+, this could mean that the user clicked on a // 'special' node, (e.g., a link or an unselectable item). So the // selection appears to be null or the full page, even though the user did // click on something. In IE, we can determine the real selection via // document.activeElement. In FF, we have to be more hacky. var specialNodeClicked; if (goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) { specialNodeClicked = goog.dom.getActiveElement(this.originalDomHelper_.getDocument()); } else { specialNodeClicked = this.savedAnchorClicked_; } var isFieldElement = function(node) { return node == fieldElement; }; if (specialNodeClicked && goog.dom.getAncestor(specialNodeClicked, isFieldElement, true)) { // Insert the cursor at the beginning of the active element to be // consistent with the behavior in FF1.5, where clicking on a // link makes the current selection equal to the cursor position // directly before that link. // // TODO(nicksantos): Is there a way to more accurately place the cursor? this.savedCaretRange_ = goog.editor.ClickToEditWrapper.createCaretRange_( goog.dom.Range.createFromNodes( specialNodeClicked, 0, specialNodeClicked, 0)); } } };