http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/tagonenterhandler.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/tagonenterhandler.js b/externs/GCL/externs/goog/editor/plugins/tagonenterhandler.js new file mode 100644 index 0000000..e5776fd --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/tagonenterhandler.js @@ -0,0 +1,744 @@ +// Copyright 2008 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 TrogEdit plugin to handle enter keys by inserting the + * specified block level tag. + * + * @author [email protected] (Robby Walker) + */ + +goog.provide('goog.editor.plugins.TagOnEnterHandler'); + +goog.require('goog.dom'); +goog.require('goog.dom.NodeType'); +goog.require('goog.dom.Range'); +goog.require('goog.dom.TagName'); +goog.require('goog.editor.Command'); +goog.require('goog.editor.node'); +goog.require('goog.editor.plugins.EnterHandler'); +goog.require('goog.editor.range'); +goog.require('goog.editor.style'); +goog.require('goog.events.KeyCodes'); +goog.require('goog.functions'); +goog.require('goog.string.Unicode'); +goog.require('goog.style'); +goog.require('goog.userAgent'); + + + +/** + * Plugin to handle enter keys. This subclass normalizes all browsers to use + * the given block tag on enter. + * @param {goog.dom.TagName} tag The type of tag to add on enter. + * @constructor + * @extends {goog.editor.plugins.EnterHandler} + */ +goog.editor.plugins.TagOnEnterHandler = function(tag) { + this.tag = tag; + + goog.editor.plugins.EnterHandler.call(this); +}; +goog.inherits(goog.editor.plugins.TagOnEnterHandler, + goog.editor.plugins.EnterHandler); + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.getTrogClassId = function() { + return 'TagOnEnterHandler'; +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.getNonCollapsingBlankHtml = + function() { + if (this.tag == goog.dom.TagName.P) { + return '<p> </p>'; + } else if (this.tag == goog.dom.TagName.DIV) { + return '<div><br></div>'; + } + return '<br>'; +}; + + +/** + * This plugin is active on uneditable fields so it can provide a value for + * queryCommandValue calls asking for goog.editor.Command.BLOCKQUOTE. + * @return {boolean} True. + * @override + */ +goog.editor.plugins.TagOnEnterHandler.prototype.activeOnUneditableFields = + goog.functions.TRUE; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.isSupportedCommand = function( + command) { + return command == goog.editor.Command.DEFAULT_TAG; +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.queryCommandValue = function( + command) { + return command == goog.editor.Command.DEFAULT_TAG ? this.tag : null; +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.handleBackspaceInternal = + function(e, range) { + goog.editor.plugins.TagOnEnterHandler.superClass_.handleBackspaceInternal. + call(this, e, range); + + if (goog.userAgent.GECKO) { + this.markBrToNotBeRemoved_(range, true); + } +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.processParagraphTagsInternal = + function(e, split) { + if ((goog.userAgent.OPERA || goog.userAgent.IE) && + this.tag != goog.dom.TagName.P) { + this.ensureBlockIeOpera(this.tag); + } +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.handleDeleteGecko = function( + e) { + var range = this.getFieldObject().getRange(); + var container = goog.editor.style.getContainer( + range && range.getContainerElement()); + if (this.getFieldObject().getElement().lastChild == container && + goog.editor.plugins.EnterHandler.isBrElem(container)) { + // Don't delete if it's the last node in the field and just has a BR. + e.preventDefault(); + // TODO(user): I think we probably don't need to stopPropagation here + e.stopPropagation(); + } else { + // Go ahead with deletion. + // Prevent an existing BR immediately following the selection being deleted + // from being removed in the keyup stage (as opposed to a BR added by FF + // after deletion, which we do remove). + this.markBrToNotBeRemoved_(range, false); + // Manually delete the selection if it's at a BR. + this.deleteBrGecko(e); + } +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.handleKeyUpInternal = function( + e) { + if (goog.userAgent.GECKO) { + if (e.keyCode == goog.events.KeyCodes.DELETE) { + this.removeBrIfNecessary_(false); + } else if (e.keyCode == goog.events.KeyCodes.BACKSPACE) { + this.removeBrIfNecessary_(true); + } + } else if ((goog.userAgent.IE || goog.userAgent.OPERA) && + e.keyCode == goog.events.KeyCodes.ENTER) { + this.ensureBlockIeOpera(this.tag, true); + } + // Safari uses DIVs by default. +}; + + +/** + * String that matches a single BR tag or NBSP surrounded by non-breaking + * whitespace + * @type {string} + * @private + */ +goog.editor.plugins.TagOnEnterHandler.BrOrNbspSurroundedWithWhiteSpace_ = + '[\t\n\r ]*(<br[^>]*\/?>| )[\t\n\r ]*'; + + +/** + * String that matches a single BR tag or NBSP surrounded by non-breaking + * whitespace + * @type {RegExp} + * @private + */ +goog.editor.plugins.TagOnEnterHandler.emptyLiRegExp_ = new RegExp('^' + + goog.editor.plugins.TagOnEnterHandler.BrOrNbspSurroundedWithWhiteSpace_ + + '$'); + + +/** + * Ensures the current node is wrapped in the tag. + * @param {Node} node The node to ensure gets wrapped. + * @param {Element} container Element containing the selection. + * @return {Element} Element containing the selection, after the wrapping. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.prototype.ensureNodeIsWrappedW3c_ = + function(node, container) { + if (container == this.getFieldObject().getElement()) { + // If the first block-level ancestor of cursor is the field, + // don't split the tree. Find all the text from the cursor + // to both block-level elements surrounding it (if they exist) + // and split the text into two elements. + // This is the IE contentEditable behavior. + + // The easy way to do this is to wrap all the text in an element + // and then split the element as if the user had hit enter + // in the paragraph + + // However, simply wrapping the text into an element creates problems + // if the text was already wrapped using some other element such as an + // anchor. For example, wrapping the text of + // <a href="">Text</a> + // would produce + // <a href=""><p>Text</p></a> + // which is not what we want. What we really want is + // <p><a href="">Text</a></p> + // So we need to search for an ancestor of position.node to be wrapped. + // We do this by iterating up the hierarchy of postiion.node until we've + // reached the node that's just under the container. + var isChildOfFn = function(child) { + return container == child.parentNode; }; + var nodeToWrap = goog.dom.getAncestor(node, isChildOfFn, true); + container = goog.editor.plugins.TagOnEnterHandler.wrapInContainerW3c_( + this.tag, {node: nodeToWrap, offset: 0}, container); + } + return container; +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype.handleEnterWebkitInternal = + function(e) { + if (this.tag == goog.dom.TagName.DIV) { + var range = this.getFieldObject().getRange(); + var container = + goog.editor.style.getContainer(range.getContainerElement()); + + var position = goog.editor.range.getDeepEndPoint(range, true); + container = this.ensureNodeIsWrappedW3c_(position.node, container); + goog.dom.Range.createCaret(position.node, position.offset).select(); + } +}; + + +/** @override */ +goog.editor.plugins.TagOnEnterHandler.prototype. + handleEnterAtCursorGeckoInternal = function(e, wasCollapsed, range) { + // We use this because there are a few cases where FF default + // implementation doesn't follow IE's: + // -Inserts BRs into empty elements instead of NBSP which has nasty + // side effects w/ making/deleting selections + // -Hitting enter when your cursor is in the field itself. IE will + // create two elements. FF just inserts a BR. + // -Hitting enter inside an empty list-item doesn't create a block + // tag. It just splits the list and puts your cursor in the middle. + var li = null; + if (wasCollapsed) { + // Only break out of lists for collapsed selections. + li = goog.dom.getAncestorByTagNameAndClass( + range && range.getContainerElement(), goog.dom.TagName.LI); + } + var isEmptyLi = (li && + li.innerHTML.match( + goog.editor.plugins.TagOnEnterHandler.emptyLiRegExp_)); + var elementAfterCursor = isEmptyLi ? + this.breakOutOfEmptyListItemGecko_(li) : + this.handleRegularEnterGecko_(); + + // Move the cursor in front of "nodeAfterCursor", and make sure it + // is visible + this.scrollCursorIntoViewGecko_(elementAfterCursor); + + // Fix for http://b/1991234 : + if (goog.editor.plugins.EnterHandler.isBrElem(elementAfterCursor)) { + // The first element in the new line is a line with just a BR and maybe some + // whitespace. + // Calling normalize() is needed because there might be empty text nodes + // before BR and empty text nodes cause the cursor position bug in Firefox. + // See http://b/5220858 + elementAfterCursor.normalize(); + var br = elementAfterCursor.getElementsByTagName(goog.dom.TagName.BR)[0]; + if (br.previousSibling && + br.previousSibling.nodeType == goog.dom.NodeType.TEXT) { + // If there is some whitespace before the BR, don't put the selection on + // the BR, put it in the text node that's there, otherwise when you type + // it will create adjacent text nodes. + elementAfterCursor = br.previousSibling; + } + } + + goog.editor.range.selectNodeStart(elementAfterCursor); + + e.preventDefault(); + // TODO(user): I think we probably don't need to stopPropagation here + e.stopPropagation(); +}; + + +/** + * If The cursor is in an empty LI then break out of the list like in IE + * @param {Node} li LI to break out of. + * @return {!Element} Element to put the cursor after. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.prototype.breakOutOfEmptyListItemGecko_ = + function(li) { + // Do this as follows: + // 1. <ul>...<li> </li>...</ul> + // 2. <ul id='foo1'>...<li id='foo2'> </li>...</ul> + // 3. <ul id='foo1'>...</ul><p id='foo3'> </p><ul id='foo2'>...</ul> + // 4. <ul>...</ul><p> </p><ul>...</ul> + // + // There are a couple caveats to the above. If the UL is contained in + // a list, then the new node inserted is an LI, not a P. + // For an OL, it's all the same, except the tagname of course. + // Finally, it's possible that with the LI at the beginning or the end + // of the list that we'll end up with an empty list. So we special case + // those cases. + + var listNode = li.parentNode; + var grandparent = listNode.parentNode; + var inSubList = grandparent.tagName == goog.dom.TagName.OL || + grandparent.tagName == goog.dom.TagName.UL; + + // TODO(robbyw): Should we apply the list or list item styles to the new node? + var newNode = goog.dom.getDomHelper(li).createElement( + inSubList ? goog.dom.TagName.LI : this.tag); + + if (!li.previousSibling) { + goog.dom.insertSiblingBefore(newNode, listNode); + } else { + if (li.nextSibling) { + var listClone = listNode.cloneNode(false); + while (li.nextSibling) { + listClone.appendChild(li.nextSibling); + } + goog.dom.insertSiblingAfter(listClone, listNode); + } + goog.dom.insertSiblingAfter(newNode, listNode); + } + if (goog.editor.node.isEmpty(listNode)) { + goog.dom.removeNode(listNode); + } + goog.dom.removeNode(li); + newNode.innerHTML = ' '; + + return newNode; +}; + + +/** + * Wrap the text indicated by "position" in an HTML container of type + * "nodeName". + * @param {string} nodeName Type of container, e.g. "p" (paragraph). + * @param {Object} position The W3C cursor position object + * (from getCursorPositionW3c). + * @param {Node} container The field containing position. + * @return {!Element} The container element that holds the contents from + * position. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.wrapInContainerW3c_ = function(nodeName, + position, container) { + var start = position.node; + while (start.previousSibling && + !goog.editor.style.isContainer(start.previousSibling)) { + start = start.previousSibling; + } + + var end = position.node; + while (end.nextSibling && + !goog.editor.style.isContainer(end.nextSibling)) { + end = end.nextSibling; + } + + var para = container.ownerDocument.createElement(nodeName); + while (start != end) { + var newStart = start.nextSibling; + goog.dom.appendChild(para, start); + start = newStart; + } + var nextSibling = end.nextSibling; + goog.dom.appendChild(para, end); + container.insertBefore(para, nextSibling); + + return para; +}; + + +/** + * When we delete an element, FF inserts a BR. We want to strip that + * BR after the fact, but in the case where your cursor is at a character + * right before a BR and you delete that character, we don't want to + * strip it. So we detect this case on keydown and mark the BR as not needing + * removal. + * @param {goog.dom.AbstractRange} range The closure range object. + * @param {boolean} isBackspace Whether this is handling the backspace key. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.prototype.markBrToNotBeRemoved_ = + function(range, isBackspace) { + var focusNode = range.getFocusNode(); + var focusOffset = range.getFocusOffset(); + var newEndOffset = isBackspace ? focusOffset : focusOffset + 1; + + if (goog.editor.node.getLength(focusNode) == newEndOffset) { + var sibling = focusNode.nextSibling; + if (sibling && sibling.tagName == goog.dom.TagName.BR) { + this.brToKeep_ = sibling; + } + } +}; + + +/** + * If we hit delete/backspace to merge elements, FF inserts a BR. + * We want to strip that BR. In markBrToNotBeRemoved, we detect if + * there was already a BR there before the delete/backspace so that + * we don't accidentally remove a user-inserted BR. + * @param {boolean} isBackSpace Whether this is handling the backspace key. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.prototype.removeBrIfNecessary_ = function( + isBackSpace) { + var range = this.getFieldObject().getRange(); + var focusNode = range.getFocusNode(); + var focusOffset = range.getFocusOffset(); + + var sibling; + if (isBackSpace && focusNode.data == '') { + // nasty hack. sometimes firefox will backspace a paragraph and put + // the cursor before the BR. when it does this, the focusNode is + // an empty textnode. + sibling = focusNode.nextSibling; + } else if (isBackSpace && focusOffset == 0) { + var node = focusNode; + while (node && !node.previousSibling && + node.parentNode != this.getFieldObject().getElement()) { + node = node.parentNode; + } + sibling = node.previousSibling; + } else if (focusNode.length == focusOffset) { + sibling = focusNode.nextSibling; + } + + if (!sibling || sibling.tagName != goog.dom.TagName.BR || + this.brToKeep_ == sibling) { + return; + } + + goog.dom.removeNode(sibling); + if (focusNode.nodeType == goog.dom.NodeType.TEXT) { + // Sometimes firefox inserts extra whitespace. Do our best to deal. + // This is buggy though. + focusNode.data = + goog.editor.plugins.TagOnEnterHandler.trimTabsAndLineBreaks_( + focusNode.data); + // When we strip whitespace, make sure that our cursor is still at + // the end of the textnode. + goog.dom.Range.createCaret(focusNode, + Math.min(focusOffset, focusNode.length)).select(); + } +}; + + +/** + * Trim the tabs and line breaks from a string. + * @param {string} string String to trim. + * @return {string} Trimmed string. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.trimTabsAndLineBreaks_ = function( + string) { + return string.replace(/^[\t\n\r]|[\t\n\r]$/g, ''); +}; + + +/** + * Called in response to a normal enter keystroke. It has the action of + * splitting elements. + * @return {Element} The node that the cursor should be before. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.prototype.handleRegularEnterGecko_ = + function() { + var range = this.getFieldObject().getRange(); + var container = + goog.editor.style.getContainer(range.getContainerElement()); + var newNode; + if (goog.editor.plugins.EnterHandler.isBrElem(container)) { + if (container.tagName == goog.dom.TagName.BODY) { + // If the field contains only a single BR, this code ensures we don't + // try to clone the body tag. + container = this.ensureNodeIsWrappedW3c_( + container.getElementsByTagName(goog.dom.TagName.BR)[0], + container); + } + + newNode = container.cloneNode(true); + goog.dom.insertSiblingAfter(newNode, container); + } else { + if (!container.firstChild) { + container.innerHTML = ' '; + } + + var position = goog.editor.range.getDeepEndPoint(range, true); + container = this.ensureNodeIsWrappedW3c_(position.node, container); + + newNode = goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_( + position.node, position.offset, container); + + // If the left half and right half of the splitted node are anchors then + // that means the user pressed enter while the caret was inside + // an anchor tag and split it. The left half is the first anchor + // found while traversing the right branch of container. The right half + // is the first anchor found while traversing the left branch of newNode. + var leftAnchor = + goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_( + container); + var rightAnchor = + goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_( + newNode, true); + if (leftAnchor && rightAnchor && + leftAnchor.tagName == goog.dom.TagName.A && + rightAnchor.tagName == goog.dom.TagName.A) { + // If the original anchor (left anchor) is now empty, that means + // the user pressed [Enter] at the beginning of the anchor, + // in which case we we + // want to replace that anchor with its child nodes + // Otherwise, we take the second half of the splitted text and break + // it out of the anchor. + var anchorToRemove = goog.editor.node.isEmpty(leftAnchor, false) ? + leftAnchor : rightAnchor; + goog.dom.flattenElement(/** @type {!Element} */ (anchorToRemove)); + } + } + return /** @type {!Element} */ (newNode); +}; + + +/** + * Scroll the cursor into view, resulting from splitting the paragraph/adding + * a br. It behaves differently than scrollIntoView + * @param {Element} element The element immediately following the cursor. Will + * be used to determine how to scroll in order to make the cursor visible. + * CANNOT be a BR, as they do not have offsetHeight/offsetTop. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.prototype.scrollCursorIntoViewGecko_ = + function(element) { + if (!this.getFieldObject().isFixedHeight()) { + return; // Only need to scroll fixed height fields. + } + + var field = this.getFieldObject().getElement(); + + // Get the y position of the element we want to scroll to + var elementY = goog.style.getPageOffsetTop(element); + + // Determine the height of that element, since we want the bottom of the + // element to be in view. + var bottomOfNode = elementY + element.offsetHeight; + + var dom = this.getFieldDomHelper(); + var win = this.getFieldDomHelper().getWindow(); + var scrollY = dom.getDocumentScroll().y; + var viewportHeight = goog.dom.getViewportSize(win).height; + + // If the botom of the element is outside the viewport, move it into view + if (bottomOfNode > viewportHeight + scrollY) { + // In standards mode, use the html element and not the body + if (field.tagName == goog.dom.TagName.BODY && + goog.editor.node.isStandardsMode(field)) { + field = field.parentNode; + } + field.scrollTop = bottomOfNode - viewportHeight; + } +}; + + +/** + * Splits the DOM tree around the given node and returns the node + * containing the second half of the tree. The first half of the tree + * is modified, but not removed from the DOM. + * @param {Node} positionNode Node to split at. + * @param {number} positionOffset Offset into positionNode to split at. If + * positionNode is a text node, this offset is an offset in to the text + * content of that node. Otherwise, positionOffset is an offset in to + * the childNodes array. All elements with child index of positionOffset + * or greater will be moved to the second half. If positionNode is an + * empty element, the dom will be split at that element, with positionNode + * ending up in the second half. positionOffset must be 0 in this case. + * @param {Node=} opt_root Node at which to stop splitting the dom (the root + * is also split). + * @return {!Node} The node containing the second half of the tree. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.splitDom_ = function( + positionNode, positionOffset, opt_root) { + if (!opt_root) opt_root = positionNode.ownerDocument.body; + + // Split the node. + var textSplit = positionNode.nodeType == goog.dom.NodeType.TEXT; + var secondHalfOfSplitNode; + if (textSplit) { + if (goog.userAgent.IE && + positionOffset == positionNode.nodeValue.length) { + // Since splitText fails in IE at the end of a node, we split it manually. + secondHalfOfSplitNode = goog.dom.getDomHelper(positionNode). + createTextNode(''); + goog.dom.insertSiblingAfter(secondHalfOfSplitNode, positionNode); + } else { + secondHalfOfSplitNode = positionNode.splitText(positionOffset); + } + } else { + // Here we ensure positionNode is the last node in the first half of the + // resulting tree. + if (positionOffset) { + // Use offset as an index in to childNodes. + positionNode = positionNode.childNodes[positionOffset - 1]; + } else { + // In this case, positionNode would be the last node in the first half + // of the tree, but we actually want to move it to the second half. + // Therefore we set secondHalfOfSplitNode to the same node. + positionNode = secondHalfOfSplitNode = positionNode.firstChild || + positionNode; + } + } + + // Create second half of the tree. + var secondHalf = goog.editor.node.splitDomTreeAt( + positionNode, secondHalfOfSplitNode, opt_root); + + if (textSplit) { + // Join secondHalfOfSplitNode and its right text siblings together and + // then replace leading NonNbspWhiteSpace with a Nbsp. If + // secondHalfOfSplitNode has a right sibling that isn't a text node, + // then we can leave secondHalfOfSplitNode empty. + secondHalfOfSplitNode = + goog.editor.plugins.TagOnEnterHandler.joinTextNodes_( + secondHalfOfSplitNode, true); + goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( + secondHalfOfSplitNode, true, !!secondHalfOfSplitNode.nextSibling); + + // Join positionNode and its left text siblings together and then replace + // trailing NonNbspWhiteSpace with a Nbsp. + var firstHalf = goog.editor.plugins.TagOnEnterHandler.joinTextNodes_( + positionNode, false); + goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( + firstHalf, false, false); + } + + return secondHalf; +}; + + +/** + * Splits the DOM tree around the given node and returns the node containing + * second half of the tree, which is appended after the old node. The first + * half of the tree is modified, but not removed from the DOM. + * @param {Node} positionNode Node to split at. + * @param {number} positionOffset Offset into positionNode to split at. If + * positionNode is a text node, this offset is an offset in to the text + * content of that node. Otherwise, positionOffset is an offset in to + * the childNodes array. All elements with child index of positionOffset + * or greater will be moved to the second half. If positionNode is an + * empty element, the dom will be split at that element, with positionNode + * ending up in the second half. positionOffset must be 0 in this case. + * @param {Node} node Node to split. + * @return {!Node} The node containing the second half of the tree. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_ = function( + positionNode, positionOffset, node) { + var newNode = goog.editor.plugins.TagOnEnterHandler.splitDom_( + positionNode, positionOffset, node); + goog.dom.insertSiblingAfter(newNode, node); + return newNode; +}; + + +/** + * Joins node and its adjacent text nodes together. + * @param {Node} node The node to start joining. + * @param {boolean} moveForward Determines whether to join left siblings (false) + * or right siblings (true). + * @return {Node} The joined text node. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.joinTextNodes_ = function(node, + moveForward) { + if (node && node.nodeName == '#text') { + var nextNodeFn = moveForward ? 'nextSibling' : 'previousSibling'; + var prevNodeFn = moveForward ? 'previousSibling' : 'nextSibling'; + var nodeValues = [node.nodeValue]; + while (node[nextNodeFn] && + node[nextNodeFn].nodeType == goog.dom.NodeType.TEXT) { + node = node[nextNodeFn]; + nodeValues.push(node.nodeValue); + goog.dom.removeNode(node[prevNodeFn]); + } + if (!moveForward) { + nodeValues.reverse(); + } + node.nodeValue = nodeValues.join(''); + } + return node; +}; + + +/** + * Replaces leading or trailing spaces of a text node to a single Nbsp. + * @param {Node} textNode The text node to search and replace white spaces. + * @param {boolean} fromStart Set to true to replace leading spaces, false to + * replace trailing spaces. + * @param {boolean} isLeaveEmpty Set to true to leave the node empty if the + * text node was empty in the first place, otherwise put a Nbsp into the + * text node. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_ = function( + textNode, fromStart, isLeaveEmpty) { + var regExp = fromStart ? /^[ \t\r\n]+/ : /[ \t\r\n]+$/; + textNode.nodeValue = textNode.nodeValue.replace(regExp, + goog.string.Unicode.NBSP); + + if (!isLeaveEmpty && textNode.nodeValue == '') { + textNode.nodeValue = goog.string.Unicode.NBSP; + } +}; + + +/** + * Finds the first A element in a traversal from the input node. The input + * node itself is not included in the search. + * @param {Node} node The node to start searching from. + * @param {boolean=} opt_useFirstChild Whether to traverse along the first child + * (true) or last child (false). + * @return {Node} The first anchor node found in the search, or null if none + * was found. + * @private + */ +goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_ = function(node, + opt_useFirstChild) { + while ((node = opt_useFirstChild ? node.firstChild : node.lastChild) && + node.tagName != goog.dom.TagName.A) { + // Do nothing - advancement is handled in the condition. + } + return node; +};
http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/undoredo.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/undoredo.js b/externs/GCL/externs/goog/editor/plugins/undoredo.js new file mode 100644 index 0000000..d273a4c --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/undoredo.js @@ -0,0 +1,1016 @@ +// Copyright 2005 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 Code for handling edit history (undo/redo). + * + */ + + +goog.provide('goog.editor.plugins.UndoRedo'); + +goog.require('goog.dom'); +goog.require('goog.dom.NodeOffset'); +goog.require('goog.dom.Range'); +goog.require('goog.editor.BrowserFeature'); +goog.require('goog.editor.Command'); +goog.require('goog.editor.Field'); +goog.require('goog.editor.Plugin'); +goog.require('goog.editor.node'); +goog.require('goog.editor.plugins.UndoRedoManager'); +goog.require('goog.editor.plugins.UndoRedoState'); +goog.require('goog.events'); +goog.require('goog.events.EventHandler'); +goog.require('goog.log'); +goog.require('goog.object'); + + + +/** + * Encapsulates undo/redo logic using a custom undo stack (i.e. not browser + * built-in). Browser built-in undo stacks are too flaky (e.g. IE's gets + * clobbered on DOM modifications). Also, this allows interleaving non-editing + * commands into the undo stack via the UndoRedoManager. + * + * @param {goog.editor.plugins.UndoRedoManager=} opt_manager An undo redo + * manager to be used by this plugin. If none is provided one is created. + * @constructor + * @extends {goog.editor.Plugin} + */ +goog.editor.plugins.UndoRedo = function(opt_manager) { + goog.editor.Plugin.call(this); + + this.setUndoRedoManager(opt_manager || + new goog.editor.plugins.UndoRedoManager()); + + // Map of goog.editor.Field hashcode to goog.events.EventHandler + this.eventHandlers_ = {}; + + this.currentStates_ = {}; + + /** + * @type {?string} + * @private + */ + this.initialFieldChange_ = null; + + /** + * A copy of {@code goog.editor.plugins.UndoRedo.restoreState} bound to this, + * used by undo-redo state objects to restore the state of an editable field. + * @type {Function} + * @see goog.editor.plugins.UndoRedo#restoreState + * @private + */ + this.boundRestoreState_ = goog.bind(this.restoreState, this); +}; +goog.inherits(goog.editor.plugins.UndoRedo, goog.editor.Plugin); + + +/** + * The logger for this class. + * @type {goog.log.Logger} + * @protected + * @override + */ +goog.editor.plugins.UndoRedo.prototype.logger = + goog.log.getLogger('goog.editor.plugins.UndoRedo'); + + +/** + * The {@code UndoState_} whose change is in progress, null if an undo or redo + * is not in progress. + * + * @type {goog.editor.plugins.UndoRedo.UndoState_?} + * @private + */ +goog.editor.plugins.UndoRedo.prototype.inProgressUndo_ = null; + + +/** + * The undo-redo stack manager used by this plugin. + * @type {goog.editor.plugins.UndoRedoManager} + * @private + */ +goog.editor.plugins.UndoRedo.prototype.undoManager_; + + +/** + * The key for the event listener handling state change events from the + * undo-redo manager. + * @type {goog.events.Key} + * @private + */ +goog.editor.plugins.UndoRedo.prototype.managerStateChangeKey_; + + +/** + * Commands implemented by this plugin. + * @enum {string} + */ +goog.editor.plugins.UndoRedo.COMMAND = { + UNDO: '+undo', + REDO: '+redo' +}; + + +/** + * Inverse map of execCommand strings to + * {@link goog.editor.plugins.UndoRedo.COMMAND} constants. Used to determine + * whether a string corresponds to a command this plugin handles in O(1) time. + * @type {Object} + * @private + */ +goog.editor.plugins.UndoRedo.SUPPORTED_COMMANDS_ = + goog.object.transpose(goog.editor.plugins.UndoRedo.COMMAND); + + +/** + * Set the max undo stack depth (not the real memory usage). + * @param {number} depth Depth of the stack. + */ +goog.editor.plugins.UndoRedo.prototype.setMaxUndoDepth = function(depth) { + this.undoManager_.setMaxUndoDepth(depth); +}; + + +/** + * Set the undo-redo manager used by this plugin. Any state on a previous + * undo-redo manager is lost. + * @param {goog.editor.plugins.UndoRedoManager} manager The undo-redo manager. + */ +goog.editor.plugins.UndoRedo.prototype.setUndoRedoManager = function(manager) { + if (this.managerStateChangeKey_) { + goog.events.unlistenByKey(this.managerStateChangeKey_); + } + + this.undoManager_ = manager; + this.managerStateChangeKey_ = + goog.events.listen(this.undoManager_, + goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE, + this.dispatchCommandValueChange_, + false, + this); +}; + + +/** + * Whether the string corresponds to a command this plugin handles. + * @param {string} command Command string to check. + * @return {boolean} Whether the string corresponds to a command + * this plugin handles. + * @override + */ +goog.editor.plugins.UndoRedo.prototype.isSupportedCommand = function(command) { + return command in goog.editor.plugins.UndoRedo.SUPPORTED_COMMANDS_; +}; + + +/** + * Unregisters and disables the fieldObject with this plugin. Thie does *not* + * clobber the undo stack for the fieldObject though. + * TODO(user): For the multifield version, we really should add a way to + * ignore undo actions on field's that have been made uneditable. + * This is probably as simple as skipping over entries in the undo stack + * that have a hashcode of an uneditable field. + * @param {goog.editor.Field} fieldObject The field to register with the plugin. + * @override + */ +goog.editor.plugins.UndoRedo.prototype.unregisterFieldObject = function( + fieldObject) { + this.disable(fieldObject); + this.setFieldObject(null); +}; + + +/** + * This is so subclasses can deal with multifield undo-redo. + * @return {goog.editor.Field} The active field object for this field. This is + * the one registered field object for the single-plugin case and the + * focused field for the multi-field plugin case. + */ +goog.editor.plugins.UndoRedo.prototype.getCurrentFieldObject = function() { + return this.getFieldObject(); +}; + + +/** + * This is so subclasses can deal with multifield undo-redo. + * @param {string} fieldHashCode The Field's hashcode. + * @return {goog.editor.Field} The field object with the hashcode. + */ +goog.editor.plugins.UndoRedo.prototype.getFieldObjectForHash = function( + fieldHashCode) { + // With single field undoredo, there's only one Field involved. + return this.getFieldObject(); +}; + + +/** + * This is so subclasses can deal with multifield undo-redo. + * @return {goog.editor.Field} Target for COMMAND_VALUE_CHANGE events. + */ +goog.editor.plugins.UndoRedo.prototype.getCurrentEventTarget = function() { + return this.getFieldObject(); +}; + + +/** @override */ +goog.editor.plugins.UndoRedo.prototype.enable = function(fieldObject) { + if (this.isEnabled(fieldObject)) { + return; + } + + // Don't want pending delayed changes from when undo-redo was disabled + // firing after undo-redo is enabled since they might cause undo-redo stack + // updates. + fieldObject.clearDelayedChange(); + + var eventHandler = new goog.events.EventHandler(this); + + // TODO(user): From ojan during a code review: + // The beforechange handler is meant to be there so you can grab the cursor + // position *before* the change is made as that's where you want the cursor to + // be after an undo. + // + // It kinda looks like updateCurrentState_ doesn't do that correctly right + // now, but it really should be fixed to do so. The cursor position stored in + // the state should be the cursor position before any changes are made, not + // the cursor position when the change finishes. + // + // It also seems like the if check below is just a bad one. We should do this + // for browsers that use mutation events as well even though the beforechange + // happens too late...maybe not. I don't know about this. + if (!goog.editor.BrowserFeature.USE_MUTATION_EVENTS) { + // We don't listen to beforechange in mutation-event browsers because + // there we fire beforechange, then syncronously file change. The point + // of before change is to capture before the user has changed anything. + eventHandler.listen(fieldObject, + goog.editor.Field.EventType.BEFORECHANGE, this.handleBeforeChange_); + } + eventHandler.listen(fieldObject, + goog.editor.Field.EventType.DELAYEDCHANGE, this.handleDelayedChange_); + eventHandler.listen(fieldObject, goog.editor.Field.EventType.BLUR, + this.handleBlur_); + + this.eventHandlers_[fieldObject.getHashCode()] = eventHandler; + + // We want to capture the initial state of a Trogedit field before any + // editing has happened. This is necessary so that we can undo the first + // change to a field, even if we don't handle beforeChange. + this.updateCurrentState_(fieldObject); +}; + + +/** @override */ +goog.editor.plugins.UndoRedo.prototype.disable = function(fieldObject) { + // Process any pending changes so we don't lose any undo-redo states that we + // want prior to disabling undo-redo. + fieldObject.clearDelayedChange(); + + var eventHandler = this.eventHandlers_[fieldObject.getHashCode()]; + if (eventHandler) { + eventHandler.dispose(); + delete this.eventHandlers_[fieldObject.getHashCode()]; + } + + // We delete the current state of the field on disable. When we re-enable + // the state will be re-fetched. In most cases the content will be the same, + // but this allows us to pick up changes while not editable. That way, when + // undoing after starting an editable session, you can always undo to the + // state you started in. Given this sequence of events: + // Make editable + // Type 'anakin' + // Make not editable + // Set HTML to be 'padme' + // Make editable + // Type 'dark side' + // Undo + // Without re-snapshoting current state on enable, the undo would go from + // 'dark-side' -> 'anakin', rather than 'dark-side' -> 'padme'. You couldn't + // undo the field to the state that existed immediately after it was made + // editable for the second time. + if (this.currentStates_[fieldObject.getHashCode()]) { + delete this.currentStates_[fieldObject.getHashCode()]; + } +}; + + +/** @override */ +goog.editor.plugins.UndoRedo.prototype.isEnabled = function(fieldObject) { + // All enabled plugins have a eventHandler so reuse that map rather than + // storing additional enabled state. + return !!this.eventHandlers_[fieldObject.getHashCode()]; +}; + + +/** @override */ +goog.editor.plugins.UndoRedo.prototype.disposeInternal = function() { + goog.editor.plugins.UndoRedo.superClass_.disposeInternal.call(this); + + for (var hashcode in this.eventHandlers_) { + this.eventHandlers_[hashcode].dispose(); + delete this.eventHandlers_[hashcode]; + } + this.setFieldObject(null); + + if (this.undoManager_) { + this.undoManager_.dispose(); + delete this.undoManager_; + } +}; + + +/** @override */ +goog.editor.plugins.UndoRedo.prototype.getTrogClassId = function() { + return 'UndoRedo'; +}; + + +/** @override */ +goog.editor.plugins.UndoRedo.prototype.execCommand = function(command, + var_args) { + if (command == goog.editor.plugins.UndoRedo.COMMAND.UNDO) { + this.undoManager_.undo(); + } else if (command == goog.editor.plugins.UndoRedo.COMMAND.REDO) { + this.undoManager_.redo(); + } +}; + + +/** @override */ +goog.editor.plugins.UndoRedo.prototype.queryCommandValue = function(command) { + var state = null; + if (command == goog.editor.plugins.UndoRedo.COMMAND.UNDO) { + state = this.undoManager_.hasUndoState(); + } else if (command == goog.editor.plugins.UndoRedo.COMMAND.REDO) { + state = this.undoManager_.hasRedoState(); + } + return state; +}; + + +/** + * Dispatches the COMMAND_VALUE_CHANGE event on the editable field or the field + * manager, as appropriate. + * Note: Really, people using multi field mode should be listening directly + * to the undo-redo manager for events. + * @private + */ +goog.editor.plugins.UndoRedo.prototype.dispatchCommandValueChange_ = + function() { + var eventTarget = this.getCurrentEventTarget(); + eventTarget.dispatchEvent({ + type: goog.editor.Field.EventType.COMMAND_VALUE_CHANGE, + commands: [goog.editor.plugins.UndoRedo.COMMAND.REDO, + goog.editor.plugins.UndoRedo.COMMAND.UNDO]}); +}; + + +/** + * Restores the state of the editable field. + * @param {goog.editor.plugins.UndoRedo.UndoState_} state The state initiating + * the restore. + * @param {string} content The content to restore. + * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition + * The cursor position within the content. + */ +goog.editor.plugins.UndoRedo.prototype.restoreState = function( + state, content, cursorPosition) { + // Fire any pending changes to get the current field state up to date and + // then stop listening to changes while doing the undo/redo. + var fieldObj = this.getFieldObjectForHash(state.fieldHashCode); + if (!fieldObj) { + return; + } + + // Fires any pending changes, and stops the change events. Still want to + // dispatch before change, as a change is being made and the change event + // will be manually dispatched below after the new content has been restored + // (also restarting change events). + fieldObj.stopChangeEvents(true, true); + + // To prevent the situation where we stop change events and then an exception + // happens before we can restart change events, the following code must be in + // a try-finally block. + try { + fieldObj.dispatchBeforeChange(); + + // Restore the state + fieldObj.execCommand(goog.editor.Command.CLEAR_LOREM, true); + + // We specifically set the raw innerHTML of the field here as that's what + // we get from the field when we save an undo/redo state. There's + // no need to clean/unclean the contents in either direction. + goog.editor.node.replaceInnerHtml(fieldObj.getElement(), content); + + if (cursorPosition) { + cursorPosition.select(); + } + + var previousFieldObject = this.getCurrentFieldObject(); + fieldObj.focus(); + + // Apps that integrate their undo-redo with Trogedit may be + // in a state where there is no previous field object (no field focused at + // the time of undo), so check for existence first. + if (previousFieldObject && + previousFieldObject.getHashCode() != state.fieldHashCode) { + previousFieldObject.execCommand(goog.editor.Command.UPDATE_LOREM); + } + + // We need to update currentState_ to reflect the change. + this.currentStates_[state.fieldHashCode].setUndoState( + content, cursorPosition); + } catch (e) { + goog.log.error(this.logger, 'Error while restoring undo state', e); + } finally { + // Clear the delayed change event, set flag so we know not to act on it. + this.inProgressUndo_ = state; + // Notify the editor that we've changed (fire autosave). + // Note that this starts up change events again, so we don't have to + // manually do so even though we stopped change events above. + fieldObj.dispatchChange(); + fieldObj.dispatchSelectionChangeEvent(); + } +}; + + +/** + * @override + */ +goog.editor.plugins.UndoRedo.prototype.handleKeyboardShortcut = function(e, key, + isModifierPressed) { + if (isModifierPressed) { + var command; + if (key == 'z') { + command = e.shiftKey ? goog.editor.plugins.UndoRedo.COMMAND.REDO : + goog.editor.plugins.UndoRedo.COMMAND.UNDO; + } else if (key == 'y') { + command = goog.editor.plugins.UndoRedo.COMMAND.REDO; + } + + if (command) { + // In the case where Trogedit shares its undo redo stack with another + // application it's possible that an undo or redo will not be for an + // goog.editor.Field. In this case we don't want to go through the + // goog.editor.Field execCommand flow which stops and restarts events on + // the current field. Only Trogedit UndoState's have a fieldHashCode so + // use that to distinguish between Trogedit and other states. + var state = command == goog.editor.plugins.UndoRedo.COMMAND.UNDO ? + this.undoManager_.undoPeek() : this.undoManager_.redoPeek(); + if (state && state.fieldHashCode) { + this.getCurrentFieldObject().execCommand(command); + } else { + this.execCommand(command); + } + + return true; + } + } + + return false; +}; + + +/** + * Clear the undo/redo stack. + */ +goog.editor.plugins.UndoRedo.prototype.clearHistory = function() { + // Fire all pending change events, so that they don't come back + // asynchronously to fill the queue. + this.getFieldObject().stopChangeEvents(true, true); + this.undoManager_.clearHistory(); + this.getFieldObject().startChangeEvents(); +}; + + +/** + * Refreshes the current state of the editable field as maintained by undo-redo, + * without adding any undo-redo states to the stack. + * @param {goog.editor.Field} fieldObject The editable field. + */ +goog.editor.plugins.UndoRedo.prototype.refreshCurrentState = function( + fieldObject) { + if (this.isEnabled(fieldObject)) { + if (this.currentStates_[fieldObject.getHashCode()]) { + delete this.currentStates_[fieldObject.getHashCode()]; + } + this.updateCurrentState_(fieldObject); + } +}; + + +/** + * Before the field changes, we want to save the state. + * @param {goog.events.Event} e The event. + * @private + */ +goog.editor.plugins.UndoRedo.prototype.handleBeforeChange_ = function(e) { + if (this.inProgressUndo_) { + // We are in between a previous undo and its delayed change event. + // Continuing here clobbers the redo stack. + // This does mean that if you are trying to undo/redo really quickly, it + // will be gated by the speed of delayed change events. + return; + } + + var fieldObj = /** @type {goog.editor.Field} */ (e.target); + var fieldHashCode = fieldObj.getHashCode(); + + if (this.initialFieldChange_ != fieldHashCode) { + this.initialFieldChange_ = fieldHashCode; + this.updateCurrentState_(fieldObj); + } +}; + + +/** + * After some idle time, we want to save the state. + * @param {goog.events.Event} e The event. + * @private + */ +goog.editor.plugins.UndoRedo.prototype.handleDelayedChange_ = function(e) { + // This was undo making a change, don't add it BACK into the history + if (this.inProgressUndo_) { + // Must clear this.inProgressUndo_ before dispatching event because the + // dispatch can cause another, queued undo that should be allowed to go + // through. + var state = this.inProgressUndo_; + this.inProgressUndo_ = null; + state.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED); + return; + } + + this.updateCurrentState_(/** @type {goog.editor.Field} */ (e.target)); +}; + + +/** + * When the user blurs away, we need to save the state on that field. + * @param {goog.events.Event} e The event. + * @private + */ +goog.editor.plugins.UndoRedo.prototype.handleBlur_ = function(e) { + var fieldObj = /** @type {goog.editor.Field} */ (e.target); + if (fieldObj) { + fieldObj.clearDelayedChange(); + } +}; + + +/** + * Returns the goog.editor.plugins.UndoRedo.CursorPosition_ for the current + * selection in the given Field. + * @param {goog.editor.Field} fieldObj The field object. + * @return {goog.editor.plugins.UndoRedo.CursorPosition_} The CursorPosition_ or + * null if there is no valid selection. + * @private + */ +goog.editor.plugins.UndoRedo.prototype.getCursorPosition_ = function(fieldObj) { + var cursorPos = new goog.editor.plugins.UndoRedo.CursorPosition_(fieldObj); + if (!cursorPos.isValid()) { + return null; + } + return cursorPos; +}; + + +/** + * Helper method for saving state. + * @param {goog.editor.Field} fieldObj The field object. + * @private + */ +goog.editor.plugins.UndoRedo.prototype.updateCurrentState_ = function( + fieldObj) { + var fieldHashCode = fieldObj.getHashCode(); + // We specifically grab the raw innerHTML of the field here as that's what + // we would set on the field in the case of an undo/redo operation. There's + // no need to clean/unclean the contents in either direction. In the case of + // lorem ipsum being used, we want to capture the effective state (empty, no + // cursor position) rather than capturing the lorem html. + var content, cursorPos; + if (fieldObj.queryCommandValue(goog.editor.Command.USING_LOREM)) { + content = ''; + cursorPos = null; + } else { + content = fieldObj.getElement().innerHTML; + cursorPos = this.getCursorPosition_(fieldObj); + } + + var currentState = this.currentStates_[fieldHashCode]; + if (currentState) { + // Don't create states if the content hasn't changed (spurious + // delayed change). This can happen when lorem is cleared, for example. + if (currentState.undoContent_ == content) { + return; + } else if (content == '' || currentState.undoContent_ == '') { + // If lorem ipsum is on we say the contents are the empty string. However, + // for an empty text shape with focus, the empty contents might not be + // the same, depending on plugins. We want these two empty states to be + // considered identical because to the user they are indistinguishable, + // so we use fieldObj.getInjectableContents to map between them. + // We cannot use getInjectableContents when first creating the undo + // content for a field with lorem, because on enable when this is first + // called we can't guarantee plugin registration order, so the + // injectableContents at that time might not match the final + // injectableContents. + var emptyContents = fieldObj.getInjectableContents('', {}); + if (content == emptyContents && currentState.undoContent_ == '' || + currentState.undoContent_ == emptyContents && content == '') { + return; + } + } + + currentState.setRedoState(content, cursorPos); + this.undoManager_.addState(currentState); + } + + this.currentStates_[fieldHashCode] = + new goog.editor.plugins.UndoRedo.UndoState_(fieldHashCode, content, + cursorPos, this.boundRestoreState_); +}; + + + +/** + * This object encapsulates the state of an editable field. + * + * @param {string} fieldHashCode String the id of the field we're saving the + * content of. + * @param {string} content String the actual text we're saving. + * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition + * CursorPosLite object for the cursor position in the field. + * @param {Function} restore The function used to restore editable field state. + * @private + * @constructor + * @extends {goog.editor.plugins.UndoRedoState} + */ +goog.editor.plugins.UndoRedo.UndoState_ = function(fieldHashCode, content, + cursorPosition, restore) { + goog.editor.plugins.UndoRedoState.call(this, true); + + /** + * The hash code for the field whose content is being saved. + * @type {string} + */ + this.fieldHashCode = fieldHashCode; + + /** + * The bound copy of {@code goog.editor.plugins.UndoRedo.restoreState} used by + * this state. + * @type {Function} + * @private + */ + this.restore_ = restore; + + this.setUndoState(content, cursorPosition); +}; +goog.inherits(goog.editor.plugins.UndoRedo.UndoState_, + goog.editor.plugins.UndoRedoState); + + +/** + * The content to restore on undo. + * @type {string} + * @private + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.undoContent_; + + +/** + * The cursor position to restore on undo. + * @type {goog.editor.plugins.UndoRedo.CursorPosition_?} + * @private + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.undoCursorPosition_; + + +/** + * The content to restore on redo, undefined until the state is pushed onto the + * undo stack. + * @type {string|undefined} + * @private + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.redoContent_; + + +/** + * The cursor position to restore on redo, undefined until the state is pushed + * onto the undo stack. + * @type {goog.editor.plugins.UndoRedo.CursorPosition_|null|undefined} + * @private + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.redoCursorPosition_; + + +/** + * Performs the undo operation represented by this state. + * @override + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.undo = function() { + this.restore_(this, this.undoContent_, + this.undoCursorPosition_); +}; + + +/** + * Performs the redo operation represented by this state. + * @override + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.redo = function() { + this.restore_(this, this.redoContent_, + this.redoCursorPosition_); +}; + + +/** + * Updates the undo portion of this state. Should only be used to update the + * current state of an editable field, which is not yet on the undo stack after + * an undo or redo operation. You should never be modifying states on the stack! + * @param {string} content The current content. + * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition + * The current cursor position. + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.setUndoState = function( + content, cursorPosition) { + this.undoContent_ = content; + this.undoCursorPosition_ = cursorPosition; +}; + + +/** + * Adds redo information to this state. This method should be called before the + * state is added onto the undo stack. + * + * @param {string} content The content to restore on a redo. + * @param {goog.editor.plugins.UndoRedo.CursorPosition_?} cursorPosition + * The cursor position to restore on a redo. + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.setRedoState = function( + content, cursorPosition) { + this.redoContent_ = content; + this.redoCursorPosition_ = cursorPosition; +}; + + +/** + * Checks if the *contents* of two + * {@code goog.editor.plugins.UndoRedo.UndoState_}s are the same. We don't + * bother checking the cursor position (that's not something we'd want to save + * anyway). + * @param {goog.editor.plugins.UndoRedoState} rhs The state to compare. + * @return {boolean} Whether the contents are the same. + * @override + */ +goog.editor.plugins.UndoRedo.UndoState_.prototype.equals = function(rhs) { + return this.fieldHashCode == rhs.fieldHashCode && + this.undoContent_ == rhs.undoContent_ && + this.redoContent_ == rhs.redoContent_; +}; + + + +/** + * Stores the state of the selection in a way the survives DOM modifications + * that don't modify the user-interactable content (e.g. making something bold + * vs. typing a character). + * + * TODO(user): Completely get rid of this and use goog.dom.SavedCaretRange. + * + * @param {goog.editor.Field} field The field the selection is in. + * @private + * @constructor + */ +goog.editor.plugins.UndoRedo.CursorPosition_ = function(field) { + this.field_ = field; + + var win = field.getEditableDomHelper().getWindow(); + var range = field.getRange(); + var isValidRange = !!range && range.isRangeInDocument() && + range.getWindow() == win; + range = isValidRange ? range : null; + + if (goog.editor.BrowserFeature.HAS_W3C_RANGES) { + this.initW3C_(range); + } else if (goog.editor.BrowserFeature.HAS_IE_RANGES) { + this.initIE_(range); + } +}; + + +/** + * The standards compliant version keeps a list of childNode offsets. + * @param {goog.dom.AbstractRange?} range The range to save. + * @private + */ +goog.editor.plugins.UndoRedo.CursorPosition_.prototype.initW3C_ = function( + range) { + this.isValid_ = false; + + // TODO: Check if the range is in the field before trying to save it + // for FF 3 contentEditable. + if (!range) { + return; + } + + var anchorNode = range.getAnchorNode(); + var focusNode = range.getFocusNode(); + if (!anchorNode || !focusNode) { + return; + } + + var anchorOffset = range.getAnchorOffset(); + var anchor = new goog.dom.NodeOffset(anchorNode, this.field_.getElement()); + + var focusOffset = range.getFocusOffset(); + var focus = new goog.dom.NodeOffset(focusNode, this.field_.getElement()); + + // Test range direction. + if (range.isReversed()) { + this.startOffset_ = focus; + this.startChildOffset_ = focusOffset; + this.endOffset_ = anchor; + this.endChildOffset_ = anchorOffset; + } else { + this.startOffset_ = anchor; + this.startChildOffset_ = anchorOffset; + this.endOffset_ = focus; + this.endChildOffset_ = focusOffset; + } + + this.isValid_ = true; +}; + + +/** + * In IE, we just keep track of the text offset (number of characters). + * @param {goog.dom.AbstractRange?} range The range to save. + * @private + */ +goog.editor.plugins.UndoRedo.CursorPosition_.prototype.initIE_ = function( + range) { + this.isValid_ = false; + + if (!range) { + return; + } + + var ieRange = range.getTextRange(0).getBrowserRangeObject(); + + if (!goog.dom.contains(this.field_.getElement(), ieRange.parentElement())) { + return; + } + + // Create a range that encompasses the contentEditable region to serve + // as a reference to form ranges below. + var contentEditableRange = + this.field_.getEditableDomHelper().getDocument().body.createTextRange(); + contentEditableRange.moveToElementText(this.field_.getElement()); + + // startMarker is a range from the start of the contentEditable node to the + // start of the current selection. + var startMarker = ieRange.duplicate(); + startMarker.collapse(true); + startMarker.setEndPoint('StartToStart', contentEditableRange); + this.startOffset_ = + goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_( + startMarker); + + // endMarker is a range from the start of teh contentEditable node to the + // end of the current selection. + var endMarker = ieRange.duplicate(); + endMarker.setEndPoint('StartToStart', contentEditableRange); + this.endOffset_ = + goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_( + endMarker); + + this.isValid_ = true; +}; + + +/** + * @return {boolean} Whether this object is valid. + */ +goog.editor.plugins.UndoRedo.CursorPosition_.prototype.isValid = function() { + return this.isValid_; +}; + + +/** + * @return {string} A string representation of this object. + * @override + */ +goog.editor.plugins.UndoRedo.CursorPosition_.prototype.toString = function() { + if (goog.editor.BrowserFeature.HAS_W3C_RANGES) { + return 'W3C:' + this.startOffset_.toString() + '\n' + + this.startChildOffset_ + ':' + this.endOffset_.toString() + '\n' + + this.endChildOffset_; + } + return 'IE:' + this.startOffset_ + ',' + this.endOffset_; +}; + + +/** + * Makes the browser's selection match the cursor position. + */ +goog.editor.plugins.UndoRedo.CursorPosition_.prototype.select = function() { + var range = this.getRange_(this.field_.getElement()); + if (range) { + if (goog.editor.BrowserFeature.HAS_IE_RANGES) { + this.field_.getElement().focus(); + } + goog.dom.Range.createFromBrowserRange(range).select(); + } +}; + + +/** + * Get the range that encompases the the cursor position relative to a given + * base node. + * @param {Element} baseNode The node to get the cursor position relative to. + * @return {Range|TextRange|null} The browser range for this position. + * @private + */ +goog.editor.plugins.UndoRedo.CursorPosition_.prototype.getRange_ = + function(baseNode) { + if (goog.editor.BrowserFeature.HAS_W3C_RANGES) { + var startNode = this.startOffset_.findTargetNode(baseNode); + var endNode = this.endOffset_.findTargetNode(baseNode); + if (!startNode || !endNode) { + return null; + } + + // Create range. + return /** @type {Range} */ ( + goog.dom.Range.createFromNodes(startNode, this.startChildOffset_, + endNode, this.endChildOffset_).getBrowserRangeObject()); + } + + // Create a collapsed selection at the start of the contentEditable region, + // which the offsets were calculated relative to before. Note that we force + // a text range here so we can use moveToElementText. + var sel = baseNode.ownerDocument.body.createTextRange(); + sel.moveToElementText(baseNode); + sel.collapse(true); + sel.moveEnd('character', this.endOffset_); + sel.moveStart('character', this.startOffset_); + return sel; +}; + + +/** + * Compute the number of characters to the end of the range in IE. + * @param {TextRange} range The range to compute an offset for. + * @return {number} The number of characters to the end of the range. + * @private + */ +goog.editor.plugins.UndoRedo.CursorPosition_.computeEndOffsetIE_ = + function(range) { + var testRange = range.duplicate(); + + // The number of offset characters is a little off depending on + // what type of block elements happen to be between the start of the + // textedit and the cursor position. We fudge the offset until the + // two ranges match. + var text = range.text; + var guess = text.length; + + testRange.collapse(true); + testRange.moveEnd('character', guess); + + // Adjust the range until the end points match. This doesn't quite + // work if we're at the end of the field so we give up after a few + // iterations. + var diff; + var numTries = 10; + while (diff = testRange.compareEndPoints('EndToEnd', range)) { + guess -= diff; + testRange.moveEnd('character', -diff); + --numTries; + if (0 == numTries) { + break; + } + } + // When we set innerHTML, blank lines become a single space, causing + // the cursor position to be off by one. So we accommodate for blank + // lines. + var offset = 0; + var pos = text.indexOf('\n\r'); + while (pos != -1) { + ++offset; + pos = text.indexOf('\n\r', pos + 1); + } + return guess + offset; +}; http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/undoredomanager.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/undoredomanager.js b/externs/GCL/externs/goog/editor/plugins/undoredomanager.js new file mode 100644 index 0000000..5e054fd --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/undoredomanager.js @@ -0,0 +1,338 @@ +// Copyright 2008 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 Code for managing series of undo-redo actions in the form of + * {@link goog.editor.plugins.UndoRedoState}s. + * + */ + + +goog.provide('goog.editor.plugins.UndoRedoManager'); +goog.provide('goog.editor.plugins.UndoRedoManager.EventType'); + +goog.require('goog.editor.plugins.UndoRedoState'); +goog.require('goog.events'); +goog.require('goog.events.EventTarget'); + + + +/** + * Manages undo and redo operations through a series of {@code UndoRedoState}s + * maintained on undo and redo stacks. + * + * @constructor + * @extends {goog.events.EventTarget} + */ +goog.editor.plugins.UndoRedoManager = function() { + goog.events.EventTarget.call(this); + + /** + * The maximum number of states on the undo stack at any time. Used to limit + * the memory footprint of the undo-redo stack. + * TODO(user) have a separate memory size based limit. + * @type {number} + * @private + */ + this.maxUndoDepth_ = 100; + + /** + * The undo stack. + * @type {Array<goog.editor.plugins.UndoRedoState>} + * @private + */ + this.undoStack_ = []; + + /** + * The redo stack. + * @type {Array<goog.editor.plugins.UndoRedoState>} + * @private + */ + this.redoStack_ = []; + + /** + * A queue of pending undo or redo actions. Stored as objects with two + * properties: func and state. The func property stores the undo or redo + * function to be called, the state property stores the state that method + * came from. + * @type {Array<Object>} + * @private + */ + this.pendingActions_ = []; +}; +goog.inherits(goog.editor.plugins.UndoRedoManager, goog.events.EventTarget); + + +/** + * Event types for the events dispatched by undo-redo manager. + * @enum {string} + */ +goog.editor.plugins.UndoRedoManager.EventType = { + /** + * Signifies that he undo or redo stack transitioned between 0 and 1 states, + * meaning that the ability to peform undo or redo operations has changed. + */ + STATE_CHANGE: 'state_change', + + /** + * Signifies that a state was just added to the undo stack. Events of this + * type will have a {@code state} property whose value is the state that + * was just added. + */ + STATE_ADDED: 'state_added', + + /** + * Signifies that the undo method of a state is about to be called. + * Events of this type will have a {@code state} property whose value is the + * state whose undo action is about to be performed. If the event is cancelled + * the action does not proceed, but the state will still transition between + * stacks. + */ + BEFORE_UNDO: 'before_undo', + + /** + * Signifies that the redo method of a state is about to be called. + * Events of this type will have a {@code state} property whose value is the + * state whose redo action is about to be performed. If the event is cancelled + * the action does not proceed, but the state will still transition between + * stacks. + */ + BEFORE_REDO: 'before_redo' +}; + + +/** + * The key for the listener for the completion of the asynchronous state whose + * undo or redo action is in progress. Null if no action is in progress. + * @type {goog.events.Key} + * @private + */ +goog.editor.plugins.UndoRedoManager.prototype.inProgressActionKey_ = null; + + +/** + * Set the max undo stack depth (not the real memory usage). + * @param {number} depth Depth of the stack. + */ +goog.editor.plugins.UndoRedoManager.prototype.setMaxUndoDepth = + function(depth) { + this.maxUndoDepth_ = depth; +}; + + +/** + * Add state to the undo stack. This clears the redo stack. + * + * @param {goog.editor.plugins.UndoRedoState} state The state to add to the undo + * stack. + */ +goog.editor.plugins.UndoRedoManager.prototype.addState = function(state) { + // TODO: is the state.equals check necessary? + if (this.undoStack_.length == 0 || + !state.equals(this.undoStack_[this.undoStack_.length - 1])) { + this.undoStack_.push(state); + if (this.undoStack_.length > this.maxUndoDepth_) { + this.undoStack_.shift(); + } + // Clobber the redo stack. + var redoLength = this.redoStack_.length; + this.redoStack_.length = 0; + + this.dispatchEvent({ + type: goog.editor.plugins.UndoRedoManager.EventType.STATE_ADDED, + state: state + }); + + // If the redo state had states on it, then clobbering the redo stack above + // has caused a state change. + if (this.undoStack_.length == 1 || redoLength) { + this.dispatchStateChange_(); + } + } +}; + + +/** + * Dispatches a STATE_CHANGE event with this manager as the target. + * @private + */ +goog.editor.plugins.UndoRedoManager.prototype.dispatchStateChange_ = + function() { + this.dispatchEvent( + goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE); +}; + + +/** + * Performs the undo operation of the state at the top of the undo stack, moving + * that state to the top of the redo stack. If the undo stack is empty, does + * nothing. + */ +goog.editor.plugins.UndoRedoManager.prototype.undo = function() { + this.shiftState_(this.undoStack_, this.redoStack_); +}; + + +/** + * Performs the redo operation of the state at the top of the redo stack, moving + * that state to the top of the undo stack. If redo undo stack is empty, does + * nothing. + */ +goog.editor.plugins.UndoRedoManager.prototype.redo = function() { + this.shiftState_(this.redoStack_, this.undoStack_); +}; + + +/** + * @return {boolean} Wether the undo stack has items on it, i.e., if it is + * possible to perform an undo operation. + */ +goog.editor.plugins.UndoRedoManager.prototype.hasUndoState = function() { + return this.undoStack_.length > 0; +}; + + +/** + * @return {boolean} Wether the redo stack has items on it, i.e., if it is + * possible to perform a redo operation. + */ +goog.editor.plugins.UndoRedoManager.prototype.hasRedoState = function() { + return this.redoStack_.length > 0; +}; + + +/** + * Move a state from one stack to the other, performing the appropriate undo + * or redo action. + * + * @param {Array<goog.editor.plugins.UndoRedoState>} fromStack Stack to move + * the state from. + * @param {Array<goog.editor.plugins.UndoRedoState>} toStack Stack to move + * the state to. + * @private + */ +goog.editor.plugins.UndoRedoManager.prototype.shiftState_ = function( + fromStack, toStack) { + if (fromStack.length) { + var state = fromStack.pop(); + + // Push the current state into the redo stack. + toStack.push(state); + + this.addAction_({ + type: fromStack == this.undoStack_ ? + goog.editor.plugins.UndoRedoManager.EventType.BEFORE_UNDO : + goog.editor.plugins.UndoRedoManager.EventType.BEFORE_REDO, + func: fromStack == this.undoStack_ ? state.undo : state.redo, + state: state + }); + + // If either stack transitioned between 0 and 1 in size then the ability + // to do an undo or redo has changed and we must dispatch a state change. + if (fromStack.length == 0 || toStack.length == 1) { + this.dispatchStateChange_(); + } + } +}; + + +/** + * Adds an action to the queue of pending undo or redo actions. If no actions + * are pending, immediately performs the action. + * + * @param {Object} action An undo or redo action. Stored as an object with two + * properties: func and state. The func property stores the undo or redo + * function to be called, the state property stores the state that method + * came from. + * @private + */ +goog.editor.plugins.UndoRedoManager.prototype.addAction_ = function(action) { + this.pendingActions_.push(action); + if (this.pendingActions_.length == 1) { + this.doAction_(); + } +}; + + +/** + * Executes the action at the front of the pending actions queue. If an action + * is already in progress or the queue is empty, does nothing. + * @private + */ +goog.editor.plugins.UndoRedoManager.prototype.doAction_ = function() { + if (this.inProgressActionKey_ || this.pendingActions_.length == 0) { + return; + } + + var action = this.pendingActions_.shift(); + + var e = { + type: action.type, + state: action.state + }; + + if (this.dispatchEvent(e)) { + if (action.state.isAsynchronous()) { + this.inProgressActionKey_ = goog.events.listen(action.state, + goog.editor.plugins.UndoRedoState.ACTION_COMPLETED, + this.finishAction_, false, this); + action.func.call(action.state); + } else { + action.func.call(action.state); + this.doAction_(); + } + } +}; + + +/** + * Finishes processing the current in progress action, starting the next queued + * action if one exists. + * @private + */ +goog.editor.plugins.UndoRedoManager.prototype.finishAction_ = function() { + goog.events.unlistenByKey(/** @type {number} */ (this.inProgressActionKey_)); + this.inProgressActionKey_ = null; + this.doAction_(); +}; + + +/** + * Clears the undo and redo stacks. + */ +goog.editor.plugins.UndoRedoManager.prototype.clearHistory = function() { + if (this.undoStack_.length > 0 || this.redoStack_.length > 0) { + this.undoStack_.length = 0; + this.redoStack_.length = 0; + this.dispatchStateChange_(); + } +}; + + +/** + * @return {goog.editor.plugins.UndoRedoState|undefined} The state at the top of + * the undo stack without removing it from the stack. + */ +goog.editor.plugins.UndoRedoManager.prototype.undoPeek = function() { + return this.undoStack_[this.undoStack_.length - 1]; +}; + + +/** + * @return {goog.editor.plugins.UndoRedoState|undefined} The state at the top of + * the redo stack without removing it from the stack. + */ +goog.editor.plugins.UndoRedoManager.prototype.redoPeek = function() { + return this.redoStack_[this.redoStack_.length - 1]; +}; http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/undoredostate.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/undoredostate.js b/externs/GCL/externs/goog/editor/plugins/undoredostate.js new file mode 100644 index 0000000..9a772dd --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/undoredostate.js @@ -0,0 +1,86 @@ +// Copyright 2008 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 Code for an UndoRedoState interface representing an undo and + * redo action for a particular state change. To be used by + * {@link goog.editor.plugins.UndoRedoManager}. + * + */ + + +goog.provide('goog.editor.plugins.UndoRedoState'); + +goog.require('goog.events.EventTarget'); + + + +/** + * Represents an undo and redo action for a particular state transition. + * + * @param {boolean} asynchronous Whether the undo or redo actions for this + * state complete asynchronously. If true, then this state must fire + * an ACTION_COMPLETED event when undo or redo is complete. + * @constructor + * @extends {goog.events.EventTarget} + */ +goog.editor.plugins.UndoRedoState = function(asynchronous) { + goog.editor.plugins.UndoRedoState.base(this, 'constructor'); + + /** + * Indicates if the undo or redo actions for this state complete + * asynchronously. + * @type {boolean} + * @private + */ + this.asynchronous_ = asynchronous; +}; +goog.inherits(goog.editor.plugins.UndoRedoState, goog.events.EventTarget); + + +/** + * Event type for events indicating that this state has completed an undo or + * redo operation. + */ +goog.editor.plugins.UndoRedoState.ACTION_COMPLETED = 'action_completed'; + + +/** + * @return {boolean} Whether or not the undo and redo actions of this state + * complete asynchronously. If true, the state will fire an ACTION_COMPLETED + * event when an undo or redo action is complete. + */ +goog.editor.plugins.UndoRedoState.prototype.isAsynchronous = function() { + return this.asynchronous_; +}; + + +/** + * Undoes the action represented by this state. + */ +goog.editor.plugins.UndoRedoState.prototype.undo = goog.abstractMethod; + + +/** + * Redoes the action represented by this state. + */ +goog.editor.plugins.UndoRedoState.prototype.redo = goog.abstractMethod; + + +/** + * Checks if two undo-redo states are the same. + * @param {goog.editor.plugins.UndoRedoState} state The state to compare. + * @return {boolean} Wether the two states are equal. + */ +goog.editor.plugins.UndoRedoState.prototype.equals = goog.abstractMethod;
