484 lines
16 KiB
JavaScript
484 lines
16 KiB
JavaScript
// 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');
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
/**
|
|
* Names of all block-level tags
|
|
* @type {Object}
|
|
* @private
|
|
*/
|
|
goog.editor.node.BLOCK_TAG_NAMES_ = goog.object.createSet(
|
|
goog.dom.TagName.ADDRESS,
|
|
goog.dom.TagName.ARTICLE,
|
|
goog.dom.TagName.ASIDE,
|
|
goog.dom.TagName.BLOCKQUOTE,
|
|
goog.dom.TagName.BODY,
|
|
goog.dom.TagName.CAPTION,
|
|
goog.dom.TagName.CENTER,
|
|
goog.dom.TagName.COL,
|
|
goog.dom.TagName.COLGROUP,
|
|
goog.dom.TagName.DETAILS,
|
|
goog.dom.TagName.DIR,
|
|
goog.dom.TagName.DIV,
|
|
goog.dom.TagName.DL,
|
|
goog.dom.TagName.DD,
|
|
goog.dom.TagName.DT,
|
|
goog.dom.TagName.FIELDSET,
|
|
goog.dom.TagName.FIGCAPTION,
|
|
goog.dom.TagName.FIGURE,
|
|
goog.dom.TagName.FOOTER,
|
|
goog.dom.TagName.FORM,
|
|
goog.dom.TagName.H1,
|
|
goog.dom.TagName.H2,
|
|
goog.dom.TagName.H3,
|
|
goog.dom.TagName.H4,
|
|
goog.dom.TagName.H5,
|
|
goog.dom.TagName.H6,
|
|
goog.dom.TagName.HEADER,
|
|
goog.dom.TagName.HGROUP,
|
|
goog.dom.TagName.HR,
|
|
goog.dom.TagName.ISINDEX,
|
|
goog.dom.TagName.OL,
|
|
goog.dom.TagName.LI,
|
|
goog.dom.TagName.MAP,
|
|
goog.dom.TagName.MENU,
|
|
goog.dom.TagName.NAV,
|
|
goog.dom.TagName.OPTGROUP,
|
|
goog.dom.TagName.OPTION,
|
|
goog.dom.TagName.P,
|
|
goog.dom.TagName.PRE,
|
|
goog.dom.TagName.SECTION,
|
|
goog.dom.TagName.SUMMARY,
|
|
goog.dom.TagName.TABLE,
|
|
goog.dom.TagName.TBODY,
|
|
goog.dom.TagName.TD,
|
|
goog.dom.TagName.TFOOT,
|
|
goog.dom.TagName.TH,
|
|
goog.dom.TagName.THEAD,
|
|
goog.dom.TagName.TR,
|
|
goog.dom.TagName.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, goog.dom.TagName.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 <tt>display</tt> 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);
|
|
};
|
|
|
|
|
|
/**
|
|
* Replaces the innerHTML of a node.
|
|
*
|
|
* IE has serious problems if you try to set innerHTML of an editable node with
|
|
* any selection. Early versions of IE tear up the old internal tree storage, to
|
|
* help avoid ref-counting loops. But this sometimes leaves the selection object
|
|
* in a bad state and leads to segfaults.
|
|
*
|
|
* Removing the nodes first prevents IE from tearing them up. This is not
|
|
* strictly necessary in nodes that do not have the selection. You should always
|
|
* use this function when setting innerHTML inside of a field.
|
|
*
|
|
* @param {Node} node A node.
|
|
* @param {string} html The innerHTML to set on the node.
|
|
*/
|
|
goog.editor.node.replaceInnerHtml = function(node, html) {
|
|
// Only do this IE. On gecko, we use element change events, and don't
|
|
// want to trigger spurious events.
|
|
if (goog.userAgent.IE) {
|
|
goog.dom.removeChildren(node);
|
|
}
|
|
node.innerHTML = html;
|
|
};
|