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 t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false;
15 // Returns a W3C DOM compatible range object by using the IE Range API
17 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed;
19 // If selection is outside the current document just return an empty range
20 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
21 if (element.ownerDocument != dom.doc)
24 collapsed = selection.isCollapsed();
26 // Handle control selection or text selection of a image
27 if (ieRange.item || !element.hasChildNodes()) {
29 domRange.setStart(element, 0);
30 domRange.setEnd(element, 0);
32 domRange.setStart(element.parentNode, dom.nodeIndex(element));
33 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
39 function findEndPoint(start) {
40 var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position;
42 // Setup temp range and collapse it
43 checkRng = ieRange.duplicate();
44 checkRng.collapse(start);
46 // Create marker and insert it at the end of the endpoints parent
47 marker = dom.create('a');
48 parent = checkRng.parentElement();
50 // If parent doesn't have any children then set the container to that parent and the index to 0
51 if (!parent.hasChildNodes()) {
52 domRange[start ? 'setStart' : 'setEnd'](parent, 0);
56 parent.appendChild(marker);
57 checkRng.moveToElementText(marker);
58 position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);
60 // The position is after the end of the parent element.
61 // This is the case where IE puts the caret to the left edge of a table.
62 domRange[start ? 'setStartAfter' : 'setEndAfter'](parent);
67 // Setup node list and endIndex
68 nodes = tinymce.grep(parent.childNodes);
69 endIndex = nodes.length - 1;
70 // Perform a binary search for the position
71 while (startIndex <= endIndex) {
72 index = Math.floor((startIndex + endIndex) / 2);
74 // Insert marker and check it's position relative to the selection
75 parent.insertBefore(marker, nodes[index]);
76 checkRng.moveToElementText(marker);
77 position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);
79 // Marker is to the right
80 startIndex = index + 1;
81 } else if (position < 0) {
82 // Marker is to the left
85 // Maker is where we are
92 container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling;
94 // Handle element selection
95 if (container.nodeType == 1) {
98 // Find offset and container
99 offset = dom.nodeIndex(container);
100 container = container.parentNode;
102 // Move the offset if we are setting the end or the position is after an element
103 if (!start || index > 0)
106 // Calculate offset within text node
107 if (position > 0 || index == 0) {
108 checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);
109 offset = checkRng.text.length;
111 checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);
112 offset = container.nodeValue.length - checkRng.text.length;
118 domRange[start ? 'setStart' : 'setEnd'](container, offset);
124 // Find end point if needed
131 this.addRange = function(rng) {
132 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;
134 function setEndPoint(start) {
135 var container, offset, marker, tmpRng, nodes;
137 marker = dom.create('a');
138 container = start ? startContainer : endContainer;
139 offset = start ? startOffset : endOffset;
140 tmpRng = ieRng.duplicate();
142 if (container == doc || container == doc.documentElement) {
147 if (container.nodeType == 3) {
148 container.parentNode.insertBefore(marker, container);
149 tmpRng.moveToElementText(marker);
150 tmpRng.moveStart('character', offset);
152 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
154 nodes = container.childNodes;
157 if (offset >= nodes.length) {
158 dom.insertAfter(marker, nodes[nodes.length - 1]);
160 container.insertBefore(marker, nodes[offset]);
163 tmpRng.moveToElementText(marker);
165 // Empty node selection for example <div>|</div>
166 marker = doc.createTextNode(invisibleChar);
167 container.appendChild(marker);
168 tmpRng.moveToElementText(marker.parentNode);
169 tmpRng.collapse(TRUE);
172 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
177 // Destroy cached range
180 // Setup some shorter versions
181 startContainer = rng.startContainer;
182 startOffset = rng.startOffset;
183 endContainer = rng.endContainer;
184 endOffset = rng.endOffset;
185 ieRng = body.createTextRange();
187 // If single element selection then try making a control selection out of it
188 if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) {
189 if (startOffset == endOffset - 1) {
191 ctrlRng = body.createControlRange();
192 ctrlRng.addElement(startContainer.childNodes[startOffset]);
201 // Set start/end point of selection
205 // Select the new range and scroll it into view
209 this.getRangeAt = function() {
210 // Setup new range if the cache is empty
211 if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) {
214 // Store away text range for next call
215 lastIERng = selection.getRng();
218 // IE will say that the range is equal then produce an invalid argument exception
219 // if you perform specific operations in a keyup event. For example Ctrl+Del.
220 // This hack will invalidate the range cache if the exception occurs
222 range.startContainer.nextSibling;
228 // Return cached range
232 this.destroy = function() {
233 // Destroy cached range and last IE range to avoid memory leaks
234 lastIERng = range = null;
238 // Expose the selection object
239 tinymce.dom.TridentSelection = Selection;