Path: blob/trunk/third_party/closure/goog/editor/seamlessfield.js
2868 views
// Copyright 2006 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 Class to encapsulate an editable field that blends in with16* the style of the page. The field can be fixed height, grow with its17* contents, or have a min height after which it grows to its contents.18* This is a goog.editor.Field, but with blending and sizing capabilities,19* and avoids using an iframe whenever possible.20*21* @author [email protected] (Nick Santos)22* @see ../demos/editor/seamlessfield.html23*/242526goog.provide('goog.editor.SeamlessField');2728goog.require('goog.cssom.iframe.style');29goog.require('goog.dom');30goog.require('goog.dom.Range');31goog.require('goog.dom.TagName');32goog.require('goog.dom.safe');33goog.require('goog.editor.BrowserFeature');34goog.require('goog.editor.Field');35goog.require('goog.editor.icontent');36goog.require('goog.editor.icontent.FieldFormatInfo');37goog.require('goog.editor.icontent.FieldStyleInfo');38goog.require('goog.editor.node');39goog.require('goog.events');40goog.require('goog.events.EventType');41goog.require('goog.html.legacyconversions');42goog.require('goog.html.uncheckedconversions');43goog.require('goog.log');44goog.require('goog.string.Const');45goog.require('goog.style');46474849/**50* This class encapsulates an editable field that blends in with the51* surrounding page.52* To see events fired by this object, please see the base class.53*54* @param {string} id An identifer for the field. This is used to find the55* field and the element associated with this field.56* @param {Document=} opt_doc The document that the element with the given57* id can be found it.58* @constructor59* @extends {goog.editor.Field}60*/61goog.editor.SeamlessField = function(id, opt_doc) {62goog.editor.Field.call(this, id, opt_doc);63};64goog.inherits(goog.editor.SeamlessField, goog.editor.Field);656667/**68* @override69*/70goog.editor.SeamlessField.prototype.logger =71goog.log.getLogger('goog.editor.SeamlessField');7273// Functions dealing with field sizing.747576/**77* The key used for listening for the "dragover" event.78* @type {goog.events.Key}79* @private80*/81goog.editor.SeamlessField.prototype.listenForDragOverEventKey_;828384/**85* The key used for listening for the iframe "load" event.86* @type {goog.events.Key}87* @private88*/89goog.editor.SeamlessField.prototype.listenForIframeLoadEventKey_;909192/**93* Sets the min height of this editable field's iframe. Only used in growing94* mode when an iframe is used. This will cause an immediate field sizing to95* update the field if necessary based on the new min height.96* @param {number} height The min height specified as a number of pixels,97* e.g., 75.98*/99goog.editor.SeamlessField.prototype.setMinHeight = function(height) {100if (height == this.minHeight_) {101// Do nothing if the min height isn't changing.102return;103}104this.minHeight_ = height;105if (this.usesIframe()) {106this.doFieldSizingGecko();107}108};109110111/**112* Whether the field should be rendered with a fixed height, or should expand113* to fit its contents.114* @type {boolean}115* @private116*/117goog.editor.SeamlessField.prototype.isFixedHeight_ = false;118119120/**121* Whether the fixed-height handling has been overridden manually.122* @type {boolean}123* @private124*/125goog.editor.SeamlessField.prototype.isFixedHeightOverridden_ = false;126127128/**129* @return {boolean} Whether the field should be rendered with a fixed130* height, or should expand to fit its contents.131* @override132*/133goog.editor.SeamlessField.prototype.isFixedHeight = function() {134return this.isFixedHeight_;135};136137138/**139* @param {boolean} newVal Explicitly set whether the field should be140* of a fixed-height. This overrides auto-detection.141*/142goog.editor.SeamlessField.prototype.overrideFixedHeight = function(newVal) {143this.isFixedHeight_ = newVal;144this.isFixedHeightOverridden_ = true;145};146147148/**149* Auto-detect whether the current field should have a fixed height.150* @private151*/152goog.editor.SeamlessField.prototype.autoDetectFixedHeight_ = function() {153if (!this.isFixedHeightOverridden_) {154var originalElement = this.getOriginalElement();155if (originalElement) {156this.isFixedHeight_ =157goog.style.getComputedOverflowY(originalElement) == 'auto';158}159}160};161162163/**164* Resize the iframe in response to the wrapper div changing size.165* @private166*/167goog.editor.SeamlessField.prototype.handleOuterDocChange_ = function() {168if (this.isEventStopped(goog.editor.Field.EventType.CHANGE)) {169return;170}171this.sizeIframeToWrapperGecko_();172};173174175/**176* Sizes the iframe to its body's height.177* @private178*/179goog.editor.SeamlessField.prototype.sizeIframeToBodyHeightGecko_ = function() {180if (this.acquireSizeIframeLockGecko_()) {181var resized = false;182var ifr = this.getEditableIframe();183if (ifr) {184var fieldHeight = this.getIframeBodyHeightGecko_();185186if (this.minHeight_) {187fieldHeight = Math.max(fieldHeight, this.minHeight_);188}189if (parseInt(goog.style.getStyle(ifr, 'height'), 10) != fieldHeight) {190ifr.style.height = fieldHeight + 'px';191resized = true;192}193}194this.releaseSizeIframeLockGecko_();195if (resized) {196this.dispatchEvent(goog.editor.Field.EventType.IFRAME_RESIZED);197}198}199};200201202/**203* @return {number} The height of the editable iframe's body.204* @private205*/206goog.editor.SeamlessField.prototype.getIframeBodyHeightGecko_ = function() {207var ifr = this.getEditableIframe();208var body = ifr.contentDocument.body;209var htmlElement = /** @type {!HTMLElement} */ (body.parentNode);210211212// If the iframe's height is 0, then the offsetHeight/scrollHeight of the213// HTML element in the iframe can be totally wack (i.e. too large214// by 50-500px). Also, in standard's mode the clientHeight is 0.215if (parseInt(goog.style.getStyle(ifr, 'height'), 10) === 0) {216goog.style.setStyle(ifr, 'height', 1 + 'px');217}218219var fieldHeight;220if (goog.editor.node.isStandardsMode(body)) {221// If in standards-mode,222// grab the HTML element as it will contain all the field's223// contents. The body's height, for example, will not include that of224// floated images at the bottom in standards mode.225// Note that this value include all scrollbars *except* for scrollbars226// on the HTML element itself.227fieldHeight = htmlElement.offsetHeight;228} else {229// In quirks-mode, the body-element always seems230// to size to the containing window. The html-element however,231// sizes to the content, and can thus end up with a value smaller232// than its child body-element if the content is shrinking.233// We want to make the iframe shrink too when the content shrinks,234// so rather than size the iframe to the body-element, size it to235// the html-element.236fieldHeight = htmlElement.scrollHeight;237238// If there is a horizontal scroll, add in the thickness of the239// scrollbar.240if (htmlElement.clientHeight != htmlElement.offsetHeight) {241fieldHeight += goog.editor.SeamlessField.getScrollbarWidth_();242}243}244245return fieldHeight;246};247248249/**250* Grabs the width of a scrollbar from the browser and caches the result.251* @return {number} The scrollbar width in pixels.252* @private253*/254goog.editor.SeamlessField.getScrollbarWidth_ = function() {255return goog.editor.SeamlessField.scrollbarWidth_ ||256(goog.editor.SeamlessField.scrollbarWidth_ =257goog.style.getScrollbarWidth());258};259260261/**262* Sizes the iframe to its container div's width. The width of the div263* is controlled by its containing context, not by its contents.264* if it extends outside of it's contents, then it gets a horizontal scroll.265* @private266*/267goog.editor.SeamlessField.prototype.sizeIframeToWrapperGecko_ = function() {268if (this.acquireSizeIframeLockGecko_()) {269var ifr = this.getEditableIframe();270var field = this.getElement();271var resized = false;272if (ifr && field) {273var fieldPaddingBox;274var widthDiv = /** @type {!HTMLElement} */ (ifr.parentNode);275276var width = widthDiv.offsetWidth;277if (parseInt(goog.style.getStyle(ifr, 'width'), 10) != width) {278fieldPaddingBox = goog.style.getPaddingBox(field);279ifr.style.width = width + 'px';280field.style.width =281width - fieldPaddingBox.left - fieldPaddingBox.right + 'px';282resized = true;283}284285var height = widthDiv.offsetHeight;286if (this.isFixedHeight() &&287parseInt(goog.style.getStyle(ifr, 'height'), 10) != height) {288if (!fieldPaddingBox) {289fieldPaddingBox = goog.style.getPaddingBox(field);290}291ifr.style.height = height + 'px';292field.style.height =293height - fieldPaddingBox.top - fieldPaddingBox.bottom + 'px';294resized = true;295}296}297this.releaseSizeIframeLockGecko_();298if (resized) {299this.dispatchEvent(goog.editor.Field.EventType.IFRAME_RESIZED);300}301}302};303304305/**306* Perform all the sizing immediately.307*/308goog.editor.SeamlessField.prototype.doFieldSizingGecko = function() {309// Because doFieldSizingGecko can be called after a setTimeout310// it is possible that the field has been destroyed before this call311// to do the sizing is executed. Check for field existence and do nothing312// if it has already been destroyed.313if (this.getElement()) {314// The order of operations is important here. Sizing the iframe to the315// wrapper could cause the width to change, which could change the line316// wrapping, which could change the body height. So we need to do that317// first, then size the iframe to fit the body height.318this.sizeIframeToWrapperGecko_();319if (!this.isFixedHeight()) {320this.sizeIframeToBodyHeightGecko_();321}322}323};324325326/**327* Acquires a lock on resizing the field iframe. This is used to ensure that328* modifications we make while in a mutation event handler don't cause329* infinite loops.330* @return {boolean} False if the lock is already acquired.331* @private332*/333goog.editor.SeamlessField.prototype.acquireSizeIframeLockGecko_ = function() {334if (this.sizeIframeLock_) {335return false;336}337return this.sizeIframeLock_ = true;338};339340341/**342* Releases a lock on resizing the field iframe. This is used to ensure that343* modifications we make while in a mutation event handler don't cause344* infinite loops.345* @private346*/347goog.editor.SeamlessField.prototype.releaseSizeIframeLockGecko_ = function() {348this.sizeIframeLock_ = false;349};350351352// Functions dealing with blending in with the surrounding page.353354355/**356* String containing the css rules that, if applied to a document's body,357* would style that body as if it were the original element we made editable.358* See goog.cssom.iframe.style.getElementContext for more details.359* @type {string}360* @private361*/362goog.editor.SeamlessField.prototype.iframeableCss_ = '';363364365/**366* Gets the css rules that should be used to style an iframe's body as if it367* were the original element that we made editable.368* @param {boolean=} opt_forceRegeneration Set to true to not read the cached369* copy and instead completely regenerate the css rules.370* @return {string} The string containing the css rules to use.371*/372goog.editor.SeamlessField.prototype.getIframeableCss = function(373opt_forceRegeneration) {374if (!this.iframeableCss_ || opt_forceRegeneration) {375var originalElement = this.getOriginalElement();376if (originalElement) {377this.iframeableCss_ = goog.cssom.iframe.style.getElementContext(378originalElement, opt_forceRegeneration);379}380}381return this.iframeableCss_;382};383384385/**386* Sets the css rules that should be used inside the editable iframe.387* Note: to clear the css cache between makeNotEditable/makeEditable,388* call this with "" as iframeableCss.389* TODO(user): Unify all these css setting methods + Nick's open390* CL. This is getting ridiculous.391* @param {string} iframeableCss String containing the css rules to use.392*/393goog.editor.SeamlessField.prototype.setIframeableCss = function(iframeableCss) {394this.iframeableCss_ = iframeableCss;395};396397398/**399* Used to ensure that CSS stylings are only installed once for none400* iframe seamless mode.401* TODO(user): Make it a formal part of the API that you can only402* set one set of styles globally.403* In seamless, non-iframe mode, all the stylings would go in the404* same document and conflict.405* @type {boolean}406* @private407*/408goog.editor.SeamlessField.haveInstalledCss_ = false;409410411/**412* Applies CSS from the wrapper-div to the field iframe.413*/414goog.editor.SeamlessField.prototype.inheritBlendedCSS = function() {415// No-op if the field isn't using an iframe.416if (!this.usesIframe()) {417return;418}419var field = this.getElement();420var head = goog.dom.getDomHelper(field).getElementsByTagNameAndClass(421goog.dom.TagName.HEAD)[0];422if (head) {423// We created this <head>, and we know the only thing we put in there424// is a <style> block. So it's safe to blow away all the children425// as part of rewriting the styles.426goog.dom.removeChildren(head);427}428429// Force a cache-clearing in CssUtil - this function was called because430// we're applying the 'blend' for the first time, or because we431// *need* to recompute the blend.432var newCSS = this.getIframeableCss(true);433goog.style.installSafeStyleSheet(434goog.html.legacyconversions.safeStyleSheetFromString(newCSS), field);435};436437438// Overridden methods.439440441/** @override */442goog.editor.SeamlessField.prototype.usesIframe = function() {443// TODO(user): Switch Firefox to using contentEditable444// rather than designMode iframe once contentEditable support445// is less buggy.446return !goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE;447};448449450/** @override */451goog.editor.SeamlessField.prototype.setupMutationEventHandlersGecko =452function() {453goog.editor.SeamlessField.superClass_.setupMutationEventHandlersGecko.call(454this);455456if (this.usesIframe()) {457var iframe = this.getEditableIframe();458var outerDoc = iframe.ownerDocument;459this.eventRegister.listen(460outerDoc, goog.editor.Field.MUTATION_EVENTS_GECKO,461this.handleOuterDocChange_, true);462463// If the images load after we do the initial sizing, then this will464// force a field resize.465this.listenForIframeLoadEventKey_ = goog.events.listenOnce(466this.getEditableDomHelper().getWindow(), goog.events.EventType.LOAD,467this.sizeIframeToBodyHeightGecko_, true, this);468469this.eventRegister.listen(470outerDoc, 'DOMAttrModified',471goog.bind(this.handleDomAttrChange, this, this.handleOuterDocChange_),472true);473}474};475476477/** @override */478goog.editor.SeamlessField.prototype.handleChange = function() {479if (this.isEventStopped(goog.editor.Field.EventType.CHANGE)) {480return;481}482483goog.editor.SeamlessField.superClass_.handleChange.call(this);484485if (this.usesIframe()) {486this.sizeIframeToBodyHeightGecko_();487}488};489490491/** @override */492goog.editor.SeamlessField.prototype.dispatchBlur = function() {493if (this.isEventStopped(goog.editor.Field.EventType.BLUR)) {494return;495}496497goog.editor.SeamlessField.superClass_.dispatchBlur.call(this);498499// Clear the selection and restore the current range back after collapsing500// it. The ideal solution would have been to just leave the range intact; but501// when there are multiple fields present on the page, its important that502// the selection isn't retained when we switch between the fields. We also503// have to make sure that the cursor position is retained when we tab in and504// out of a field and our approach addresses both these issues.505// Another point to note is that we do it on a setTimeout to allow for506// DOM modifications on blur. Otherwise, something like setLoremIpsum will507// leave a blinking cursor in the field even though it's blurred.508if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE &&509!goog.editor.BrowserFeature.CLEARS_SELECTION_WHEN_FOCUS_LEAVES) {510var win = this.getEditableDomHelper().getWindow();511var dragging = false;512goog.events.unlistenByKey(this.listenForDragOverEventKey_);513this.listenForDragOverEventKey_ = goog.events.listenOnce(514win.document.body, 'dragover', function() { dragging = true; });515goog.global.setTimeout(goog.bind(function() {516// Do not clear the selection if we're only dragging text.517// This addresses a bug on FF1.5/linux where dragging fires a blur,518// but clearing the selection confuses Firefox's drag-and-drop519// implementation. For more info, see http://b/1061064520if (!dragging) {521if (this.editableDomHelper) {522var rng = this.getRange();523524// If there are multiple fields on a page, we need to make sure that525// the selection isn't retained when we switch between fields. We526// could have collapsed the range but there is a bug in GECKO where527// the selection stays highlighted even though its backing range is528// collapsed (http://b/1390115). To get around this, we clear the529// selection and restore the collapsed range back in. Restoring the530// range is important so that the cursor stays intact when we tab out531// and into a field (See http://b/1790301 for additional details on532// this).533var iframeWindow = this.editableDomHelper.getWindow();534goog.dom.Range.clearSelection(iframeWindow);535536if (rng) {537rng.collapse(true);538rng.select();539}540}541}542}, this), 0);543}544};545546547/** @override */548goog.editor.SeamlessField.prototype.turnOnDesignModeGecko = function() {549goog.editor.SeamlessField.superClass_.turnOnDesignModeGecko.call(this);550var doc = this.getEditableDomHelper().getDocument();551552doc.execCommand('enableInlineTableEditing', false, 'false');553doc.execCommand('enableObjectResizing', false, 'false');554};555556557/** @override */558goog.editor.SeamlessField.prototype.installStyles = function() {559if (!this.usesIframe()) {560if (!goog.editor.SeamlessField.haveInstalledCss_) {561if (this.cssStyles) {562goog.style.installSafeStyleSheet(563goog.html.legacyconversions.safeStyleSheetFromString(564this.cssStyles),565this.getElement());566}567568// TODO(user): this should be reset to false when the editor is quit.569// In non-iframe mode, CSS styles should only be instaled once.570goog.editor.SeamlessField.haveInstalledCss_ = true;571}572}573};574575576/** @override */577goog.editor.SeamlessField.prototype.makeEditableInternal = function(578opt_iframeSrc) {579if (this.usesIframe()) {580goog.editor.SeamlessField.superClass_.makeEditableInternal.call(581this, opt_iframeSrc);582} else {583var field = this.getOriginalElement();584if (field) {585this.setupFieldObject(field);586field.contentEditable = true;587588this.injectContents(field.innerHTML, field);589590this.handleFieldLoad();591}592}593};594595596/** @override */597goog.editor.SeamlessField.prototype.handleFieldLoad = function() {598if (this.usesIframe()) {599// If the CSS inheriting code screws up (e.g. makes fonts too large) and600// the field is sized off in goog.editor.Field.makeIframeField, then we need601// to size it correctly, but it needs to be visible for the browser602// to have fully rendered it. We need to put this on a timeout to give603// the browser time to render.604var self = this;605goog.global.setTimeout(function() { self.doFieldSizingGecko(); }, 0);606}607goog.editor.SeamlessField.superClass_.handleFieldLoad.call(this);608};609610611/** @override */612goog.editor.SeamlessField.prototype.getIframeAttributes = function() {613return {'frameBorder': 0, 'style': 'padding:0;'};614};615616617/** @override */618goog.editor.SeamlessField.prototype.attachIframe = function(iframe) {619this.autoDetectFixedHeight_();620var field = this.getOriginalElement();621var dh = goog.dom.getDomHelper(field);622623// Grab the width/height values of the field before modifying any CSS624// as some of the modifications affect its size (e.g. innerHTML='')625// Here, we set the size of the field to fixed so there's not too much626// jiggling when we set the innerHTML of the field.627var oldWidth = field.style.width;628var oldHeight = field.style.height;629goog.style.setStyle(field, 'visibility', 'hidden');630631// If there is a floated element at the bottom of the field,632// then it needs a clearing div at the end to cause the clientHeight633// to contain the entire field.634// Also, with css re-writing, the margins of the first/last635// paragraph don't seem to get included in the clientHeight. Specifically,636// the extra divs below force the field's clientHeight to include the637// margins on the first and last elements contained within it.638var startDiv = dh.createDom(639goog.dom.TagName.DIV,640{'style': 'height:0;clear:both', 'innerHTML': ' '});641var endDiv = startDiv.cloneNode(true);642field.insertBefore(startDiv, field.firstChild);643goog.dom.appendChild(field, endDiv);644645var contentBox = goog.style.getContentBoxSize(field);646var width = contentBox.width;647var height = contentBox.height;648649var html = '';650if (this.isFixedHeight()) {651html = ' ';652653goog.style.setStyle(field, 'position', 'relative');654goog.style.setStyle(field, 'overflow', 'visible');655656goog.style.setStyle(iframe, 'position', 'absolute');657goog.style.setStyle(iframe, 'top', '0');658goog.style.setStyle(iframe, 'left', '0');659}660goog.style.setSize(field, width, height);661662// In strict mode, browsers put blank space at the bottom and right663// if a field when it has an iframe child, to fill up the remaining line664// height. So make the line height = 0.665if (goog.editor.node.isStandardsMode(field)) {666this.originalFieldLineHeight_ = field.style.lineHeight;667goog.style.setStyle(field, 'lineHeight', '0');668}669670goog.editor.node.replaceInnerHtml(field, html);671// Set the initial size672goog.style.setSize(iframe, width, height);673goog.style.setSize(field, oldWidth, oldHeight);674goog.style.setStyle(field, 'visibility', '');675goog.dom.appendChild(field, iframe);676677// Only write if its not IE HTTPS in which case we're waiting for load.678if (!this.shouldLoadAsynchronously()) {679var doc = iframe.contentWindow.document;680if (goog.editor.node.isStandardsMode(iframe.ownerDocument)) {681doc.open();682var emptyHtml =683goog.html.uncheckedconversions684.safeHtmlFromStringKnownToSatisfyTypeContract(685goog.string.Const.from('HTML from constant string'),686'<!DOCTYPE HTML><html></html>');687goog.dom.safe.documentWrite(doc, emptyHtml);688doc.close();689}690}691};692693694/** @override */695goog.editor.SeamlessField.prototype.getFieldFormatInfo = function(extraStyles) {696var originalElement = this.getOriginalElement();697if (originalElement) {698return new goog.editor.icontent.FieldFormatInfo(699this.id, goog.editor.node.isStandardsMode(originalElement), true,700this.isFixedHeight(), extraStyles);701}702throw Error('no field');703};704705706/** @override */707goog.editor.SeamlessField.prototype.writeIframeContent = function(708iframe, innerHtml, extraStyles) {709// For seamless iframes, hide the iframe while we're laying it out to710// prevent the flicker.711goog.style.setStyle(iframe, 'visibility', 'hidden');712var formatInfo = this.getFieldFormatInfo(extraStyles);713var styleInfo = new goog.editor.icontent.FieldStyleInfo(714this.getOriginalElement(), this.cssStyles + this.getIframeableCss());715goog.editor.icontent.writeNormalInitialBlendedIframe(716formatInfo, innerHtml, styleInfo, iframe);717this.doFieldSizingGecko();718goog.style.setStyle(iframe, 'visibility', 'visible');719};720721722/** @override */723goog.editor.SeamlessField.prototype.restoreDom = function() {724// TODO(user): Consider only removing the iframe if we are725// restoring the original node.726if (this.usesIframe()) {727goog.dom.removeNode(this.getEditableIframe());728}729};730731732/** @override */733goog.editor.SeamlessField.prototype.clearListeners = function() {734goog.events.unlistenByKey(this.listenForDragOverEventKey_);735goog.events.unlistenByKey(this.listenForIframeLoadEventKey_);736737goog.editor.SeamlessField.base(this, 'clearListeners');738};739740741