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