RangeUtils.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * Range.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /**
  11. * RangeUtils
  12. *
  13. * @class tinymce.dom.RangeUtils
  14. * @private
  15. */
  16. define("tinymce/dom/RangeUtils", [
  17. "tinymce/util/Tools",
  18. "tinymce/dom/TreeWalker"
  19. ], function(Tools, TreeWalker) {
  20. var each = Tools.each;
  21. function RangeUtils(dom) {
  22. /**
  23. * Walks the specified range like object and executes the callback for each sibling collection it finds.
  24. *
  25. * @method walk
  26. * @param {Object} rng Range like object.
  27. * @param {function} callback Callback function to execute for each sibling collection.
  28. */
  29. this.walk = function(rng, callback) {
  30. var startContainer = rng.startContainer,
  31. startOffset = rng.startOffset,
  32. endContainer = rng.endContainer,
  33. endOffset = rng.endOffset,
  34. ancestor, startPoint,
  35. endPoint, node, parent, siblings, nodes;
  36. // Handle table cell selection the table plugin enables
  37. // you to fake select table cells and perform formatting actions on them
  38. nodes = dom.select('td.mce-item-selected,th.mce-item-selected');
  39. if (nodes.length > 0) {
  40. each(nodes, function(node) {
  41. callback([node]);
  42. });
  43. return;
  44. }
  45. /**
  46. * Excludes start/end text node if they are out side the range
  47. *
  48. * @private
  49. * @param {Array} nodes Nodes to exclude items from.
  50. * @return {Array} Array with nodes excluding the start/end container if needed.
  51. */
  52. function exclude(nodes) {
  53. var node;
  54. // First node is excluded
  55. node = nodes[0];
  56. if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
  57. nodes.splice(0, 1);
  58. }
  59. // Last node is excluded
  60. node = nodes[nodes.length - 1];
  61. if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
  62. nodes.splice(nodes.length - 1, 1);
  63. }
  64. return nodes;
  65. }
  66. /**
  67. * Collects siblings
  68. *
  69. * @private
  70. * @param {Node} node Node to collect siblings from.
  71. * @param {String} name Name of the sibling to check for.
  72. * @return {Array} Array of collected siblings.
  73. */
  74. function collectSiblings(node, name, end_node) {
  75. var siblings = [];
  76. for (; node && node != end_node; node = node[name]) {
  77. siblings.push(node);
  78. }
  79. return siblings;
  80. }
  81. /**
  82. * Find an end point this is the node just before the common ancestor root.
  83. *
  84. * @private
  85. * @param {Node} node Node to start at.
  86. * @param {Node} root Root/ancestor element to stop just before.
  87. * @return {Node} Node just before the root element.
  88. */
  89. function findEndPoint(node, root) {
  90. do {
  91. if (node.parentNode == root) {
  92. return node;
  93. }
  94. node = node.parentNode;
  95. } while(node);
  96. }
  97. function walkBoundary(start_node, end_node, next) {
  98. var siblingName = next ? 'nextSibling' : 'previousSibling';
  99. for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
  100. parent = node.parentNode;
  101. siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
  102. if (siblings.length) {
  103. if (!next) {
  104. siblings.reverse();
  105. }
  106. callback(exclude(siblings));
  107. }
  108. }
  109. }
  110. // If index based start position then resolve it
  111. if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
  112. startContainer = startContainer.childNodes[startOffset];
  113. }
  114. // If index based end position then resolve it
  115. if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
  116. endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];
  117. }
  118. // Same container
  119. if (startContainer == endContainer) {
  120. return callback(exclude([startContainer]));
  121. }
  122. // Find common ancestor and end points
  123. ancestor = dom.findCommonAncestor(startContainer, endContainer);
  124. // Process left side
  125. for (node = startContainer; node; node = node.parentNode) {
  126. if (node === endContainer) {
  127. return walkBoundary(startContainer, ancestor, true);
  128. }
  129. if (node === ancestor) {
  130. break;
  131. }
  132. }
  133. // Process right side
  134. for (node = endContainer; node; node = node.parentNode) {
  135. if (node === startContainer) {
  136. return walkBoundary(endContainer, ancestor);
  137. }
  138. if (node === ancestor) {
  139. break;
  140. }
  141. }
  142. // Find start/end point
  143. startPoint = findEndPoint(startContainer, ancestor) || startContainer;
  144. endPoint = findEndPoint(endContainer, ancestor) || endContainer;
  145. // Walk left leaf
  146. walkBoundary(startContainer, startPoint, true);
  147. // Walk the middle from start to end point
  148. siblings = collectSiblings(
  149. startPoint == startContainer ? startPoint : startPoint.nextSibling,
  150. 'nextSibling',
  151. endPoint == endContainer ? endPoint.nextSibling : endPoint
  152. );
  153. if (siblings.length) {
  154. callback(exclude(siblings));
  155. }
  156. // Walk right leaf
  157. walkBoundary(endContainer, endPoint);
  158. };
  159. /**
  160. * Splits the specified range at it's start/end points.
  161. *
  162. * @private
  163. * @param {Range/RangeObject} rng Range to split.
  164. * @return {Object} Range position object.
  165. */
  166. this.split = function(rng) {
  167. var startContainer = rng.startContainer,
  168. startOffset = rng.startOffset,
  169. endContainer = rng.endContainer,
  170. endOffset = rng.endOffset;
  171. function splitText(node, offset) {
  172. return node.splitText(offset);
  173. }
  174. // Handle single text node
  175. if (startContainer == endContainer && startContainer.nodeType == 3) {
  176. if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
  177. endContainer = splitText(startContainer, startOffset);
  178. startContainer = endContainer.previousSibling;
  179. if (endOffset > startOffset) {
  180. endOffset = endOffset - startOffset;
  181. startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
  182. endOffset = endContainer.nodeValue.length;
  183. startOffset = 0;
  184. } else {
  185. endOffset = 0;
  186. }
  187. }
  188. } else {
  189. // Split startContainer text node if needed
  190. if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
  191. startContainer = splitText(startContainer, startOffset);
  192. startOffset = 0;
  193. }
  194. // Split endContainer text node if needed
  195. if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
  196. endContainer = splitText(endContainer, endOffset).previousSibling;
  197. endOffset = endContainer.nodeValue.length;
  198. }
  199. }
  200. return {
  201. startContainer: startContainer,
  202. startOffset: startOffset,
  203. endContainer: endContainer,
  204. endOffset: endOffset
  205. };
  206. };
  207. /**
  208. * Normalizes the specified range by finding the closest best suitable caret location.
  209. *
  210. * @private
  211. * @param {Range} rng Range to normalize.
  212. * @return {Boolean} True/false if the specified range was normalized or not.
  213. */
  214. this.normalize = function(rng) {
  215. var normalized, collapsed;
  216. function normalizeEndPoint(start) {
  217. var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
  218. var directionLeft, isAfterNode;
  219. function hasBrBeforeAfter(node, left) {
  220. var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
  221. while ((node = walker[left ? 'prev' : 'next']())) {
  222. if (node.nodeName === "BR") {
  223. return true;
  224. }
  225. }
  226. }
  227. function isPrevNode(node, name) {
  228. return node.previousSibling && node.previousSibling.nodeName == name;
  229. }
  230. // Walks the dom left/right to find a suitable text node to move the endpoint into
  231. // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
  232. function findTextNodeRelative(left, startNode) {
  233. var walker, lastInlineElement, parentBlockContainer;
  234. startNode = startNode || container;
  235. parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
  236. // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
  237. // This: <p><br>|</p> becomes <p>|<br></p>
  238. if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
  239. container = startNode.parentNode;
  240. offset = dom.nodeIndex(startNode);
  241. normalized = true;
  242. return;
  243. }
  244. // Walk left until we hit a text node we can move to or a block/br/img
  245. walker = new TreeWalker(startNode, parentBlockContainer);
  246. while ((node = walker[left ? 'prev' : 'next']())) {
  247. // Break if we hit a non content editable node
  248. if (dom.getContentEditableParent(node) === "false") {
  249. return;
  250. }
  251. // Found text node that has a length
  252. if (node.nodeType === 3 && node.nodeValue.length > 0) {
  253. container = node;
  254. offset = left ? node.nodeValue.length : 0;
  255. normalized = true;
  256. return;
  257. }
  258. // Break if we find a block or a BR/IMG/INPUT etc
  259. if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
  260. return;
  261. }
  262. lastInlineElement = node;
  263. }
  264. // Only fetch the last inline element when in caret mode for now
  265. if (collapsed && lastInlineElement) {
  266. container = lastInlineElement;
  267. normalized = true;
  268. offset = 0;
  269. }
  270. }
  271. container = rng[(start ? 'start' : 'end') + 'Container'];
  272. offset = rng[(start ? 'start' : 'end') + 'Offset'];
  273. isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
  274. nonEmptyElementsMap = dom.schema.getNonEmptyElements();
  275. directionLeft = start;
  276. if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
  277. directionLeft = false;
  278. }
  279. // If the container is a document move it to the body element
  280. if (container.nodeType === 9) {
  281. container = dom.getRoot();
  282. offset = 0;
  283. }
  284. // If the container is body try move it into the closest text node or position
  285. if (container === body) {
  286. // If start is before/after a image, table etc
  287. if (directionLeft) {
  288. node = container.childNodes[offset > 0 ? offset - 1 : 0];
  289. if (node) {
  290. if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
  291. return;
  292. }
  293. }
  294. }
  295. // Resolve the index
  296. if (container.hasChildNodes()) {
  297. offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
  298. container = container.childNodes[offset];
  299. offset = 0;
  300. // Don't walk into elements that doesn't have any child nodes like a IMG
  301. if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
  302. // Walk the DOM to find a text node to place the caret at or a BR
  303. node = container;
  304. walker = new TreeWalker(container, body);
  305. do {
  306. // Found a text node use that position
  307. if (node.nodeType === 3 && node.nodeValue.length > 0) {
  308. offset = directionLeft ? 0 : node.nodeValue.length;
  309. container = node;
  310. normalized = true;
  311. break;
  312. }
  313. // Found a BR/IMG element that we can place the caret before
  314. if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
  315. offset = dom.nodeIndex(node);
  316. container = node.parentNode;
  317. // Put caret after image when moving the end point
  318. if (node.nodeName == "IMG" && !directionLeft) {
  319. offset++;
  320. }
  321. normalized = true;
  322. break;
  323. }
  324. } while ((node = (directionLeft ? walker.next() : walker.prev())));
  325. }
  326. }
  327. }
  328. // Lean the caret to the left if possible
  329. if (collapsed) {
  330. // So this: <b>x</b><i>|x</i>
  331. // Becomes: <b>x|</b><i>x</i>
  332. // Seems that only gecko has issues with this
  333. if (container.nodeType === 3 && offset === 0) {
  334. findTextNodeRelative(true);
  335. }
  336. // Lean left into empty inline elements when the caret is before a BR
  337. // So this: <i><b></b><i>|<br></i>
  338. // Becomes: <i><b>|</b><i><br></i>
  339. // Seems that only gecko has issues with this.
  340. // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
  341. if (container.nodeType === 1) {
  342. node = container.childNodes[offset];
  343. // Offset is after the containers last child
  344. // then use the previous child for normalization
  345. if (!node) {
  346. node = container.childNodes[offset - 1];
  347. }
  348. if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
  349. !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
  350. findTextNodeRelative(true, node);
  351. }
  352. }
  353. }
  354. // Lean the start of the selection right if possible
  355. // So this: x[<b>x]</b>
  356. // Becomes: x<b>[x]</b>
  357. if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
  358. findTextNodeRelative(false);
  359. }
  360. // Set endpoint if it was normalized
  361. if (normalized) {
  362. rng['set' + (start ? 'Start' : 'End')](container, offset);
  363. }
  364. }
  365. collapsed = rng.collapsed;
  366. normalizeEndPoint(true);
  367. if (!collapsed) {
  368. normalizeEndPoint();
  369. }
  370. // If it was collapsed then make sure it still is
  371. if (normalized && collapsed) {
  372. rng.collapse(true);
  373. }
  374. return normalized;
  375. };
  376. }
  377. /**
  378. * Compares two ranges and checks if they are equal.
  379. *
  380. * @static
  381. * @method compareRanges
  382. * @param {DOMRange} rng1 First range to compare.
  383. * @param {DOMRange} rng2 First range to compare.
  384. * @return {Boolean} true/false if the ranges are equal.
  385. */
  386. RangeUtils.compareRanges = function(rng1, rng2) {
  387. if (rng1 && rng2) {
  388. // Compare native IE ranges
  389. if (rng1.item || rng1.duplicate) {
  390. // Both are control ranges and the selected element matches
  391. if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
  392. return true;
  393. }
  394. // Both are text ranges and the range matches
  395. if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
  396. return true;
  397. }
  398. } else {
  399. // Compare w3c ranges
  400. return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
  401. }
  402. }
  403. return false;
  404. };
  405. return RangeUtils;
  406. });