node.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. // Copyright 2005 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Utilties for working with DOM nodes related to rich text
  16. * editing. Many of these are not general enough to go into goog.dom.
  17. *
  18. * @author nicksantos@google.com (Nick Santos)
  19. */
  20. goog.provide('goog.editor.node');
  21. goog.require('goog.dom');
  22. goog.require('goog.dom.NodeType');
  23. goog.require('goog.dom.TagName');
  24. goog.require('goog.dom.iter.ChildIterator');
  25. goog.require('goog.dom.iter.SiblingIterator');
  26. goog.require('goog.iter');
  27. goog.require('goog.object');
  28. goog.require('goog.string');
  29. goog.require('goog.string.Unicode');
  30. goog.require('goog.userAgent');
  31. /**
  32. * Names of all block-level tags
  33. * @type {Object}
  34. * @private
  35. */
  36. goog.editor.node.BLOCK_TAG_NAMES_ = goog.object.createSet(
  37. goog.dom.TagName.ADDRESS, goog.dom.TagName.ARTICLE, goog.dom.TagName.ASIDE,
  38. goog.dom.TagName.BLOCKQUOTE, goog.dom.TagName.BODY,
  39. goog.dom.TagName.CAPTION, goog.dom.TagName.CENTER, goog.dom.TagName.COL,
  40. goog.dom.TagName.COLGROUP, goog.dom.TagName.DETAILS, goog.dom.TagName.DIR,
  41. goog.dom.TagName.DIV, goog.dom.TagName.DL, goog.dom.TagName.DD,
  42. goog.dom.TagName.DT, goog.dom.TagName.FIELDSET, goog.dom.TagName.FIGCAPTION,
  43. goog.dom.TagName.FIGURE, goog.dom.TagName.FOOTER, goog.dom.TagName.FORM,
  44. goog.dom.TagName.H1, goog.dom.TagName.H2, goog.dom.TagName.H3,
  45. goog.dom.TagName.H4, goog.dom.TagName.H5, goog.dom.TagName.H6,
  46. goog.dom.TagName.HEADER, goog.dom.TagName.HGROUP, goog.dom.TagName.HR,
  47. goog.dom.TagName.ISINDEX, goog.dom.TagName.OL, goog.dom.TagName.LI,
  48. goog.dom.TagName.MAP, goog.dom.TagName.MENU, goog.dom.TagName.NAV,
  49. goog.dom.TagName.OPTGROUP, goog.dom.TagName.OPTION, goog.dom.TagName.P,
  50. goog.dom.TagName.PRE, goog.dom.TagName.SECTION, goog.dom.TagName.SUMMARY,
  51. goog.dom.TagName.TABLE, goog.dom.TagName.TBODY, goog.dom.TagName.TD,
  52. goog.dom.TagName.TFOOT, goog.dom.TagName.TH, goog.dom.TagName.THEAD,
  53. goog.dom.TagName.TR, goog.dom.TagName.UL);
  54. /**
  55. * Names of tags that have intrinsic content.
  56. * TODO(robbyw): What about object, br, input, textarea, button, isindex,
  57. * hr, keygen, select, table, tr, td?
  58. * @type {Object}
  59. * @private
  60. */
  61. goog.editor.node.NON_EMPTY_TAGS_ = goog.object.createSet(
  62. goog.dom.TagName.IMG, goog.dom.TagName.IFRAME, goog.dom.TagName.EMBED);
  63. /**
  64. * Check if the node is in a standards mode document.
  65. * @param {Node} node The node to test.
  66. * @return {boolean} Whether the node is in a standards mode document.
  67. */
  68. goog.editor.node.isStandardsMode = function(node) {
  69. return goog.dom.getDomHelper(node).isCss1CompatMode();
  70. };
  71. /**
  72. * Get the right-most non-ignorable leaf node of the given node.
  73. * @param {Node} parent The parent ndoe.
  74. * @return {Node} The right-most non-ignorable leaf node.
  75. */
  76. goog.editor.node.getRightMostLeaf = function(parent) {
  77. var temp;
  78. while (temp = goog.editor.node.getLastChild(parent)) {
  79. parent = temp;
  80. }
  81. return parent;
  82. };
  83. /**
  84. * Get the left-most non-ignorable leaf node of the given node.
  85. * @param {Node} parent The parent ndoe.
  86. * @return {Node} The left-most non-ignorable leaf node.
  87. */
  88. goog.editor.node.getLeftMostLeaf = function(parent) {
  89. var temp;
  90. while (temp = goog.editor.node.getFirstChild(parent)) {
  91. parent = temp;
  92. }
  93. return parent;
  94. };
  95. /**
  96. * Version of firstChild that skips nodes that are entirely
  97. * whitespace and comments.
  98. * @param {Node} parent The reference node.
  99. * @return {Node} The first child of sibling that is important according to
  100. * goog.editor.node.isImportant, or null if no such node exists.
  101. */
  102. goog.editor.node.getFirstChild = function(parent) {
  103. return goog.editor.node.getChildHelper_(parent, false);
  104. };
  105. /**
  106. * Version of lastChild that skips nodes that are entirely whitespace or
  107. * comments. (Normally lastChild is a property of all DOM nodes that gives the
  108. * last of the nodes contained directly in the reference node.)
  109. * @param {Node} parent The reference node.
  110. * @return {Node} The last child of sibling that is important according to
  111. * goog.editor.node.isImportant, or null if no such node exists.
  112. */
  113. goog.editor.node.getLastChild = function(parent) {
  114. return goog.editor.node.getChildHelper_(parent, true);
  115. };
  116. /**
  117. * Version of previoussibling that skips nodes that are entirely
  118. * whitespace or comments. (Normally previousSibling is a property
  119. * of all DOM nodes that gives the sibling node, the node that is
  120. * a child of the same parent, that occurs immediately before the
  121. * reference node.)
  122. * @param {Node} sibling The reference node.
  123. * @return {Node} The closest previous sibling to sibling that is
  124. * important according to goog.editor.node.isImportant, or null if no such
  125. * node exists.
  126. */
  127. goog.editor.node.getPreviousSibling = function(sibling) {
  128. return /** @type {Node} */ (
  129. goog.editor.node.getFirstValue_(
  130. goog.iter.filter(
  131. new goog.dom.iter.SiblingIterator(sibling, false, true),
  132. goog.editor.node.isImportant)));
  133. };
  134. /**
  135. * Version of nextSibling that skips nodes that are entirely whitespace or
  136. * comments.
  137. * @param {Node} sibling The reference node.
  138. * @return {Node} The closest next sibling to sibling that is important
  139. * according to goog.editor.node.isImportant, or null if no
  140. * such node exists.
  141. */
  142. goog.editor.node.getNextSibling = function(sibling) {
  143. return /** @type {Node} */ (
  144. goog.editor.node.getFirstValue_(
  145. goog.iter.filter(
  146. new goog.dom.iter.SiblingIterator(sibling),
  147. goog.editor.node.isImportant)));
  148. };
  149. /**
  150. * Internal helper for lastChild/firstChild that skips nodes that are entirely
  151. * whitespace or comments.
  152. * @param {Node} parent The reference node.
  153. * @param {boolean} isReversed Whether children should be traversed forward
  154. * or backward.
  155. * @return {Node} The first/last child of sibling that is important according
  156. * to goog.editor.node.isImportant, or null if no such node exists.
  157. * @private
  158. */
  159. goog.editor.node.getChildHelper_ = function(parent, isReversed) {
  160. return (!parent || parent.nodeType != goog.dom.NodeType.ELEMENT) ?
  161. null :
  162. /** @type {Node} */ (
  163. goog.editor.node.getFirstValue_(
  164. goog.iter.filter(
  165. new goog.dom.iter.ChildIterator(
  166. /** @type {!Element} */ (parent), isReversed),
  167. goog.editor.node.isImportant)));
  168. };
  169. /**
  170. * Utility function that returns the first value from an iterator or null if
  171. * the iterator is empty.
  172. * @param {goog.iter.Iterator} iterator The iterator to get a value from.
  173. * @return {*} The first value from the iterator.
  174. * @private
  175. */
  176. goog.editor.node.getFirstValue_ = function(iterator) {
  177. try {
  178. return iterator.next();
  179. } catch (e) {
  180. return null;
  181. }
  182. };
  183. /**
  184. * Determine if a node should be returned by the iterator functions.
  185. * @param {Node} node An object implementing the DOM1 Node interface.
  186. * @return {boolean} Whether the node is an element, or a text node that
  187. * is not all whitespace.
  188. */
  189. goog.editor.node.isImportant = function(node) {
  190. // Return true if the node is not either a TextNode or an ElementNode.
  191. return node.nodeType == goog.dom.NodeType.ELEMENT ||
  192. node.nodeType == goog.dom.NodeType.TEXT &&
  193. !goog.editor.node.isAllNonNbspWhiteSpace(node);
  194. };
  195. /**
  196. * Determine whether a node's text content is entirely whitespace.
  197. * @param {Node} textNode A node implementing the CharacterData interface (i.e.,
  198. * a Text, Comment, or CDATASection node.
  199. * @return {boolean} Whether the text content of node is whitespace,
  200. * otherwise false.
  201. */
  202. goog.editor.node.isAllNonNbspWhiteSpace = function(textNode) {
  203. return goog.string.isBreakingWhitespace(textNode.nodeValue);
  204. };
  205. /**
  206. * Returns true if the node contains only whitespace and is not and does not
  207. * contain any images, iframes or embed tags.
  208. * @param {Node} node The node to check.
  209. * @param {boolean=} opt_prohibitSingleNbsp By default, this function treats a
  210. * single nbsp as empty. Set this to true to treat this case as non-empty.
  211. * @return {boolean} Whether the node contains only whitespace.
  212. */
  213. goog.editor.node.isEmpty = function(node, opt_prohibitSingleNbsp) {
  214. var nodeData = goog.dom.getRawTextContent(node);
  215. if (node.getElementsByTagName) {
  216. node = /** @type {!Element} */ (node);
  217. for (var tag in goog.editor.node.NON_EMPTY_TAGS_) {
  218. if (node.tagName == tag || node.getElementsByTagName(tag).length > 0) {
  219. return false;
  220. }
  221. }
  222. }
  223. return (!opt_prohibitSingleNbsp && nodeData == goog.string.Unicode.NBSP) ||
  224. goog.string.isBreakingWhitespace(nodeData);
  225. };
  226. /**
  227. * Returns the length of the text in node if it is a text node, or the number
  228. * of children of the node, if it is an element. Useful for range-manipulation
  229. * code where you need to know the offset for the right side of the node.
  230. * @param {Node} node The node to get the length of.
  231. * @return {number} The length of the node.
  232. */
  233. goog.editor.node.getLength = function(node) {
  234. return node.length || node.childNodes.length;
  235. };
  236. /**
  237. * Search child nodes using a predicate function and return the first node that
  238. * satisfies the condition.
  239. * @param {Node} parent The parent node to search.
  240. * @param {function(Node):boolean} hasProperty A function that takes a child
  241. * node as a parameter and returns true if it meets the criteria.
  242. * @return {?number} The index of the node found, or null if no node is found.
  243. */
  244. goog.editor.node.findInChildren = function(parent, hasProperty) {
  245. for (var i = 0, len = parent.childNodes.length; i < len; i++) {
  246. if (hasProperty(parent.childNodes[i])) {
  247. return i;
  248. }
  249. }
  250. return null;
  251. };
  252. /**
  253. * Search ancestor nodes using a predicate function and returns the topmost
  254. * ancestor in the chain of consecutive ancestors that satisfies the condition.
  255. *
  256. * @param {Node} node The node whose ancestors have to be searched.
  257. * @param {function(Node): boolean} hasProperty A function that takes a parent
  258. * node as a parameter and returns true if it meets the criteria.
  259. * @return {Node} The topmost ancestor or null if no ancestor satisfies the
  260. * predicate function.
  261. */
  262. goog.editor.node.findHighestMatchingAncestor = function(node, hasProperty) {
  263. var parent = node.parentNode;
  264. var ancestor = null;
  265. while (parent && hasProperty(parent)) {
  266. ancestor = parent;
  267. parent = parent.parentNode;
  268. }
  269. return ancestor;
  270. };
  271. /**
  272. * Checks if node is a block-level html element. The <tt>display</tt> css
  273. * property is ignored.
  274. * @param {Node} node The node to test.
  275. * @return {boolean} Whether the node is a block-level node.
  276. */
  277. goog.editor.node.isBlockTag = function(node) {
  278. return !!goog.editor.node.BLOCK_TAG_NAMES_[
  279. /** @type {!Element} */ (node).tagName];
  280. };
  281. /**
  282. * Skips siblings of a node that are empty text nodes.
  283. * @param {Node} node A node. May be null.
  284. * @return {Node} The node or the first sibling of the node that is not an
  285. * empty text node. May be null.
  286. */
  287. goog.editor.node.skipEmptyTextNodes = function(node) {
  288. while (node && node.nodeType == goog.dom.NodeType.TEXT && !node.nodeValue) {
  289. node = node.nextSibling;
  290. }
  291. return node;
  292. };
  293. /**
  294. * Checks if an element is a top-level editable container (meaning that
  295. * it itself is not editable, but all its child nodes are editable).
  296. * @param {Node} element The element to test.
  297. * @return {boolean} Whether the element is a top-level editable container.
  298. */
  299. goog.editor.node.isEditableContainer = function(element) {
  300. return element.getAttribute && element.getAttribute('g_editable') == 'true';
  301. };
  302. /**
  303. * Checks if a node is inside an editable container.
  304. * @param {Node} node The node to test.
  305. * @return {boolean} Whether the node is in an editable container.
  306. */
  307. goog.editor.node.isEditable = function(node) {
  308. return !!goog.dom.getAncestor(node, goog.editor.node.isEditableContainer);
  309. };
  310. /**
  311. * Finds the top-most DOM node inside an editable field that is an ancestor
  312. * (or self) of a given DOM node and meets the specified criteria.
  313. * @param {Node} node The DOM node where the search starts.
  314. * @param {function(Node) : boolean} criteria A function that takes a DOM node
  315. * as a parameter and returns a boolean to indicate whether the node meets
  316. * the criteria or not.
  317. * @return {Node} The DOM node if found, or null.
  318. */
  319. goog.editor.node.findTopMostEditableAncestor = function(node, criteria) {
  320. var targetNode = null;
  321. while (node && !goog.editor.node.isEditableContainer(node)) {
  322. if (criteria(node)) {
  323. targetNode = node;
  324. }
  325. node = node.parentNode;
  326. }
  327. return targetNode;
  328. };
  329. /**
  330. * Splits off a subtree.
  331. * @param {!Node} currentNode The starting splitting point.
  332. * @param {Node=} opt_secondHalf The initial leftmost leaf the new subtree.
  333. * If null, siblings after currentNode will be placed in the subtree, but
  334. * no additional node will be.
  335. * @param {Node=} opt_root The top of the tree where splitting stops at.
  336. * @return {!Node} The new subtree.
  337. */
  338. goog.editor.node.splitDomTreeAt = function(
  339. currentNode, opt_secondHalf, opt_root) {
  340. var parent;
  341. while (currentNode != opt_root && (parent = currentNode.parentNode)) {
  342. opt_secondHalf = goog.editor.node.getSecondHalfOfNode_(
  343. parent, currentNode, opt_secondHalf);
  344. currentNode = parent;
  345. }
  346. return /** @type {!Node} */ (opt_secondHalf);
  347. };
  348. /**
  349. * Creates a clone of node, moving all children after startNode to it.
  350. * When firstChild is not null or undefined, it is also appended to the clone
  351. * as the first child.
  352. * @param {!Node} node The node to clone.
  353. * @param {!Node} startNode All siblings after this node will be moved to the
  354. * clone.
  355. * @param {Node|undefined} firstChild The first child of the new cloned element.
  356. * @return {!Node} The cloned node that now contains the children after
  357. * startNode.
  358. * @private
  359. */
  360. goog.editor.node.getSecondHalfOfNode_ = function(node, startNode, firstChild) {
  361. var secondHalf = /** @type {!Node} */ (node.cloneNode(false));
  362. while (startNode.nextSibling) {
  363. goog.dom.appendChild(secondHalf, startNode.nextSibling);
  364. }
  365. if (firstChild) {
  366. secondHalf.insertBefore(firstChild, secondHalf.firstChild);
  367. }
  368. return secondHalf;
  369. };
  370. /**
  371. * Appends all of oldNode's children to newNode. This removes all children from
  372. * oldNode and appends them to newNode. oldNode is left with no children.
  373. * @param {!Node} newNode Node to transfer children to.
  374. * @param {Node} oldNode Node to transfer children from.
  375. * @deprecated Use goog.dom.append directly instead.
  376. */
  377. goog.editor.node.transferChildren = function(newNode, oldNode) {
  378. goog.dom.append(newNode, oldNode.childNodes);
  379. };
  380. /**
  381. * Replaces the innerHTML of a node.
  382. *
  383. * IE has serious problems if you try to set innerHTML of an editable node with
  384. * any selection. Early versions of IE tear up the old internal tree storage, to
  385. * help avoid ref-counting loops. But this sometimes leaves the selection object
  386. * in a bad state and leads to segfaults.
  387. *
  388. * Removing the nodes first prevents IE from tearing them up. This is not
  389. * strictly necessary in nodes that do not have the selection. You should always
  390. * use this function when setting innerHTML inside of a field.
  391. *
  392. * @param {Node} node A node.
  393. * @param {string} html The innerHTML to set on the node.
  394. */
  395. goog.editor.node.replaceInnerHtml = function(node, html) {
  396. // Only do this IE. On gecko, we use element change events, and don't
  397. // want to trigger spurious events.
  398. if (goog.userAgent.IE) {
  399. goog.dom.removeChildren(node);
  400. }
  401. node.innerHTML = html;
  402. };