4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
12 * Selection class for old explorer versions. This one fakes the
13 * native selection object available on modern browsers.
15 * @class tinymce.dom.TridentSelection
17 define("tinymce/dom/TridentSelection", [], function() {
18 function Selection(selection) {
19 var self = this, dom = selection.dom, FALSE = false;
21 function getPosition(rng, start) {
22 var checkRng, startIndex = 0, endIndex, inside,
23 children, child, offset, index, position = -1, parent;
25 // Setup test range, collapse it and get the parent
26 checkRng = rng.duplicate();
27 checkRng.collapse(start);
28 parent = checkRng.parentElement();
30 // Check if the selection is within the right document
31 if (parent.ownerDocument !== selection.dom.doc) {
35 // IE will report non editable elements as it's parent so look for an editable one
36 while (parent.contentEditable === "false") {
37 parent = parent.parentNode;
40 // If parent doesn't have any children then return that we are inside the element
41 if (!parent.hasChildNodes()) {
42 return {node: parent, inside: 1};
45 // Setup node list and endIndex
46 children = parent.children;
47 endIndex = children.length - 1;
49 // Perform a binary search for the position
50 while (startIndex <= endIndex) {
51 index = Math.floor((startIndex + endIndex) / 2);
53 // Move selection to node and compare the ranges
54 child = children[index];
55 checkRng.moveToElementText(child);
56 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
58 // Before/after or an exact match
61 } else if (position < 0) {
62 startIndex = index + 1;
68 // Check if child position is before or we didn't find a position
70 // No element child was found use the parent element and the offset inside that
72 checkRng.moveToElementText(parent);
73 checkRng.collapse(true);
77 checkRng.collapse(false);
80 // Walk character by character in text node until we hit the selected range endpoint,
81 // hit the end of document or parent isn't the right one
82 // We need to walk char by char since rng.text or rng.htmlText will trim line endings
84 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
85 if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) {
92 // Child position is after the selection endpoint
93 checkRng.collapse(true);
95 // Walk character by character in text node until we hit the selected range endpoint, hit
96 // the end of document or parent isn't the right one
98 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
99 if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) {
107 return {node: child, position: position, offset: offset, inside: inside};
110 // Returns a W3C DOM compatible range object by using the IE Range API
111 function getRange() {
112 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark;
114 // If selection is outside the current document just return an empty range
115 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
116 if (element.ownerDocument != dom.doc) {
120 collapsed = selection.isCollapsed();
122 // Handle control selection
124 domRange.setStart(element.parentNode, dom.nodeIndex(element));
125 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
130 function findEndPoint(start) {
131 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
133 container = endPoint.node;
134 offset = endPoint.offset;
136 if (endPoint.inside && !container.hasChildNodes()) {
137 domRange[start ? 'setStart' : 'setEnd'](container, 0);
141 if (offset === undef) {
142 domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
146 if (endPoint.position < 0) {
147 sibling = endPoint.inside ? container.firstChild : container.nextSibling;
150 domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
155 if (sibling.nodeType == 3) {
156 domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
158 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
164 // Find the text node and offset
166 nodeValue = sibling.nodeValue;
167 textNodeOffset += nodeValue.length;
169 // We are at or passed the position we where looking for
170 if (textNodeOffset >= offset) {
172 textNodeOffset -= offset;
173 textNodeOffset = nodeValue.length - textNodeOffset;
177 sibling = sibling.nextSibling;
180 // Find the text node and offset
181 sibling = container.previousSibling;
184 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
187 // If there isn't any text to loop then use the first position
189 if (container.nodeType == 3) {
190 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
192 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
199 textNodeOffset += sibling.nodeValue.length;
201 // We are at or passed the position we where looking for
202 if (textNodeOffset >= offset) {
204 textNodeOffset -= offset;
208 sibling = sibling.previousSibling;
212 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
219 // Find end point if needed
224 // IE has a nasty bug where text nodes might throw "invalid argument" when you
225 // access the nodeValue or other properties of text nodes. This seems to happend when
226 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
227 if (ex.number == -2147024809) {
228 // Get the current selection
229 bookmark = self.getBookmark(2);
232 tmpRange = ieRange.duplicate();
233 tmpRange.collapse(true);
234 element = tmpRange.parentElement();
238 tmpRange = ieRange.duplicate();
239 tmpRange.collapse(false);
240 element2 = tmpRange.parentElement();
241 element2.innerHTML = element2.innerHTML;
244 // Remove the broken elements
245 element.innerHTML = element.innerHTML;
247 // Restore the selection
248 self.moveToBookmark(bookmark);
250 // Since the range has moved we need to re-get it
251 ieRange = selection.getRng();
256 // Find end point if needed
261 throw ex; // Throw other errors
268 this.getBookmark = function(type) {
269 var rng = selection.getRng(), bookmark = {};
271 function getIndexes(node) {
272 var parent, root, children, i, indexes = [];
274 parent = node.parentNode;
275 root = dom.getRoot().parentNode;
277 while (parent != root && parent.nodeType !== 9) {
278 children = parent.children;
282 if (node === children[i]) {
289 parent = parent.parentNode;
295 function getBookmarkEndPoint(start) {
298 position = getPosition(rng, start);
301 position: position.position,
302 offset: position.offset,
303 indexes: getIndexes(position.node),
304 inside: position.inside
309 // Non ubstructive bookmark
311 // Handle text selection
313 bookmark.start = getBookmarkEndPoint(true);
315 if (!selection.isCollapsed()) {
316 bookmark.end = getBookmarkEndPoint();
319 bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))};
326 this.moveToBookmark = function(bookmark) {
327 var rng, body = dom.doc.body;
329 function resolveIndexes(indexes) {
330 var node, i, idx, children;
332 node = dom.getRoot();
333 for (i = indexes.length - 1; i >= 0; i--) {
334 children = node.children;
337 if (idx <= children.length - 1) {
338 node = children[idx];
345 function setBookmarkEndPoint(start) {
346 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset;
349 moveLeft = endPoint.position > 0;
351 moveRng = body.createTextRange();
352 moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
354 offset = endPoint.offset;
355 if (offset !== undef) {
356 moveRng.collapse(endPoint.inside || moveLeft);
357 moveRng.moveStart('character', moveLeft ? -offset : offset);
359 moveRng.collapse(start);
362 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
370 if (bookmark.start) {
371 if (bookmark.start.ctrl) {
372 rng = body.createControlRange();
373 rng.addElement(resolveIndexes(bookmark.start.indexes));
376 rng = body.createTextRange();
377 setBookmarkEndPoint(true);
378 setBookmarkEndPoint();
384 this.addRange = function(rng) {
385 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,
386 doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;
388 function setEndPoint(start) {
389 var container, offset, marker, tmpRng, nodes;
391 marker = dom.create('a');
392 container = start ? startContainer : endContainer;
393 offset = start ? startOffset : endOffset;
394 tmpRng = ieRng.duplicate();
396 if (container == doc || container == doc.documentElement) {
401 if (container.nodeType == 3) {
402 container.parentNode.insertBefore(marker, container);
403 tmpRng.moveToElementText(marker);
404 tmpRng.moveStart('character', offset);
406 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
408 nodes = container.childNodes;
411 if (offset >= nodes.length) {
412 dom.insertAfter(marker, nodes[nodes.length - 1]);
414 container.insertBefore(marker, nodes[offset]);
417 tmpRng.moveToElementText(marker);
418 } else if (container.canHaveHTML) {
419 // Empty node selection for example <div>|</div>
420 // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
421 container.innerHTML = '<span></span>';
422 marker = container.firstChild;
423 tmpRng.moveToElementText(marker);
424 tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
427 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
432 // Setup some shorter versions
433 startContainer = rng.startContainer;
434 startOffset = rng.startOffset;
435 endContainer = rng.endContainer;
436 endOffset = rng.endOffset;
437 ieRng = body.createTextRange();
439 // If single element selection then try making a control selection out of it
440 if (startContainer == endContainer && startContainer.nodeType == 1) {
441 // Trick to place the caret inside an empty block element like <p></p>
442 if (startOffset == endOffset && !startContainer.hasChildNodes()) {
443 if (startContainer.canHaveHTML) {
444 // Check if previous sibling is an empty block if it is then we need to render it
445 // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236
446 // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>
447 sibling = startContainer.previousSibling;
448 if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
449 sibling.innerHTML = '';
454 startContainer.innerHTML = '<span></span><span></span>';
455 ieRng.moveToElementText(startContainer.lastChild);
457 dom.doc.selection.clear();
458 startContainer.innerHTML = '';
461 sibling.innerHTML = '';
465 startOffset = dom.nodeIndex(startContainer);
466 startContainer = startContainer.parentNode;
470 if (startOffset == endOffset - 1) {
472 ctrlElm = startContainer.childNodes[startOffset];
473 ctrlRng = body.createControlRange();
474 ctrlRng.addElement(ctrlElm);
477 // Check if the range produced is on the correct element and is a control range
478 // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398
479 nativeRng = selection.getRng();
480 if (nativeRng.item && ctrlElm === nativeRng.item(0)) {
489 // Set start/end point of selection
493 // Select the new range and scroll it into view
497 // Expose range method
498 this.getRangeAt = getRange;