4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
12 function Selection(selection) {
13 var self = this, dom = selection.dom, TRUE = true, FALSE = false;
15 function getPosition(rng, start) {
16 var checkRng, startIndex = 0, endIndex, inside,
17 children, child, offset, index, position = -1, parent;
19 // Setup test range, collapse it and get the parent
20 checkRng = rng.duplicate();
21 checkRng.collapse(start);
22 parent = checkRng.parentElement();
24 // Check if the selection is within the right document
25 if (parent.ownerDocument !== selection.dom.doc)
28 // IE will report non editable elements as it's parent so look for an editable one
29 while (parent.contentEditable === "false") {
30 parent = parent.parentNode;
33 // If parent doesn't have any children then return that we are inside the element
34 if (!parent.hasChildNodes()) {
35 return {node : parent, inside : 1};
38 // Setup node list and endIndex
39 children = parent.children;
40 endIndex = children.length - 1;
42 // Perform a binary search for the position
43 while (startIndex <= endIndex) {
44 index = Math.floor((startIndex + endIndex) / 2);
46 // Move selection to node and compare the ranges
47 child = children[index];
48 checkRng.moveToElementText(child);
49 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
51 // Before/after or an exact match
54 } else if (position < 0) {
55 startIndex = index + 1;
57 return {node : child};
61 // Check if child position is before or we didn't find a position
63 // No element child was found use the parent element and the offset inside that
65 checkRng.moveToElementText(parent);
66 checkRng.collapse(true);
70 checkRng.collapse(false);
72 checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng);
74 // Fix for edge case: <div style="width: 100px; height:100px;"><table>..</table>ab|c</div>
75 if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) {
76 checkRng = rng.duplicate();
77 checkRng.collapse(start);
80 while (parent == checkRng.parentElement()) {
81 if (checkRng.move('character', -1) == 0)
88 offset = offset || checkRng.text.replace('\r\n', ' ').length;
90 // Child position is after the selection endpoint
91 checkRng.collapse(true);
92 checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng);
94 // Get the length of the text to find where the endpoint is relative to it's container
95 offset = checkRng.text.replace('\r\n', ' ').length;
98 return {node : child, position : position, offset : offset, inside : inside};
101 // Returns a W3C DOM compatible range object by using the IE Range API
102 function getRange() {
103 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail;
105 // If selection is outside the current document just return an empty range
106 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
107 if (element.ownerDocument != dom.doc)
110 collapsed = selection.isCollapsed();
112 // Handle control selection
114 domRange.setStart(element.parentNode, dom.nodeIndex(element));
115 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
120 function findEndPoint(start) {
121 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
123 container = endPoint.node;
124 offset = endPoint.offset;
126 if (endPoint.inside && !container.hasChildNodes()) {
127 domRange[start ? 'setStart' : 'setEnd'](container, 0);
131 if (offset === undef) {
132 domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
136 if (endPoint.position < 0) {
137 sibling = endPoint.inside ? container.firstChild : container.nextSibling;
140 domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
145 if (sibling.nodeType == 3)
146 domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
148 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
153 // Find the text node and offset
155 nodeValue = sibling.nodeValue;
156 textNodeOffset += nodeValue.length;
158 // We are at or passed the position we where looking for
159 if (textNodeOffset >= offset) {
161 textNodeOffset -= offset;
162 textNodeOffset = nodeValue.length - textNodeOffset;
166 sibling = sibling.nextSibling;
169 // Find the text node and offset
170 sibling = container.previousSibling;
173 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
175 // If there isn't any text to loop then use the first position
177 if (container.nodeType == 3)
178 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
180 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
186 textNodeOffset += sibling.nodeValue.length;
188 // We are at or passed the position we where looking for
189 if (textNodeOffset >= offset) {
191 textNodeOffset -= offset;
195 sibling = sibling.previousSibling;
199 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
206 // Find end point if needed
210 // IE has a nasty bug where text nodes might throw "invalid argument" when you
211 // access the nodeValue or other properties of text nodes. This seems to happend when
212 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
213 if (ex.number == -2147024809) {
214 // Get the current selection
215 bookmark = self.getBookmark(2);
218 tmpRange = ieRange.duplicate();
219 tmpRange.collapse(true);
220 element = tmpRange.parentElement();
224 tmpRange = ieRange.duplicate();
225 tmpRange.collapse(false);
226 element2 = tmpRange.parentElement();
227 element2.innerHTML = element2.innerHTML;
230 // Remove the broken elements
231 element.innerHTML = element.innerHTML;
233 // Restore the selection
234 self.moveToBookmark(bookmark);
236 // Since the range has moved we need to re-get it
237 ieRange = selection.getRng();
242 // Find end point if needed
246 throw ex; // Throw other errors
252 this.getBookmark = function(type) {
253 var rng = selection.getRng(), start, end, bookmark = {};
255 function getIndexes(node) {
256 var node, parent, root, children, i, indexes = [];
258 parent = node.parentNode;
259 root = dom.getRoot().parentNode;
261 while (parent != root) {
262 children = parent.children;
266 if (node === children[i]) {
273 parent = parent.parentNode;
279 function getBookmarkEndPoint(start) {
282 position = getPosition(rng, start);
285 position : position.position,
286 offset : position.offset,
287 indexes : getIndexes(position.node),
288 inside : position.inside
293 // Non ubstructive bookmark
295 // Handle text selection
297 bookmark.start = getBookmarkEndPoint(true);
299 if (!selection.isCollapsed())
300 bookmark.end = getBookmarkEndPoint();
302 bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))};
308 this.moveToBookmark = function(bookmark) {
309 var rng, body = dom.doc.body;
311 function resolveIndexes(indexes) {
312 var node, i, idx, children;
314 node = dom.getRoot();
315 for (i = indexes.length - 1; i >= 0; i--) {
316 children = node.children;
319 if (idx <= children.length - 1) {
320 node = children[idx];
327 function setBookmarkEndPoint(start) {
328 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef;
331 moveLeft = endPoint.position > 0;
333 moveRng = body.createTextRange();
334 moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
336 offset = endPoint.offset;
337 if (offset !== undef) {
338 moveRng.collapse(endPoint.inside || moveLeft);
339 moveRng.moveStart('character', moveLeft ? -offset : offset);
341 moveRng.collapse(start);
343 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
350 if (bookmark.start) {
351 if (bookmark.start.ctrl) {
352 rng = body.createControlRange();
353 rng.addElement(resolveIndexes(bookmark.start.indexes));
356 rng = body.createTextRange();
357 setBookmarkEndPoint(true);
358 setBookmarkEndPoint();
364 this.addRange = function(rng) {
365 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;
367 function setEndPoint(start) {
368 var container, offset, marker, tmpRng, nodes;
370 marker = dom.create('a');
371 container = start ? startContainer : endContainer;
372 offset = start ? startOffset : endOffset;
373 tmpRng = ieRng.duplicate();
375 if (container == doc || container == doc.documentElement) {
380 if (container.nodeType == 3) {
381 container.parentNode.insertBefore(marker, container);
382 tmpRng.moveToElementText(marker);
383 tmpRng.moveStart('character', offset);
385 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
387 nodes = container.childNodes;
390 if (offset >= nodes.length) {
391 dom.insertAfter(marker, nodes[nodes.length - 1]);
393 container.insertBefore(marker, nodes[offset]);
396 tmpRng.moveToElementText(marker);
398 // Empty node selection for example <div>|</div>
399 marker = doc.createTextNode('\uFEFF');
400 container.appendChild(marker);
401 tmpRng.moveToElementText(marker.parentNode);
402 tmpRng.collapse(TRUE);
405 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
410 // Setup some shorter versions
411 startContainer = rng.startContainer;
412 startOffset = rng.startOffset;
413 endContainer = rng.endContainer;
414 endOffset = rng.endOffset;
415 ieRng = body.createTextRange();
417 // If single element selection then try making a control selection out of it
418 if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) {
419 if (startOffset == endOffset - 1) {
421 ctrlRng = body.createControlRange();
422 ctrlRng.addElement(startContainer.childNodes[startOffset]);
431 // Set start/end point of selection
435 // Select the new range and scroll it into view
439 // Expose range method
440 this.getRangeAt = getRange;
443 // Expose the selection object
444 tinymce.dom.TridentSelection = Selection;