// 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 Utilties for working with DOM nodes related to rich text * editing. Many of these are not general enough to go into goog.dom. * */ goog.provide('goog.editor.node'); goog.require('goog.dom'); goog.require('goog.dom.NodeType'); goog.require('goog.dom.TagName'); goog.require('goog.dom.iter.ChildIterator'); goog.require('goog.dom.iter.SiblingIterator'); goog.require('goog.iter'); goog.require('goog.object'); goog.require('goog.string'); goog.require('goog.string.Unicode'); /** * Names of all block-level tags * @type {Object} * @private */ goog.editor.node.BLOCK_TAG_NAMES_ = goog.object.createSet( 'ADDRESS', 'BLOCKQUOTE', 'BODY', 'CAPTION', 'CENTER', 'COL', 'COLGROUP', 'DIR', 'DIV', 'DL', 'DD', 'DT', 'FIELDSET', 'FORM', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HR', 'ISINDEX', 'OL', 'LI', 'MAP', 'MENU', 'OPTGROUP', 'OPTION', 'P', 'PRE', 'TABLE', 'TBODY', 'TD', 'TFOOT', 'TH', 'THEAD', 'TR', 'TL', 'UL'); /** * Names of tags that have intrinsic content. * TODO(robbyw): What about object, br, input, textarea, button, isindex, * hr, keygen, select, table, tr, td? * @type {Object} * @private */ goog.editor.node.NON_EMPTY_TAGS_ = goog.object.createSet( goog.dom.TagName.IMG, goog.dom.TagName.IFRAME, 'EMBED'); /** * Check if the node is in a standards mode document. * @param {Node} node The node to test. * @return {boolean} Whether the node is in a standards mode document. */ goog.editor.node.isStandardsMode = function(node) { return goog.dom.getDomHelper(node).isCss1CompatMode(); }; /** * Get the right-most non-ignorable leaf node of the given node. * @param {Node} parent The parent ndoe. * @return {Node} The right-most non-ignorable leaf node. */ goog.editor.node.getRightMostLeaf = function(parent) { var temp; while (temp = goog.editor.node.getLastChild(parent)) { parent = temp; } return parent; }; /** * Get the left-most non-ignorable leaf node of the given node. * @param {Node} parent The parent ndoe. * @return {Node} The left-most non-ignorable leaf node. */ goog.editor.node.getLeftMostLeaf = function(parent) { var temp; while (temp = goog.editor.node.getFirstChild(parent)) { parent = temp; } return parent; }; /** * Version of firstChild that skips nodes that are entirely * whitespace and comments. * @param {Node} parent The reference node. * @return {Node} The first child of sibling that is important according to * goog.editor.node.isImportant, or null if no such node exists. */ goog.editor.node.getFirstChild = function(parent) { return goog.editor.node.getChildHelper_(parent, false); }; /** * Version of lastChild that skips nodes that are entirely whitespace or * comments. (Normally lastChild is a property of all DOM nodes that gives the * last of the nodes contained directly in the reference node.) * @param {Node} parent The reference node. * @return {Node} The last child of sibling that is important according to * goog.editor.node.isImportant, or null if no such node exists. */ goog.editor.node.getLastChild = function(parent) { return goog.editor.node.getChildHelper_(parent, true); }; /** * Version of previoussibling that skips nodes that are entirely * whitespace or comments. (Normally previousSibling is a property * of all DOM nodes that gives the sibling node, the node that is * a child of the same parent, that occurs immediately before the * reference node.) * @param {Node} sibling The reference node. * @return {Node} The closest previous sibling to sibling that is * important according to goog.editor.node.isImportant, or null if no such * node exists. */ goog.editor.node.getPreviousSibling = function(sibling) { return /** @type {Node} */ (goog.editor.node.getFirstValue_( goog.iter.filter(new goog.dom.iter.SiblingIterator(sibling, false, true), goog.editor.node.isImportant))); }; /** * Version of nextSibling that skips nodes that are entirely whitespace or * comments. * @param {Node} sibling The reference node. * @return {Node} The closest next sibling to sibling that is important * according to goog.editor.node.isImportant, or null if no * such node exists. */ goog.editor.node.getNextSibling = function(sibling) { return /** @type {Node} */ (goog.editor.node.getFirstValue_( goog.iter.filter(new goog.dom.iter.SiblingIterator(sibling), goog.editor.node.isImportant))); }; /** * Internal helper for lastChild/firstChild that skips nodes that are entirely * whitespace or comments. * @param {Node} parent The reference node. * @param {boolean} isReversed Whether children should be traversed forward * or backward. * @return {Node} The first/last child of sibling that is important according * to goog.editor.node.isImportant, or null if no such node exists. * @private */ goog.editor.node.getChildHelper_ = function(parent, isReversed) { return (!parent || parent.nodeType != goog.dom.NodeType.ELEMENT) ? null : /** @type {Node} */ (goog.editor.node.getFirstValue_(goog.iter.filter( new goog.dom.iter.ChildIterator( /** @type {Element} */ (parent), isReversed), goog.editor.node.isImportant))); }; /** * Utility function that returns the first value from an iterator or null if * the iterator is empty. * @param {goog.iter.Iterator} iterator The iterator to get a value from. * @return {*} The first value from the iterator. * @private */ goog.editor.node.getFirstValue_ = function(iterator) { /** @preserveTry */ try { return iterator.next(); } catch (e) { return null; } }; /** * Determine if a node should be returned by the iterator functions. * @param {Node} node An object implementing the DOM1 Node interface. * @return {boolean} Whether the node is an element, or a text node that * is not all whitespace. */ goog.editor.node.isImportant = function(node) { // Return true if the node is not either a TextNode or an ElementNode. return node.nodeType == goog.dom.NodeType.ELEMENT || node.nodeType == goog.dom.NodeType.TEXT && !goog.editor.node.isAllNonNbspWhiteSpace(node); }; /** * Determine whether a node's text content is entirely whitespace. * @param {Node} textNode A node implementing the CharacterData interface (i.e., * a Text, Comment, or CDATASection node. * @return {boolean} Whether the text content of node is whitespace, * otherwise false. */ goog.editor.node.isAllNonNbspWhiteSpace = function(textNode) { return goog.string.isBreakingWhitespace(textNode.nodeValue); }; /** * Returns true if the node contains only whitespace and is not and does not * contain any images, iframes or embed tags. * @param {Node} node The node to check. * @param {boolean=} opt_prohibitSingleNbsp By default, this function treats a * single nbsp as empty. Set this to true to treat this case as non-empty. * @return {boolean} Whether the node contains only whitespace. */ goog.editor.node.isEmpty = function(node, opt_prohibitSingleNbsp) { var nodeData = goog.dom.getRawTextContent(node); if (node.getElementsByTagName) { for (var tag in goog.editor.node.NON_EMPTY_TAGS_) { if (node.tagName == tag || node.getElementsByTagName(tag).length > 0) { return false; } } } return (!opt_prohibitSingleNbsp && nodeData == goog.string.Unicode.NBSP) || goog.string.isBreakingWhitespace(nodeData); }; /** * Returns the length of the text in node if it is a text node, or the number * of children of the node, if it is an element. Useful for range-manipulation * code where you need to know the offset for the right side of the node. * @param {Node} node The node to get the length of. * @return {number} The length of the node. */ goog.editor.node.getLength = function(node) { return node.length || node.childNodes.length; }; /** * Search child nodes using a predicate function and return the first node that * satisfies the condition. * @param {Node} parent The parent node to search. * @param {function(Node):boolean} hasProperty A function that takes a child * node as a parameter and returns true if it meets the criteria. * @return {?number} The index of the node found, or null if no node is found. */ goog.editor.node.findInChildren = function(parent, hasProperty) { for (var i = 0, len = parent.childNodes.length; i < len; i++) { if (hasProperty(parent.childNodes[i])) { return i; } } return null; }; /** * Search ancestor nodes using a predicate function and returns the topmost * ancestor in the chain of consecutive ancestors that satisfies the condition. * * @param {Node} node The node whose ancestors have to be searched. * @param {function(Node): boolean} hasProperty A function that takes a parent * node as a parameter and returns true if it meets the criteria. * @return {Node} The topmost ancestor or null if no ancestor satisfies the * predicate function. */ goog.editor.node.findHighestMatchingAncestor = function(node, hasProperty) { var parent = node.parentNode; var ancestor = null; while (parent && hasProperty(parent)) { ancestor = parent; parent = parent.parentNode; } return ancestor; }; /** * Checks if node is a block-level html element. The display css * property is ignored. * @param {Node} node The node to test. * @return {boolean} Whether the node is a block-level node. */ goog.editor.node.isBlockTag = function(node) { return !!goog.editor.node.BLOCK_TAG_NAMES_[node.tagName]; }; /** * Skips siblings of a node that are empty text nodes. * @param {Node} node A node. May be null. * @return {Node} The node or the first sibling of the node that is not an * empty text node. May be null. */ goog.editor.node.skipEmptyTextNodes = function(node) { while (node && node.nodeType == goog.dom.NodeType.TEXT && !node.nodeValue) { node = node.nextSibling; } return node; }; /** * Checks if an element is a top-level editable container (meaning that * it itself is not editable, but all its child nodes are editable). * @param {Node} element The element to test. * @return {boolean} Whether the element is a top-level editable container. */ goog.editor.node.isEditableContainer = function(element) { return element.getAttribute && element.getAttribute('g_editable') == 'true'; }; /** * Checks if a node is inside an editable container. * @param {Node} node The node to test. * @return {boolean} Whether the node is in an editable container. */ goog.editor.node.isEditable = function(node) { return !!goog.dom.getAncestor(node, goog.editor.node.isEditableContainer); }; /** * Finds the top-most DOM node inside an editable field that is an ancestor * (or self) of a given DOM node and meets the specified criteria. * @param {Node} node The DOM node where the search starts. * @param {function(Node) : boolean} criteria A function that takes a DOM node * as a parameter and returns a boolean to indicate whether the node meets * the criteria or not. * @return {Node} The DOM node if found, or null. */ goog.editor.node.findTopMostEditableAncestor = function(node, criteria) { var targetNode = null; while (node && !goog.editor.node.isEditableContainer(node)) { if (criteria(node)) { targetNode = node; } node = node.parentNode; } return targetNode; }; /** * Splits off a subtree. * @param {!Node} currentNode The starting splitting point. * @param {Node=} opt_secondHalf The initial leftmost leaf the new subtree. * If null, siblings after currentNode will be placed in the subtree, but * no additional node will be. * @param {Node=} opt_root The top of the tree where splitting stops at. * @return {!Node} The new subtree. */ goog.editor.node.splitDomTreeAt = function(currentNode, opt_secondHalf, opt_root) { var parent; while (currentNode != opt_root && (parent = currentNode.parentNode)) { opt_secondHalf = goog.editor.node.getSecondHalfOfNode_(parent, currentNode, opt_secondHalf); currentNode = parent; } return /** @type {!Node} */(opt_secondHalf); }; /** * Creates a clone of node, moving all children after startNode to it. * When firstChild is not null or undefined, it is also appended to the clone * as the first child. * @param {!Node} node The node to clone. * @param {!Node} startNode All siblings after this node will be moved to the * clone. * @param {Node|undefined} firstChild The first child of the new cloned element. * @return {!Node} The cloned node that now contains the children after * startNode. * @private */ goog.editor.node.getSecondHalfOfNode_ = function(node, startNode, firstChild) { var secondHalf = /** @type {!Node} */(node.cloneNode(false)); while (startNode.nextSibling) { goog.dom.appendChild(secondHalf, startNode.nextSibling); } if (firstChild) { secondHalf.insertBefore(firstChild, secondHalf.firstChild); } return secondHalf; }; /** * Appends all of oldNode's children to newNode. This removes all children from * oldNode and appends them to newNode. oldNode is left with no children. * @param {!Node} newNode Node to transfer children to. * @param {Node} oldNode Node to transfer children from. * @deprecated Use goog.dom.append directly instead. */ goog.editor.node.transferChildren = function(newNode, oldNode) { goog.dom.append(newNode, oldNode.childNodes); };