TridentSelection.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /**
  2. * TridentSelection.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. * Selection class for old explorer versions. This one fakes the
  12. * native selection object available on modern browsers.
  13. *
  14. * @class tinymce.dom.TridentSelection
  15. */
  16. define("tinymce/dom/TridentSelection", [], function() {
  17. function Selection(selection) {
  18. var self = this, dom = selection.dom, FALSE = false;
  19. function getPosition(rng, start) {
  20. var checkRng, startIndex = 0, endIndex, inside,
  21. children, child, offset, index, position = -1, parent;
  22. // Setup test range, collapse it and get the parent
  23. checkRng = rng.duplicate();
  24. checkRng.collapse(start);
  25. parent = checkRng.parentElement();
  26. // Check if the selection is within the right document
  27. if (parent.ownerDocument !== selection.dom.doc) {
  28. return;
  29. }
  30. // IE will report non editable elements as it's parent so look for an editable one
  31. while (parent.contentEditable === "false") {
  32. parent = parent.parentNode;
  33. }
  34. // If parent doesn't have any children then return that we are inside the element
  35. if (!parent.hasChildNodes()) {
  36. return {node: parent, inside: 1};
  37. }
  38. // Setup node list and endIndex
  39. children = parent.children;
  40. endIndex = children.length - 1;
  41. // Perform a binary search for the position
  42. while (startIndex <= endIndex) {
  43. index = Math.floor((startIndex + endIndex) / 2);
  44. // Move selection to node and compare the ranges
  45. child = children[index];
  46. checkRng.moveToElementText(child);
  47. position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
  48. // Before/after or an exact match
  49. if (position > 0) {
  50. endIndex = index - 1;
  51. } else if (position < 0) {
  52. startIndex = index + 1;
  53. } else {
  54. return {node: child};
  55. }
  56. }
  57. // Check if child position is before or we didn't find a position
  58. if (position < 0) {
  59. // No element child was found use the parent element and the offset inside that
  60. if (!child) {
  61. checkRng.moveToElementText(parent);
  62. checkRng.collapse(true);
  63. child = parent;
  64. inside = true;
  65. } else {
  66. checkRng.collapse(false);
  67. }
  68. // Walk character by character in text node until we hit the selected range endpoint,
  69. // hit the end of document or parent isn't the right one
  70. // We need to walk char by char since rng.text or rng.htmlText will trim line endings
  71. offset = 0;
  72. while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
  73. if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) {
  74. break;
  75. }
  76. offset++;
  77. }
  78. } else {
  79. // Child position is after the selection endpoint
  80. checkRng.collapse(true);
  81. // Walk character by character in text node until we hit the selected range endpoint, hit
  82. // the end of document or parent isn't the right one
  83. offset = 0;
  84. while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
  85. if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) {
  86. break;
  87. }
  88. offset++;
  89. }
  90. }
  91. return {node: child, position: position, offset: offset, inside: inside};
  92. }
  93. // Returns a W3C DOM compatible range object by using the IE Range API
  94. function getRange() {
  95. var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark;
  96. // If selection is outside the current document just return an empty range
  97. element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
  98. if (element.ownerDocument != dom.doc) {
  99. return domRange;
  100. }
  101. collapsed = selection.isCollapsed();
  102. // Handle control selection
  103. if (ieRange.item) {
  104. domRange.setStart(element.parentNode, dom.nodeIndex(element));
  105. domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
  106. return domRange;
  107. }
  108. function findEndPoint(start) {
  109. var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
  110. container = endPoint.node;
  111. offset = endPoint.offset;
  112. if (endPoint.inside && !container.hasChildNodes()) {
  113. domRange[start ? 'setStart' : 'setEnd'](container, 0);
  114. return;
  115. }
  116. if (offset === undef) {
  117. domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
  118. return;
  119. }
  120. if (endPoint.position < 0) {
  121. sibling = endPoint.inside ? container.firstChild : container.nextSibling;
  122. if (!sibling) {
  123. domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
  124. return;
  125. }
  126. if (!offset) {
  127. if (sibling.nodeType == 3) {
  128. domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
  129. } else {
  130. domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
  131. }
  132. return;
  133. }
  134. // Find the text node and offset
  135. while (sibling) {
  136. nodeValue = sibling.nodeValue;
  137. textNodeOffset += nodeValue.length;
  138. // We are at or passed the position we where looking for
  139. if (textNodeOffset >= offset) {
  140. container = sibling;
  141. textNodeOffset -= offset;
  142. textNodeOffset = nodeValue.length - textNodeOffset;
  143. break;
  144. }
  145. sibling = sibling.nextSibling;
  146. }
  147. } else {
  148. // Find the text node and offset
  149. sibling = container.previousSibling;
  150. if (!sibling) {
  151. return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
  152. }
  153. // If there isn't any text to loop then use the first position
  154. if (!offset) {
  155. if (container.nodeType == 3) {
  156. domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
  157. } else {
  158. domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
  159. }
  160. return;
  161. }
  162. while (sibling) {
  163. textNodeOffset += sibling.nodeValue.length;
  164. // We are at or passed the position we where looking for
  165. if (textNodeOffset >= offset) {
  166. container = sibling;
  167. textNodeOffset -= offset;
  168. break;
  169. }
  170. sibling = sibling.previousSibling;
  171. }
  172. }
  173. domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
  174. }
  175. try {
  176. // Find start point
  177. findEndPoint(true);
  178. // Find end point if needed
  179. if (!collapsed) {
  180. findEndPoint();
  181. }
  182. } catch (ex) {
  183. // IE has a nasty bug where text nodes might throw "invalid argument" when you
  184. // access the nodeValue or other properties of text nodes. This seems to happend when
  185. // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
  186. if (ex.number == -2147024809) {
  187. // Get the current selection
  188. bookmark = self.getBookmark(2);
  189. // Get start element
  190. tmpRange = ieRange.duplicate();
  191. tmpRange.collapse(true);
  192. element = tmpRange.parentElement();
  193. // Get end element
  194. if (!collapsed) {
  195. tmpRange = ieRange.duplicate();
  196. tmpRange.collapse(false);
  197. element2 = tmpRange.parentElement();
  198. element2.innerHTML = element2.innerHTML;
  199. }
  200. // Remove the broken elements
  201. element.innerHTML = element.innerHTML;
  202. // Restore the selection
  203. self.moveToBookmark(bookmark);
  204. // Since the range has moved we need to re-get it
  205. ieRange = selection.getRng();
  206. // Find start point
  207. findEndPoint(true);
  208. // Find end point if needed
  209. if (!collapsed) {
  210. findEndPoint();
  211. }
  212. } else {
  213. throw ex; // Throw other errors
  214. }
  215. }
  216. return domRange;
  217. }
  218. this.getBookmark = function(type) {
  219. var rng = selection.getRng(), bookmark = {};
  220. function getIndexes(node) {
  221. var parent, root, children, i, indexes = [];
  222. parent = node.parentNode;
  223. root = dom.getRoot().parentNode;
  224. while (parent != root && parent.nodeType !== 9) {
  225. children = parent.children;
  226. i = children.length;
  227. while (i--) {
  228. if (node === children[i]) {
  229. indexes.push(i);
  230. break;
  231. }
  232. }
  233. node = parent;
  234. parent = parent.parentNode;
  235. }
  236. return indexes;
  237. }
  238. function getBookmarkEndPoint(start) {
  239. var position;
  240. position = getPosition(rng, start);
  241. if (position) {
  242. return {
  243. position: position.position,
  244. offset: position.offset,
  245. indexes: getIndexes(position.node),
  246. inside: position.inside
  247. };
  248. }
  249. }
  250. // Non ubstructive bookmark
  251. if (type === 2) {
  252. // Handle text selection
  253. if (!rng.item) {
  254. bookmark.start = getBookmarkEndPoint(true);
  255. if (!selection.isCollapsed()) {
  256. bookmark.end = getBookmarkEndPoint();
  257. }
  258. } else {
  259. bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))};
  260. }
  261. }
  262. return bookmark;
  263. };
  264. this.moveToBookmark = function(bookmark) {
  265. var rng, body = dom.doc.body;
  266. function resolveIndexes(indexes) {
  267. var node, i, idx, children;
  268. node = dom.getRoot();
  269. for (i = indexes.length - 1; i >= 0; i--) {
  270. children = node.children;
  271. idx = indexes[i];
  272. if (idx <= children.length - 1) {
  273. node = children[idx];
  274. }
  275. }
  276. return node;
  277. }
  278. function setBookmarkEndPoint(start) {
  279. var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset;
  280. if (endPoint) {
  281. moveLeft = endPoint.position > 0;
  282. moveRng = body.createTextRange();
  283. moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
  284. offset = endPoint.offset;
  285. if (offset !== undef) {
  286. moveRng.collapse(endPoint.inside || moveLeft);
  287. moveRng.moveStart('character', moveLeft ? -offset : offset);
  288. } else {
  289. moveRng.collapse(start);
  290. }
  291. rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
  292. if (start) {
  293. rng.collapse(true);
  294. }
  295. }
  296. }
  297. if (bookmark.start) {
  298. if (bookmark.start.ctrl) {
  299. rng = body.createControlRange();
  300. rng.addElement(resolveIndexes(bookmark.start.indexes));
  301. rng.select();
  302. } else {
  303. rng = body.createTextRange();
  304. setBookmarkEndPoint(true);
  305. setBookmarkEndPoint();
  306. rng.select();
  307. }
  308. }
  309. };
  310. this.addRange = function(rng) {
  311. var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,
  312. doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;
  313. function setEndPoint(start) {
  314. var container, offset, marker, tmpRng, nodes;
  315. marker = dom.create('a');
  316. container = start ? startContainer : endContainer;
  317. offset = start ? startOffset : endOffset;
  318. tmpRng = ieRng.duplicate();
  319. if (container == doc || container == doc.documentElement) {
  320. container = body;
  321. offset = 0;
  322. }
  323. if (container.nodeType == 3) {
  324. container.parentNode.insertBefore(marker, container);
  325. tmpRng.moveToElementText(marker);
  326. tmpRng.moveStart('character', offset);
  327. dom.remove(marker);
  328. ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
  329. } else {
  330. nodes = container.childNodes;
  331. if (nodes.length) {
  332. if (offset >= nodes.length) {
  333. dom.insertAfter(marker, nodes[nodes.length - 1]);
  334. } else {
  335. container.insertBefore(marker, nodes[offset]);
  336. }
  337. tmpRng.moveToElementText(marker);
  338. } else if (container.canHaveHTML) {
  339. // Empty node selection for example <div>|</div>
  340. // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
  341. container.innerHTML = '<span>&#xFEFF;</span>';
  342. marker = container.firstChild;
  343. tmpRng.moveToElementText(marker);
  344. tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
  345. }
  346. ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
  347. dom.remove(marker);
  348. }
  349. }
  350. // Setup some shorter versions
  351. startContainer = rng.startContainer;
  352. startOffset = rng.startOffset;
  353. endContainer = rng.endContainer;
  354. endOffset = rng.endOffset;
  355. ieRng = body.createTextRange();
  356. // If single element selection then try making a control selection out of it
  357. if (startContainer == endContainer && startContainer.nodeType == 1) {
  358. // Trick to place the caret inside an empty block element like <p></p>
  359. if (startOffset == endOffset && !startContainer.hasChildNodes()) {
  360. if (startContainer.canHaveHTML) {
  361. // Check if previous sibling is an empty block if it is then we need to render it
  362. // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236
  363. // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>
  364. sibling = startContainer.previousSibling;
  365. if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
  366. sibling.innerHTML = '&#xFEFF;';
  367. } else {
  368. sibling = null;
  369. }
  370. startContainer.innerHTML = '<span>&#xFEFF;</span><span>&#xFEFF;</span>';
  371. ieRng.moveToElementText(startContainer.lastChild);
  372. ieRng.select();
  373. dom.doc.selection.clear();
  374. startContainer.innerHTML = '';
  375. if (sibling) {
  376. sibling.innerHTML = '';
  377. }
  378. return;
  379. } else {
  380. startOffset = dom.nodeIndex(startContainer);
  381. startContainer = startContainer.parentNode;
  382. }
  383. }
  384. if (startOffset == endOffset - 1) {
  385. try {
  386. ctrlElm = startContainer.childNodes[startOffset];
  387. ctrlRng = body.createControlRange();
  388. ctrlRng.addElement(ctrlElm);
  389. ctrlRng.select();
  390. // Check if the range produced is on the correct element and is a control range
  391. // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398
  392. nativeRng = selection.getRng();
  393. if (nativeRng.item && ctrlElm === nativeRng.item(0)) {
  394. return;
  395. }
  396. } catch (ex) {
  397. // Ignore
  398. }
  399. }
  400. }
  401. // Set start/end point of selection
  402. setEndPoint(true);
  403. setEndPoint();
  404. // Select the new range and scroll it into view
  405. ieRng.select();
  406. };
  407. // Expose range method
  408. this.getRangeAt = getRange;
  409. }
  410. return Selection;
  411. });