Path: blob/trunk/third_party/closure/goog/editor/node.js
2868 views
// Copyright 2005 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 Utilties for working with DOM nodes related to rich text16* editing. Many of these are not general enough to go into goog.dom.17*18* @author [email protected] (Nick Santos)19*/2021goog.provide('goog.editor.node');2223goog.require('goog.dom');24goog.require('goog.dom.NodeType');25goog.require('goog.dom.TagName');26goog.require('goog.dom.iter.ChildIterator');27goog.require('goog.dom.iter.SiblingIterator');28goog.require('goog.iter');29goog.require('goog.object');30goog.require('goog.string');31goog.require('goog.string.Unicode');32goog.require('goog.userAgent');333435/**36* Names of all block-level tags37* @type {Object}38* @private39*/40goog.editor.node.BLOCK_TAG_NAMES_ = goog.object.createSet(41goog.dom.TagName.ADDRESS, goog.dom.TagName.ARTICLE, goog.dom.TagName.ASIDE,42goog.dom.TagName.BLOCKQUOTE, goog.dom.TagName.BODY,43goog.dom.TagName.CAPTION, goog.dom.TagName.CENTER, goog.dom.TagName.COL,44goog.dom.TagName.COLGROUP, goog.dom.TagName.DETAILS, goog.dom.TagName.DIR,45goog.dom.TagName.DIV, goog.dom.TagName.DL, goog.dom.TagName.DD,46goog.dom.TagName.DT, goog.dom.TagName.FIELDSET, goog.dom.TagName.FIGCAPTION,47goog.dom.TagName.FIGURE, goog.dom.TagName.FOOTER, goog.dom.TagName.FORM,48goog.dom.TagName.H1, goog.dom.TagName.H2, goog.dom.TagName.H3,49goog.dom.TagName.H4, goog.dom.TagName.H5, goog.dom.TagName.H6,50goog.dom.TagName.HEADER, goog.dom.TagName.HGROUP, goog.dom.TagName.HR,51goog.dom.TagName.ISINDEX, goog.dom.TagName.OL, goog.dom.TagName.LI,52goog.dom.TagName.MAP, goog.dom.TagName.MENU, goog.dom.TagName.NAV,53goog.dom.TagName.OPTGROUP, goog.dom.TagName.OPTION, goog.dom.TagName.P,54goog.dom.TagName.PRE, goog.dom.TagName.SECTION, goog.dom.TagName.SUMMARY,55goog.dom.TagName.TABLE, goog.dom.TagName.TBODY, goog.dom.TagName.TD,56goog.dom.TagName.TFOOT, goog.dom.TagName.TH, goog.dom.TagName.THEAD,57goog.dom.TagName.TR, goog.dom.TagName.UL);585960/**61* Names of tags that have intrinsic content.62* TODO(robbyw): What about object, br, input, textarea, button, isindex,63* hr, keygen, select, table, tr, td?64* @type {Object}65* @private66*/67goog.editor.node.NON_EMPTY_TAGS_ = goog.object.createSet(68goog.dom.TagName.IMG, goog.dom.TagName.IFRAME, goog.dom.TagName.EMBED);697071/**72* Check if the node is in a standards mode document.73* @param {Node} node The node to test.74* @return {boolean} Whether the node is in a standards mode document.75*/76goog.editor.node.isStandardsMode = function(node) {77return goog.dom.getDomHelper(node).isCss1CompatMode();78};798081/**82* Get the right-most non-ignorable leaf node of the given node.83* @param {Node} parent The parent ndoe.84* @return {Node} The right-most non-ignorable leaf node.85*/86goog.editor.node.getRightMostLeaf = function(parent) {87var temp;88while (temp = goog.editor.node.getLastChild(parent)) {89parent = temp;90}91return parent;92};939495/**96* Get the left-most non-ignorable leaf node of the given node.97* @param {Node} parent The parent ndoe.98* @return {Node} The left-most non-ignorable leaf node.99*/100goog.editor.node.getLeftMostLeaf = function(parent) {101var temp;102while (temp = goog.editor.node.getFirstChild(parent)) {103parent = temp;104}105return parent;106};107108109/**110* Version of firstChild that skips nodes that are entirely111* whitespace and comments.112* @param {Node} parent The reference node.113* @return {Node} The first child of sibling that is important according to114* goog.editor.node.isImportant, or null if no such node exists.115*/116goog.editor.node.getFirstChild = function(parent) {117return goog.editor.node.getChildHelper_(parent, false);118};119120121/**122* Version of lastChild that skips nodes that are entirely whitespace or123* comments. (Normally lastChild is a property of all DOM nodes that gives the124* last of the nodes contained directly in the reference node.)125* @param {Node} parent The reference node.126* @return {Node} The last child of sibling that is important according to127* goog.editor.node.isImportant, or null if no such node exists.128*/129goog.editor.node.getLastChild = function(parent) {130return goog.editor.node.getChildHelper_(parent, true);131};132133134/**135* Version of previoussibling that skips nodes that are entirely136* whitespace or comments. (Normally previousSibling is a property137* of all DOM nodes that gives the sibling node, the node that is138* a child of the same parent, that occurs immediately before the139* reference node.)140* @param {Node} sibling The reference node.141* @return {Node} The closest previous sibling to sibling that is142* important according to goog.editor.node.isImportant, or null if no such143* node exists.144*/145goog.editor.node.getPreviousSibling = function(sibling) {146return /** @type {Node} */ (147goog.editor.node.getFirstValue_(148goog.iter.filter(149new goog.dom.iter.SiblingIterator(sibling, false, true),150goog.editor.node.isImportant)));151};152153154/**155* Version of nextSibling that skips nodes that are entirely whitespace or156* comments.157* @param {Node} sibling The reference node.158* @return {Node} The closest next sibling to sibling that is important159* according to goog.editor.node.isImportant, or null if no160* such node exists.161*/162goog.editor.node.getNextSibling = function(sibling) {163return /** @type {Node} */ (164goog.editor.node.getFirstValue_(165goog.iter.filter(166new goog.dom.iter.SiblingIterator(sibling),167goog.editor.node.isImportant)));168};169170171/**172* Internal helper for lastChild/firstChild that skips nodes that are entirely173* whitespace or comments.174* @param {Node} parent The reference node.175* @param {boolean} isReversed Whether children should be traversed forward176* or backward.177* @return {Node} The first/last child of sibling that is important according178* to goog.editor.node.isImportant, or null if no such node exists.179* @private180*/181goog.editor.node.getChildHelper_ = function(parent, isReversed) {182return (!parent || parent.nodeType != goog.dom.NodeType.ELEMENT) ?183null :184/** @type {Node} */ (185goog.editor.node.getFirstValue_(186goog.iter.filter(187new goog.dom.iter.ChildIterator(188/** @type {!Element} */ (parent), isReversed),189goog.editor.node.isImportant)));190};191192193/**194* Utility function that returns the first value from an iterator or null if195* the iterator is empty.196* @param {goog.iter.Iterator} iterator The iterator to get a value from.197* @return {*} The first value from the iterator.198* @private199*/200goog.editor.node.getFirstValue_ = function(iterator) {201202try {203return iterator.next();204} catch (e) {205return null;206}207};208209210/**211* Determine if a node should be returned by the iterator functions.212* @param {Node} node An object implementing the DOM1 Node interface.213* @return {boolean} Whether the node is an element, or a text node that214* is not all whitespace.215*/216goog.editor.node.isImportant = function(node) {217// Return true if the node is not either a TextNode or an ElementNode.218return node.nodeType == goog.dom.NodeType.ELEMENT ||219node.nodeType == goog.dom.NodeType.TEXT &&220!goog.editor.node.isAllNonNbspWhiteSpace(node);221};222223224/**225* Determine whether a node's text content is entirely whitespace.226* @param {Node} textNode A node implementing the CharacterData interface (i.e.,227* a Text, Comment, or CDATASection node.228* @return {boolean} Whether the text content of node is whitespace,229* otherwise false.230*/231goog.editor.node.isAllNonNbspWhiteSpace = function(textNode) {232return goog.string.isBreakingWhitespace(textNode.nodeValue);233};234235236/**237* Returns true if the node contains only whitespace and is not and does not238* contain any images, iframes or embed tags.239* @param {Node} node The node to check.240* @param {boolean=} opt_prohibitSingleNbsp By default, this function treats a241* single nbsp as empty. Set this to true to treat this case as non-empty.242* @return {boolean} Whether the node contains only whitespace.243*/244goog.editor.node.isEmpty = function(node, opt_prohibitSingleNbsp) {245var nodeData = goog.dom.getRawTextContent(node);246247if (node.getElementsByTagName) {248node = /** @type {!Element} */ (node);249for (var tag in goog.editor.node.NON_EMPTY_TAGS_) {250if (node.tagName == tag || node.getElementsByTagName(tag).length > 0) {251return false;252}253}254}255return (!opt_prohibitSingleNbsp && nodeData == goog.string.Unicode.NBSP) ||256goog.string.isBreakingWhitespace(nodeData);257};258259260/**261* Returns the length of the text in node if it is a text node, or the number262* of children of the node, if it is an element. Useful for range-manipulation263* code where you need to know the offset for the right side of the node.264* @param {Node} node The node to get the length of.265* @return {number} The length of the node.266*/267goog.editor.node.getLength = function(node) {268return node.length || node.childNodes.length;269};270271272/**273* Search child nodes using a predicate function and return the first node that274* satisfies the condition.275* @param {Node} parent The parent node to search.276* @param {function(Node):boolean} hasProperty A function that takes a child277* node as a parameter and returns true if it meets the criteria.278* @return {?number} The index of the node found, or null if no node is found.279*/280goog.editor.node.findInChildren = function(parent, hasProperty) {281for (var i = 0, len = parent.childNodes.length; i < len; i++) {282if (hasProperty(parent.childNodes[i])) {283return i;284}285}286return null;287};288289290/**291* Search ancestor nodes using a predicate function and returns the topmost292* ancestor in the chain of consecutive ancestors that satisfies the condition.293*294* @param {Node} node The node whose ancestors have to be searched.295* @param {function(Node): boolean} hasProperty A function that takes a parent296* node as a parameter and returns true if it meets the criteria.297* @return {Node} The topmost ancestor or null if no ancestor satisfies the298* predicate function.299*/300goog.editor.node.findHighestMatchingAncestor = function(node, hasProperty) {301var parent = node.parentNode;302var ancestor = null;303while (parent && hasProperty(parent)) {304ancestor = parent;305parent = parent.parentNode;306}307return ancestor;308};309310311/**312* Checks if node is a block-level html element. The <tt>display</tt> css313* property is ignored.314* @param {Node} node The node to test.315* @return {boolean} Whether the node is a block-level node.316*/317goog.editor.node.isBlockTag = function(node) {318return !!goog.editor.node.BLOCK_TAG_NAMES_[319/** @type {!Element} */ (node).tagName];320};321322323/**324* Skips siblings of a node that are empty text nodes.325* @param {Node} node A node. May be null.326* @return {Node} The node or the first sibling of the node that is not an327* empty text node. May be null.328*/329goog.editor.node.skipEmptyTextNodes = function(node) {330while (node && node.nodeType == goog.dom.NodeType.TEXT && !node.nodeValue) {331node = node.nextSibling;332}333return node;334};335336337/**338* Checks if an element is a top-level editable container (meaning that339* it itself is not editable, but all its child nodes are editable).340* @param {Node} element The element to test.341* @return {boolean} Whether the element is a top-level editable container.342*/343goog.editor.node.isEditableContainer = function(element) {344return element.getAttribute && element.getAttribute('g_editable') == 'true';345};346347348/**349* Checks if a node is inside an editable container.350* @param {Node} node The node to test.351* @return {boolean} Whether the node is in an editable container.352*/353goog.editor.node.isEditable = function(node) {354return !!goog.dom.getAncestor(node, goog.editor.node.isEditableContainer);355};356357358/**359* Finds the top-most DOM node inside an editable field that is an ancestor360* (or self) of a given DOM node and meets the specified criteria.361* @param {Node} node The DOM node where the search starts.362* @param {function(Node) : boolean} criteria A function that takes a DOM node363* as a parameter and returns a boolean to indicate whether the node meets364* the criteria or not.365* @return {Node} The DOM node if found, or null.366*/367goog.editor.node.findTopMostEditableAncestor = function(node, criteria) {368var targetNode = null;369while (node && !goog.editor.node.isEditableContainer(node)) {370if (criteria(node)) {371targetNode = node;372}373node = node.parentNode;374}375return targetNode;376};377378379/**380* Splits off a subtree.381* @param {!Node} currentNode The starting splitting point.382* @param {Node=} opt_secondHalf The initial leftmost leaf the new subtree.383* If null, siblings after currentNode will be placed in the subtree, but384* no additional node will be.385* @param {Node=} opt_root The top of the tree where splitting stops at.386* @return {!Node} The new subtree.387*/388goog.editor.node.splitDomTreeAt = function(389currentNode, opt_secondHalf, opt_root) {390var parent;391while (currentNode != opt_root && (parent = currentNode.parentNode)) {392opt_secondHalf = goog.editor.node.getSecondHalfOfNode_(393parent, currentNode, opt_secondHalf);394currentNode = parent;395}396return /** @type {!Node} */ (opt_secondHalf);397};398399400/**401* Creates a clone of node, moving all children after startNode to it.402* When firstChild is not null or undefined, it is also appended to the clone403* as the first child.404* @param {!Node} node The node to clone.405* @param {!Node} startNode All siblings after this node will be moved to the406* clone.407* @param {Node|undefined} firstChild The first child of the new cloned element.408* @return {!Node} The cloned node that now contains the children after409* startNode.410* @private411*/412goog.editor.node.getSecondHalfOfNode_ = function(node, startNode, firstChild) {413var secondHalf = /** @type {!Node} */ (node.cloneNode(false));414while (startNode.nextSibling) {415goog.dom.appendChild(secondHalf, startNode.nextSibling);416}417if (firstChild) {418secondHalf.insertBefore(firstChild, secondHalf.firstChild);419}420return secondHalf;421};422423424/**425* Appends all of oldNode's children to newNode. This removes all children from426* oldNode and appends them to newNode. oldNode is left with no children.427* @param {!Node} newNode Node to transfer children to.428* @param {Node} oldNode Node to transfer children from.429* @deprecated Use goog.dom.append directly instead.430*/431goog.editor.node.transferChildren = function(newNode, oldNode) {432goog.dom.append(newNode, oldNode.childNodes);433};434435436/**437* Replaces the innerHTML of a node.438*439* IE has serious problems if you try to set innerHTML of an editable node with440* any selection. Early versions of IE tear up the old internal tree storage, to441* help avoid ref-counting loops. But this sometimes leaves the selection object442* in a bad state and leads to segfaults.443*444* Removing the nodes first prevents IE from tearing them up. This is not445* strictly necessary in nodes that do not have the selection. You should always446* use this function when setting innerHTML inside of a field.447*448* @param {Node} node A node.449* @param {string} html The innerHTML to set on the node.450*/451goog.editor.node.replaceInnerHtml = function(node, html) {452// Only do this IE. On gecko, we use element change events, and don't453// want to trigger spurious events.454if (goog.userAgent.IE) {455goog.dom.removeChildren(node);456}457node.innerHTML = html;458};459460461