Path: blob/trunk/third_party/closure/goog/editor/plugins/blockquote.js
2868 views
// Copyright 2008 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 goog.editor plugin to handle splitting block quotes.16*17* @author [email protected] (Robby Walker)18*/1920goog.provide('goog.editor.plugins.Blockquote');2122goog.require('goog.dom');23goog.require('goog.dom.NodeType');24goog.require('goog.dom.TagName');25goog.require('goog.dom.classlist');26goog.require('goog.editor.BrowserFeature');27goog.require('goog.editor.Command');28goog.require('goog.editor.Plugin');29goog.require('goog.editor.node');30goog.require('goog.functions');31goog.require('goog.log');32333435/**36* Plugin to handle splitting block quotes. This plugin does nothing on its37* own and should be used in conjunction with EnterHandler or one of its38* subclasses.39* @param {boolean} requiresClassNameToSplit Whether to split only blockquotes40* that have the given classname.41* @param {string=} opt_className The classname to apply to generated42* blockquotes. Defaults to 'tr_bq'.43* @constructor44* @extends {goog.editor.Plugin}45* @final46*/47goog.editor.plugins.Blockquote = function(48requiresClassNameToSplit, opt_className) {49goog.editor.Plugin.call(this);5051/**52* Whether we only split blockquotes that have {@link classname}, or whether53* all blockquote tags should be split on enter.54* @type {boolean}55* @private56*/57this.requiresClassNameToSplit_ = requiresClassNameToSplit;5859/**60* Classname to put on blockquotes that are generated via the toolbar for61* blockquote, so that we can internally distinguish these from blockquotes62* that are used for indentation. This classname can be over-ridden by63* clients for styling or other purposes.64* @type {string}65* @private66*/67this.className_ = opt_className || goog.getCssName('tr_bq');68};69goog.inherits(goog.editor.plugins.Blockquote, goog.editor.Plugin);707172/**73* Command implemented by this plugin.74* @type {string}75*/76goog.editor.plugins.Blockquote.SPLIT_COMMAND = '+splitBlockquote';777879/**80* Class ID used to identify this plugin.81* @type {string}82*/83goog.editor.plugins.Blockquote.CLASS_ID = 'Blockquote';848586/**87* Logging object.88* @type {goog.log.Logger}89* @protected90* @override91*/92goog.editor.plugins.Blockquote.prototype.logger =93goog.log.getLogger('goog.editor.plugins.Blockquote');949596/** @override */97goog.editor.plugins.Blockquote.prototype.getTrogClassId = function() {98return goog.editor.plugins.Blockquote.CLASS_ID;99};100101102/**103* Since our exec command is always called from elsewhere, we make it silent.104* @override105*/106goog.editor.plugins.Blockquote.prototype.isSilentCommand = goog.functions.TRUE;107108109/**110* Checks if a node is a blockquote which can be split. A splittable blockquote111* meets the following criteria:112* <ol>113* <li>Node is a blockquote element</li>114* <li>Node has the blockquote classname if the classname is required to115* split</li>116* </ol>117*118* @param {Node} node DOM node in question.119* @return {boolean} Whether the node is a splittable blockquote.120*/121goog.editor.plugins.Blockquote.prototype.isSplittableBlockquote = function(122node) {123if (/** @type {!Element} */ (node).tagName != goog.dom.TagName.BLOCKQUOTE) {124return false;125}126127if (!this.requiresClassNameToSplit_) {128return true;129}130131return goog.dom.classlist.contains(132/** @type {!Element} */ (node), this.className_);133};134135136/**137* Checks if a node is a blockquote element which has been setup.138* @param {Node} node DOM node to check.139* @return {boolean} Whether the node is a blockquote with the required class140* name applied.141*/142goog.editor.plugins.Blockquote.prototype.isSetupBlockquote = function(node) {143return /** @type {!Element} */ (node).tagName ==144goog.dom.TagName.BLOCKQUOTE &&145goog.dom.classlist.contains(146/** @type {!Element} */ (node), this.className_);147};148149150/**151* Checks if a node is a blockquote element which has not been setup yet.152* @param {Node} node DOM node to check.153* @return {boolean} Whether the node is a blockquote without the required154* class name applied.155*/156goog.editor.plugins.Blockquote.prototype.isUnsetupBlockquote = function(node) {157return /** @type {!Element} */ (node).tagName ==158goog.dom.TagName.BLOCKQUOTE &&159!this.isSetupBlockquote(node);160};161162163/**164* Gets the class name required for setup blockquotes.165* @return {string} The blockquote class name.166*/167goog.editor.plugins.Blockquote.prototype.getBlockquoteClassName = function() {168return this.className_;169};170171172/**173* Helper routine which walks up the tree to find the topmost174* ancestor with only a single child. The ancestor node or the original175* node (if no ancestor was found) is then removed from the DOM.176*177* @param {Node} node The node whose ancestors have to be searched.178* @param {Node} root The root node to stop the search at.179* @private180*/181goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_ = function(182node, root) {183var predicateFunc = function(parentNode) {184return parentNode != root && parentNode.childNodes.length == 1;185};186var ancestor =187goog.editor.node.findHighestMatchingAncestor(node, predicateFunc);188if (!ancestor) {189ancestor = node;190}191goog.dom.removeNode(ancestor);192};193194195/**196* Remove every nodes from the DOM tree that are all white space nodes.197* @param {Array<Node>} nodes Nodes to be checked.198* @private199*/200goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_ = function(nodes) {201for (var i = 0; i < nodes.length; ++i) {202if (goog.editor.node.isEmpty(nodes[i], true)) {203goog.dom.removeNode(nodes[i]);204}205}206};207208209/** @override */210goog.editor.plugins.Blockquote.prototype.isSupportedCommand = function(211command) {212return command == goog.editor.plugins.Blockquote.SPLIT_COMMAND;213};214215216/**217* Splits a quoted region if any. To be called on a key press event. When this218* function returns true, the event that caused it to be called should be219* canceled.220* @param {string} command The command to execute.221* @param {...*} var_args Single additional argument representing the current222* cursor position. If BrowserFeature.HAS_W3C_RANGES it is an object with a223* {@code node} key and an {@code offset} key. In other cases (legacy IE)224* it is a single node.225* @return {boolean|undefined} Boolean true when the quoted region has been226* split, false or undefined otherwise.227* @override228*/229goog.editor.plugins.Blockquote.prototype.execCommandInternal = function(230command, var_args) {231var pos = arguments[1];232if (command == goog.editor.plugins.Blockquote.SPLIT_COMMAND && pos &&233(this.className_ || !this.requiresClassNameToSplit_)) {234return goog.editor.BrowserFeature.HAS_W3C_RANGES ?235this.splitQuotedBlockW3C_(pos) :236this.splitQuotedBlockIE_(/** @type {Node} */ (pos));237}238};239240241/**242* Version of splitQuotedBlock_ that uses W3C ranges.243* @param {Object} anchorPos The current cursor position.244* @return {boolean} Whether the blockquote was split.245* @private246*/247goog.editor.plugins.Blockquote.prototype.splitQuotedBlockW3C_ = function(248anchorPos) {249var cursorNode = anchorPos.node;250var quoteNode = goog.editor.node.findTopMostEditableAncestor(251cursorNode.parentNode, goog.bind(this.isSplittableBlockquote, this));252253var secondHalf, textNodeToRemove;254var insertTextNode = false;255// There are two special conditions that we account for here.256//257// 1. Whenever the cursor is after (one<BR>|) or just before a BR element258// (one|<BR>) and the user presses enter, the second quoted block starts259// with a BR which appears to the user as an extra newline. This stems260// from the fact that we create two text nodes as our split boundaries261// and the BR becomes a part of the second half because of this.262//263// 2. When the cursor is at the end of a text node with no siblings and264// the user presses enter, the second blockquote might contain a265// empty subtree that ends in a 0 length text node. We account for that266// as a post-splitting operation.267if (quoteNode) {268// selection is in a line that has text in it269if (cursorNode.nodeType == goog.dom.NodeType.TEXT) {270if (anchorPos.offset == cursorNode.length) {271var siblingNode = cursorNode.nextSibling;272273// This accounts for the condition where the cursor appears at the274// end of a text node and right before the BR eg: one|<BR>. We ensure275// that we split on the BR in that case.276if (siblingNode && siblingNode.tagName == goog.dom.TagName.BR) {277cursorNode = siblingNode;278// This might be null but splitDomTreeAt accounts for the null case.279secondHalf = siblingNode.nextSibling;280} else {281textNodeToRemove = cursorNode.splitText(anchorPos.offset);282secondHalf = textNodeToRemove;283}284} else {285secondHalf = cursorNode.splitText(anchorPos.offset);286}287} else if (cursorNode.tagName == goog.dom.TagName.BR) {288// This might be null but splitDomTreeAt accounts for the null case.289secondHalf = cursorNode.nextSibling;290} else {291// The selection is in a line that is empty, with more than 1 level292// of quote.293insertTextNode = true;294}295} else {296// Check if current node is a quote node.297// This will happen if user clicks in an empty line in the quote,298// when there is 1 level of quote.299if (this.isSetupBlockquote(cursorNode)) {300quoteNode = cursorNode;301insertTextNode = true;302}303}304305if (insertTextNode) {306// Create two empty text nodes to split between.307cursorNode = this.insertEmptyTextNodeBeforeRange_();308secondHalf = this.insertEmptyTextNodeBeforeRange_();309}310311if (!quoteNode) {312return false;313}314315secondHalf =316goog.editor.node.splitDomTreeAt(cursorNode, secondHalf, quoteNode);317goog.dom.insertSiblingAfter(secondHalf, quoteNode);318319// Set the insertion point.320var dh = this.getFieldDomHelper();321var tagToInsert = this.getFieldObject().queryCommandValue(322goog.editor.Command.DEFAULT_TAG) ||323goog.dom.TagName.DIV;324var container = dh.createElement(/** @type {string} */ (tagToInsert));325container.innerHTML = ' '; // Prevent the div from collapsing.326quoteNode.parentNode.insertBefore(container, secondHalf);327dh.getWindow().getSelection().collapse(container, 0);328329// We need to account for the condition where the second blockquote330// might contain an empty DOM tree. This arises from trying to split331// at the end of an empty text node. We resolve this by walking up the tree332// till we either reach the blockquote or till we hit a node with more333// than one child. The resulting node is then removed from the DOM.334if (textNodeToRemove) {335goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(336textNodeToRemove, secondHalf);337}338339goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(340[quoteNode, secondHalf]);341return true;342};343344345/**346* Inserts an empty text node before the field's range.347* @return {!Node} The empty text node.348* @private349*/350goog.editor.plugins.Blockquote.prototype.insertEmptyTextNodeBeforeRange_ =351function() {352var range = this.getFieldObject().getRange();353var node = this.getFieldDomHelper().createTextNode('');354range.insertNode(node, true);355return node;356};357358359/**360* IE version of splitQuotedBlock_.361* @param {Node} splitNode The current cursor position.362* @return {boolean} Whether the blockquote was split.363* @private364*/365goog.editor.plugins.Blockquote.prototype.splitQuotedBlockIE_ = function(366splitNode) {367var dh = this.getFieldDomHelper();368var quoteNode = goog.editor.node.findTopMostEditableAncestor(369splitNode.parentNode, goog.bind(this.isSplittableBlockquote, this));370371if (!quoteNode) {372return false;373}374375var clone = splitNode.cloneNode(false);376377// Whenever the cursor is just before a BR element (one|<BR>) and the user378// presses enter, the second quoted block starts with a BR which appears379// to the user as an extra newline. This stems from the fact that the380// dummy span that we create (splitNode) occurs before the BR and we split381// on that.382if (splitNode.nextSibling &&383/** @type {!Element} */ (splitNode.nextSibling).tagName ==384goog.dom.TagName.BR) {385splitNode = splitNode.nextSibling;386}387var secondHalf = goog.editor.node.splitDomTreeAt(splitNode, clone, quoteNode);388goog.dom.insertSiblingAfter(secondHalf, quoteNode);389390// Set insertion point.391var tagToInsert = this.getFieldObject().queryCommandValue(392goog.editor.Command.DEFAULT_TAG) ||393goog.dom.TagName.DIV;394var div = dh.createElement(/** @type {string} */ (tagToInsert));395quoteNode.parentNode.insertBefore(div, secondHalf);396397// The div needs non-whitespace contents in order for the insertion point398// to get correctly inserted.399div.innerHTML = ' ';400401// Moving the range 1 char isn't enough when you have markup.402// This moves the range to the end of the nbsp.403var range = dh.getDocument().selection.createRange();404range.moveToElementText(splitNode);405range.move('character', 2);406range.select();407408// Remove the no-longer-necessary nbsp.409goog.dom.removeChildren(div);410411// Clear the original selection.412range.pasteHTML('');413414// We need to remove clone from the DOM but just removing clone alone will415// not suffice. Let's assume we have the following DOM structure and the416// cursor is placed after the first numbered list item "one".417//418// <blockquote class="gmail-quote">419// <div><div>a</div><ol><li>one|</li></ol></div>420// <div>b</div>421// </blockquote>422//423// After pressing enter, we have the following structure.424//425// <blockquote class="gmail-quote">426// <div><div>a</div><ol><li>one|</li></ol></div>427// </blockquote>428// <div> </div>429// <blockquote class="gmail-quote">430// <div><ol><li><span id=""></span></li></ol></div>431// <div>b</div>432// </blockquote>433//434// The clone is contained in a subtree which should be removed. This stems435// from the fact that we invoke splitDomTreeAt with the dummy span436// as the starting splitting point and this results in the empty subtree437// <div><ol><li><span id=""></span></li></ol></div>.438//439// We resolve this by walking up the tree till we either reach the440// blockquote or till we hit a node with more than one child. The resulting441// node is then removed from the DOM.442goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(443clone, secondHalf);444445goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(446[quoteNode, secondHalf]);447return true;448};449450451