Path: blob/trunk/third_party/closure/goog/editor/clicktoeditwrapper.js
2868 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview A wrapper around a goog.editor.Field16* that listens to mouse events on the specified un-editable field, and makes17* the field editable if the user clicks on it. Clients are still responsible18* for determining when to make the field un-editable again.19*20* Clients can still determine when the field has loaded by listening to21* field's load event.22*23* @author [email protected] (Nick Santos)24*/2526goog.provide('goog.editor.ClickToEditWrapper');2728goog.require('goog.Disposable');29goog.require('goog.dom');30goog.require('goog.dom.Range');31goog.require('goog.dom.TagName');32goog.require('goog.editor.BrowserFeature');33goog.require('goog.editor.Command');34goog.require('goog.editor.Field');35goog.require('goog.editor.range');36goog.require('goog.events.BrowserEvent');37goog.require('goog.events.EventHandler');38goog.require('goog.events.EventType');39404142/**43* Initialize the wrapper, and begin listening to mouse events immediately.44* @param {goog.editor.Field} fieldObj The editable field being wrapped.45* @constructor46* @extends {goog.Disposable}47*/48goog.editor.ClickToEditWrapper = function(fieldObj) {49goog.Disposable.call(this);5051/**52* The field this wrapper interacts with.53* @type {goog.editor.Field}54* @private55*/56this.fieldObj_ = fieldObj;5758/**59* DOM helper for the field's original element.60* @type {goog.dom.DomHelper}61* @private62*/63this.originalDomHelper_ =64goog.dom.getDomHelper(fieldObj.getOriginalElement());6566/**67* @type {goog.dom.SavedCaretRange}68* @private69*/70this.savedCaretRange_ = null;7172/**73* Event handler for field related events.74* @type {!goog.events.EventHandler<!goog.editor.ClickToEditWrapper>}75* @private76*/77this.fieldEventHandler_ = new goog.events.EventHandler(this);7879/**80* Bound version of the finishMouseUp method.81* @type {Function}82* @private83*/84this.finishMouseUpBound_ = goog.bind(this.finishMouseUp_, this);8586/**87* Event handler for mouse events.88* @type {!goog.events.EventHandler<!goog.editor.ClickToEditWrapper>}89* @private90*/91this.mouseEventHandler_ = new goog.events.EventHandler(this);9293// Start listening to mouse events immediately if necessary.94if (!this.fieldObj_.isLoaded()) {95this.enterDocument();96}9798this.fieldEventHandler_99.100// Whenever the field is made editable, we need to check if there101// are any carets in it, and if so, use them to render the selection.102listen(103this.fieldObj_, goog.editor.Field.EventType.LOAD,104this.renderSelection_)105.106// Whenever the field is made uneditable, we need to set up107// the click-to-edit listeners.108listen(109this.fieldObj_, goog.editor.Field.EventType.UNLOAD,110this.enterDocument);111};112goog.inherits(goog.editor.ClickToEditWrapper, goog.Disposable);113114115116/** @return {goog.editor.Field} The field. */117goog.editor.ClickToEditWrapper.prototype.getFieldObject = function() {118return this.fieldObj_;119};120121122/** @return {goog.dom.DomHelper} The dom helper of the uneditable element. */123goog.editor.ClickToEditWrapper.prototype.getOriginalDomHelper = function() {124return this.originalDomHelper_;125};126127128/** @override */129goog.editor.ClickToEditWrapper.prototype.disposeInternal = function() {130goog.editor.ClickToEditWrapper.base(this, 'disposeInternal');131this.exitDocument();132133if (this.savedCaretRange_) {134this.savedCaretRange_.dispose();135}136137this.fieldEventHandler_.dispose();138this.mouseEventHandler_.dispose();139this.savedCaretRange_ = null;140delete this.fieldEventHandler_;141delete this.mouseEventHandler_;142};143144145/**146* Initialize listeners when the uneditable field is added to the document.147* Also sets up lorem ipsum text.148*/149goog.editor.ClickToEditWrapper.prototype.enterDocument = function() {150if (this.isInDocument_) {151return;152}153154this.isInDocument_ = true;155156this.mouseEventTriggeredLoad_ = false;157var field = this.fieldObj_.getOriginalElement();158159// To do artificial selection preservation, we have to listen to mouseup,160// get the current selection, and re-select the same text in the iframe.161//162// NOTE(nicksantos): Artificial selection preservation is needed in all cases163// where we set the field contents by setting innerHTML. There are a few164// rare cases where we don't need it. But these cases are highly165// implementation-specific, and computationally hard to detect (bidi166// and ig modules both set innerHTML), so we just do it in all cases.167this.savedAnchorClicked_ = null;168this.mouseEventHandler_169.listen(field, goog.events.EventType.MOUSEUP, this.handleMouseUp_)170.listen(field, goog.events.EventType.CLICK, this.handleClick_);171172// manage lorem ipsum text, if necessary173this.fieldObj_.execCommand(goog.editor.Command.UPDATE_LOREM);174};175176177/**178* Destroy listeners when the field is removed from the document.179*/180goog.editor.ClickToEditWrapper.prototype.exitDocument = function() {181this.mouseEventHandler_.removeAll();182this.isInDocument_ = false;183};184185186/**187* Returns the uneditable field element if the field is not yet editable188* (equivalent to EditableField.getOriginalElement()), and the editable DOM189* element if the field is currently editable (equivalent to190* EditableField.getElement()).191* @return {Element} The element containing the editable field contents.192*/193goog.editor.ClickToEditWrapper.prototype.getElement = function() {194return this.fieldObj_.isLoaded() ? this.fieldObj_.getElement() :195this.fieldObj_.getOriginalElement();196};197198199/**200* True if a mouse event should be handled, false if it should be ignored.201* @param {goog.events.BrowserEvent} e The mouse event.202* @return {boolean} Wether or not this mouse event should be handled.203* @private204*/205goog.editor.ClickToEditWrapper.prototype.shouldHandleMouseEvent_ = function(e) {206return e.isButton(goog.events.BrowserEvent.MouseButton.LEFT) &&207!(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey);208};209210211/**212* Handle mouse click events on the field.213* @param {goog.events.BrowserEvent} e The click event.214* @private215*/216goog.editor.ClickToEditWrapper.prototype.handleClick_ = function(e) {217// If the user clicked on a link in an uneditable field,218// we want to cancel the click.219var anchorAncestor = goog.dom.getAncestorByTagNameAndClass(220/** @type {Node} */ (e.target), goog.dom.TagName.A);221if (anchorAncestor) {222e.preventDefault();223224if (!goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) {225this.savedAnchorClicked_ = anchorAncestor;226}227}228};229230231/**232* Handle a mouse up event on the field.233* @param {goog.events.BrowserEvent} e The mouseup event.234* @private235*/236goog.editor.ClickToEditWrapper.prototype.handleMouseUp_ = function(e) {237// Only respond to the left mouse button.238if (this.shouldHandleMouseEvent_(e)) {239// We need to get the selection when the user mouses up, but the240// selection doesn't actually change until after the mouseup event has241// propagated. So we need to do this asynchronously.242this.originalDomHelper_.getWindow().setTimeout(this.finishMouseUpBound_, 0);243}244};245246247/**248* A helper function for handleMouseUp_ -- does the actual work249* when the event is finished propagating.250* @private251*/252goog.editor.ClickToEditWrapper.prototype.finishMouseUp_ = function() {253// Make sure that the field is still not editable.254if (!this.fieldObj_.isLoaded()) {255if (this.savedCaretRange_) {256this.savedCaretRange_.dispose();257this.savedCaretRange_ = null;258}259260if (!this.fieldObj_.queryCommandValue(goog.editor.Command.USING_LOREM)) {261// We need carets (blank span nodes) to maintain the selection when262// the html is copied into an iframe. However, because our code263// clears the selection to make the behavior consistent, we need to do264// this even when we're not using an iframe.265this.insertCarets_();266}267268this.ensureFieldEditable_();269}270271this.exitDocument();272this.savedAnchorClicked_ = null;273};274275276/**277* Ensure that the field is editable. If the field is not editable,278* make it so, and record the fact that it was done by a user mouse event.279* @private280*/281goog.editor.ClickToEditWrapper.prototype.ensureFieldEditable_ = function() {282if (!this.fieldObj_.isLoaded()) {283this.mouseEventTriggeredLoad_ = true;284this.makeFieldEditable(this.fieldObj_);285}286};287288289/**290* Once the field has loaded in an iframe, re-create the selection291* as marked by the carets.292* @private293*/294goog.editor.ClickToEditWrapper.prototype.renderSelection_ = function() {295if (this.savedCaretRange_) {296// Make sure that the restoration document is inside the iframe297// if we're using one.298this.savedCaretRange_.setRestorationDocument(299this.fieldObj_.getEditableDomHelper().getDocument());300301var startCaret = this.savedCaretRange_.getCaret(true);302var endCaret = this.savedCaretRange_.getCaret(false);303var hasCarets = startCaret && endCaret;304}305306// There are two reasons why we might want to focus the field:307// 1) makeFieldEditable was triggered by the click-to-edit wrapper.308// In this case, the mouse event should have triggered a focus, but309// the editor might have taken the focus away to create lorem ipsum310// text or create an iframe for the field. So we make sure the focus311// is restored.312// 2) somebody placed carets, and we need to select those carets. The field313// needs focus to ensure that the selection appears.314if (this.mouseEventTriggeredLoad_ || hasCarets) {315this.focusOnFieldObj(this.fieldObj_);316}317318if (hasCarets) {319this.savedCaretRange_.restore();320this.fieldObj_.dispatchSelectionChangeEvent();321322// NOTE(nicksantos): Bubbles aren't actually enabled until the end323// if the load sequence, so if the user clicked on a link, the bubble324// will not pop up.325}326327if (this.savedCaretRange_) {328this.savedCaretRange_.dispose();329this.savedCaretRange_ = null;330}331332this.mouseEventTriggeredLoad_ = false;333};334335336/**337* Focus on the field object.338* @param {goog.editor.Field} field The field to focus.339* @protected340*/341goog.editor.ClickToEditWrapper.prototype.focusOnFieldObj = function(field) {342field.focusAndPlaceCursorAtStart();343};344345346/**347* Make the field object editable.348* @param {goog.editor.Field} field The field to make editable.349* @protected350*/351goog.editor.ClickToEditWrapper.prototype.makeFieldEditable = function(field) {352field.makeEditable();353};354355356//================================================================357// Caret-handling methods358359360/**361* Gets a saved caret range for the given range.362* @param {goog.dom.AbstractRange} range A range wrapper.363* @return {goog.dom.SavedCaretRange} The range, saved with carets, or null364* if the range wrapper was null.365* @private366*/367goog.editor.ClickToEditWrapper.createCaretRange_ = function(range) {368return range && goog.editor.range.saveUsingNormalizedCarets(range);369};370371372/**373* Inserts the carets, given the current selection.374*375* Note that for all practical purposes, a cursor position is just376* a selection with the start and end at the same point.377* @private378*/379goog.editor.ClickToEditWrapper.prototype.insertCarets_ = function() {380var fieldElement = this.fieldObj_.getOriginalElement();381382this.savedCaretRange_ = null;383var originalWindow = this.originalDomHelper_.getWindow();384if (goog.dom.Range.hasSelection(originalWindow)) {385var range = goog.dom.Range.createFromWindow(originalWindow);386range = range && goog.editor.range.narrow(range, fieldElement);387this.savedCaretRange_ =388goog.editor.ClickToEditWrapper.createCaretRange_(range);389}390391if (!this.savedCaretRange_) {392// We couldn't figure out where to put the carets.393// But in FF2/IE6+, this could mean that the user clicked on a394// 'special' node, (e.g., a link or an unselectable item). So the395// selection appears to be null or the full page, even though the user did396// click on something. In IE, we can determine the real selection via397// document.activeElement. In FF, we have to be more hacky.398var specialNodeClicked;399if (goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) {400specialNodeClicked =401goog.dom.getActiveElement(this.originalDomHelper_.getDocument());402} else {403specialNodeClicked = this.savedAnchorClicked_;404}405406var isFieldElement = function(node) { return node == fieldElement; };407if (specialNodeClicked &&408goog.dom.getAncestor(specialNodeClicked, isFieldElement, true)) {409// Insert the cursor at the beginning of the active element to be410// consistent with the behavior in FF1.5, where clicking on a411// link makes the current selection equal to the cursor position412// directly before that link.413//414// TODO(nicksantos): Is there a way to more accurately place the cursor?415this.savedCaretRange_ = goog.editor.ClickToEditWrapper.createCaretRange_(416goog.dom.Range.createFromNodes(417specialNodeClicked, 0, specialNodeClicked, 0));418}419}420};421422423