]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/javascript/tiny_mce/classes/dom/Selection.js
Release 6.2.3
[Github/sugarcrm.git] / include / javascript / tiny_mce / classes / dom / Selection.js
1 /**
2  * Selection.js
3  *
4  * Copyright 2009, Moxiecode Systems AB
5  * Released under LGPL License.
6  *
7  * License: http://tinymce.moxiecode.com/license
8  * Contributing: http://tinymce.moxiecode.com/contributing
9  */
10
11 (function(tinymce) {
12         function trimNl(s) {
13                 return s.replace(/[\n\r]+/g, '');
14         };
15
16         // Shorten names
17         var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each;
18
19         /**
20          * This class handles text and control selection it's an crossbrowser utility class.
21          * Consult the TinyMCE Wiki API for more details and examples on how to use this class.
22          *
23          * @class tinymce.dom.Selection
24          * @example
25          * // Getting the currently selected node for the active editor
26          * alert(tinymce.activeEditor.selection.getNode().nodeName);
27          */
28         tinymce.create('tinymce.dom.Selection', {
29                 /**
30                  * Constructs a new selection instance.
31                  *
32                  * @constructor
33                  * @method Selection
34                  * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
35                  * @param {Window} win Window to bind the selection object to.
36                  * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
37                  */
38                 Selection : function(dom, win, serializer) {
39                         var t = this;
40
41                         t.dom = dom;
42                         t.win = win;
43                         t.serializer = serializer;
44
45                         // Add events
46                         each([
47                                 /**
48                                  * This event gets executed before contents is extracted from the selection.
49                                  *
50                                  * @event onBeforeSetContent
51                                  * @param {tinymce.dom.Selection} selection Selection object that fired the event.
52                                  * @param {Object} args Contains things like the contents that will be returned. 
53                                  */
54                                 'onBeforeSetContent',
55
56                                 /**
57                                  * This event gets executed before contents is inserted into selection. 
58                                  *
59                                  * @event onBeforeGetContent
60                                  * @param {tinymce.dom.Selection} selection Selection object that fired the event.
61                                  * @param {Object} args Contains things like the contents that will be inserted. 
62                                  */
63                                 'onBeforeGetContent',
64
65                                 /**
66                                  * This event gets executed when contents is inserted into selection.
67                                  *
68                                  * @event onSetContent
69                                  * @param {tinymce.dom.Selection} selection Selection object that fired the event.
70                                  * @param {Object} args Contains things like the contents that will be inserted. 
71                                  */
72                                 'onSetContent',
73
74                                 /**
75                                  * This event gets executed when contents is extracted from the selection.
76                                  *
77                                  * @event onGetContent
78                                  * @param {tinymce.dom.Selection} selection Selection object that fired the event.
79                                  * @param {Object} args Contains things like the contents that will be returned. 
80                                  */
81                                 'onGetContent'
82                         ], function(e) {
83                                 t[e] = new tinymce.util.Dispatcher(t);
84                         });
85
86                         // No W3C Range support
87                         if (!t.win.getSelection)
88                                 t.tridentSel = new tinymce.dom.TridentSelection(t);
89
90                         if (tinymce.isIE && dom.boxModel)
91                                 this._fixIESelection();
92
93                         // Prevent leaks
94                         tinymce.addUnload(t.destroy, t);
95                 },
96
97                 /**
98                  * Move the selection cursor range to the specified node and offset.
99                  * @param node Node to put the cursor in.
100                  * @param offset Offset from the start of the node to put the cursor at.
101                  */
102                 setCursorLocation: function(node, offset) {
103                         var t = this; var r = t.dom.createRng();
104                         r.setStart(node, offset);
105                         r.setEnd(node, offset);
106                         t.setRng(r);
107                         t.collapse(false);
108                 },
109                 /**
110                  * Returns the selected contents using the DOM serializer passed in to this class.
111                  *
112                  * @method getContent
113                  * @param {Object} s Optional settings class with for example output format text or html.
114                  * @return {String} Selected contents in for example HTML format.
115                  * @example
116                  * // Alerts the currently selected contents
117                  * alert(tinyMCE.activeEditor.selection.getContent());
118                  * 
119                  * // Alerts the currently selected contents as plain text
120                  * alert(tinyMCE.activeEditor.selection.getContent({format : 'text'}));
121                  */
122                 getContent : function(s) {
123                         var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;
124
125                         s = s || {};
126                         wb = wa = '';
127                         s.get = true;
128                         s.format = s.format || 'html';
129                         s.forced_root_block = '';
130                         t.onBeforeGetContent.dispatch(t, s);
131
132                         if (s.format == 'text')
133                                 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));
134
135                         if (r.cloneContents) {
136                                 n = r.cloneContents();
137
138                                 if (n)
139                                         e.appendChild(n);
140                         } else if (is(r.item) || is(r.htmlText))
141                                 e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText;
142                         else
143                                 e.innerHTML = r.toString();
144
145                         // Keep whitespace before and after
146                         if (/^\s/.test(e.innerHTML))
147                                 wb = ' ';
148
149                         if (/\s+$/.test(e.innerHTML))
150                                 wa = ' ';
151
152                         s.getInner = true;
153
154                         s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
155                         t.onGetContent.dispatch(t, s);
156
157                         return s.content;
158                 },
159
160                 /**
161                  * Sets the current selection to the specified content. If any contents is selected it will be replaced
162                  * with the contents passed in to this function. If there is no selection the contents will be inserted
163                  * where the caret is placed in the editor/page.
164                  *
165                  * @method setContent
166                  * @param {String} content HTML contents to set could also be other formats depending on settings.
167                  * @param {Object} args Optional settings object with for example data format.
168                  * @example
169                  * // Inserts some HTML contents at the current selection
170                  * tinyMCE.activeEditor.selection.setContent('<strong>Some contents</strong>');
171                  */
172                 setContent : function(content, args) {
173                         var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
174
175                         args = args || {format : 'html'};
176                         args.set = true;
177                         content = args.content = content;
178
179                         // Dispatch before set content event
180                         if (!args.no_events)
181                                 self.onBeforeSetContent.dispatch(self, args);
182
183                         content = args.content;
184
185                         if (rng.insertNode) {
186                                 // Make caret marker since insertNode places the caret in the beginning of text after insert
187                                 content += '<span id="__caret">_</span>';
188
189                                 // Delete and insert new node
190                                 if (rng.startContainer == doc && rng.endContainer == doc) {
191                                         // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
192                                         doc.body.innerHTML = content;
193                                 } else {
194                                         rng.deleteContents();
195
196                                         if (doc.body.childNodes.length == 0) {
197                                                 doc.body.innerHTML = content;
198                                         } else {
199                                                 // createContextualFragment doesn't exists in IE 9 DOMRanges
200                                                 if (rng.createContextualFragment) {
201                                                         rng.insertNode(rng.createContextualFragment(content));
202                                                 } else {
203                                                         // Fake createContextualFragment call in IE 9
204                                                         frag = doc.createDocumentFragment();
205                                                         temp = doc.createElement('div');
206
207                                                         frag.appendChild(temp);
208                                                         temp.outerHTML = content;
209
210                                                         rng.insertNode(frag);
211                                                 }
212                                         }
213                                 }
214
215                                 // Move to caret marker
216                                 caretNode = self.dom.get('__caret');
217
218                                 // Make sure we wrap it compleatly, Opera fails with a simple select call
219                                 rng = doc.createRange();
220                                 rng.setStartBefore(caretNode);
221                                 rng.setEndBefore(caretNode);
222                                 self.setRng(rng);
223
224                                 // Remove the caret position
225                                 self.dom.remove('__caret');
226
227                                 try {
228                                         self.setRng(rng);
229                                 } catch (ex) {
230                                         // Might fail on Opera for some odd reason
231                                 }
232                         } else {
233                                 if (rng.item) {
234                                         // Delete content and get caret text selection
235                                         doc.execCommand('Delete', false, null);
236                                         rng = self.getRng();
237                                 }
238
239                                 rng.pasteHTML(content);
240                         }
241
242                         // Dispatch set content event
243                         if (!args.no_events)
244                                 self.onSetContent.dispatch(self, args);
245                 },
246
247                 /**
248                  * Returns the start element of a selection range. If the start is in a text
249                  * node the parent element will be returned.
250                  *
251                  * @method getStart
252                  * @return {Element} Start element of selection range.
253                  */
254                 getStart : function() {
255                         var rng = this.getRng(), startElement, parentElement, checkRng, node;
256
257                         if (rng.duplicate || rng.item) {
258                                 // Control selection, return first item
259                                 if (rng.item)
260                                         return rng.item(0);
261
262                                 // Get start element
263                                 checkRng = rng.duplicate();
264                                 checkRng.collapse(1);
265                                 startElement = checkRng.parentElement();
266
267                                 // Check if range parent is inside the start element, then return the inner parent element
268                                 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
269                                 parentElement = node = rng.parentElement();
270                                 while (node = node.parentNode) {
271                                         if (node == startElement) {
272                                                 startElement = parentElement;
273                                                 break;
274                                         }
275                                 }
276
277                                 return startElement;
278                         } else {
279                                 startElement = rng.startContainer;
280
281                                 if (startElement.nodeType == 1 && startElement.hasChildNodes())
282                                         startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
283
284                                 if (startElement && startElement.nodeType == 3)
285                                         return startElement.parentNode;
286
287                                 return startElement;
288                         }
289                 },
290
291                 /**
292                  * Returns the end element of a selection range. If the end is in a text
293                  * node the parent element will be returned.
294                  *
295                  * @method getEnd
296                  * @return {Element} End element of selection range.
297                  */
298                 getEnd : function() {
299                         var t = this, r = t.getRng(), e, eo;
300
301                         if (r.duplicate || r.item) {
302                                 if (r.item)
303                                         return r.item(0);
304
305                                 r = r.duplicate();
306                                 r.collapse(0);
307                                 e = r.parentElement();
308
309                                 if (e && e.nodeName == 'BODY')
310                                         return e.lastChild || e;
311
312                                 return e;
313                         } else {
314                                 e = r.endContainer;
315                                 eo = r.endOffset;
316
317                                 if (e.nodeType == 1 && e.hasChildNodes())
318                                         e = e.childNodes[eo > 0 ? eo - 1 : eo];
319
320                                 if (e && e.nodeType == 3)
321                                         return e.parentNode;
322
323                                 return e;
324                         }
325                 },
326
327                 /**
328                  * Returns a bookmark location for the current selection. This bookmark object
329                  * can then be used to restore the selection after some content modification to the document.
330                  *
331                  * @method getBookmark
332                  * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
333                  * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
334                  * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
335                  * @example
336                  * // Stores a bookmark of the current selection
337                  * var bm = tinyMCE.activeEditor.selection.getBookmark();
338                  * 
339                  * tinyMCE.activeEditor.setContent(tinyMCE.activeEditor.getContent() + 'Some new content');
340                  * 
341                  * // Restore the selection bookmark
342                  * tinyMCE.activeEditor.selection.moveToBookmark(bm);
343                  */
344                 getBookmark : function(type, normalized) {
345                         var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles;
346
347                         function findIndex(name, element) {
348                                 var index = 0;
349
350                                 each(dom.select(name), function(node, i) {
351                                         if (node == element)
352                                                 index = i;
353                                 });
354
355                                 return index;
356                         };
357
358                         if (type == 2) {
359                                 function getLocation() {
360                                         var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};
361
362                                         function getPoint(rng, start) {
363                                                 var container = rng[start ? 'startContainer' : 'endContainer'],
364                                                         offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
365
366                                                 if (container.nodeType == 3) {
367                                                         if (normalized) {
368                                                                 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling)
369                                                                         offset += node.nodeValue.length;
370                                                         }
371
372                                                         point.push(offset);
373                                                 } else {
374                                                         childNodes = container.childNodes;
375
376                                                         if (offset >= childNodes.length && childNodes.length) {
377                                                                 after = 1;
378                                                                 offset = Math.max(0, childNodes.length - 1);
379                                                         }
380
381                                                         point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);
382                                                 }
383
384                                                 for (; container && container != root; container = container.parentNode)
385                                                         point.push(t.dom.nodeIndex(container, normalized));
386
387                                                 return point;
388                                         };
389
390                                         bookmark.start = getPoint(rng, true);
391
392                                         if (!t.isCollapsed())
393                                                 bookmark.end = getPoint(rng);
394
395                                         return bookmark;
396                                 };
397
398                                 if (t.tridentSel)
399                                         return t.tridentSel.getBookmark(type);
400
401                                 return getLocation();
402                         }
403
404                         // Handle simple range
405                         if (type)
406                                 return {rng : t.getRng()};
407
408                         rng = t.getRng();
409                         id = dom.uniqueId();
410                         collapsed = tinyMCE.activeEditor.selection.isCollapsed();
411                         styles = 'overflow:hidden;line-height:0px';
412
413                         // Explorer method
414                         if (rng.duplicate || rng.item) {
415                                 // Text selection
416                                 if (!rng.item) {
417                                         rng2 = rng.duplicate();
418
419                                         try {
420                                                 // Insert start marker
421                                                 rng.collapse();
422                                                 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
423
424                                                 // Insert end marker
425                                                 if (!collapsed) {
426                                                         rng2.collapse(false);
427
428                                                         // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p>
429                                                         rng.moveToElementText(rng2.parentElement());
430                                                         if (rng.compareEndPoints('StartToEnd', rng2) == 0)
431                                                                 rng2.move('character', -1);
432
433                                                         rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
434                                                 }
435                                         } catch (ex) {
436                                                 // IE might throw unspecified error so lets ignore it
437                                                 return null;
438                                         }
439                                 } else {
440                                         // Control selection
441                                         element = rng.item(0);
442                                         name = element.nodeName;
443
444                                         return {name : name, index : findIndex(name, element)};
445                                 }
446                         } else {
447                                 element = t.getNode();
448                                 name = element.nodeName;
449                                 if (name == 'IMG')
450                                         return {name : name, index : findIndex(name, element)};
451
452                                 // W3C method
453                                 rng2 = rng.cloneRange();
454
455                                 // Insert end marker
456                                 if (!collapsed) {
457                                         rng2.collapse(false);
458                                         rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr));
459                                 }
460
461                                 rng.collapse(true);
462                                 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr));
463                         }
464
465                         t.moveToBookmark({id : id, keep : 1});
466
467                         return {id : id};
468                 },
469
470                 /**
471                  * Restores the selection to the specified bookmark.
472                  *
473                  * @method moveToBookmark
474                  * @param {Object} bookmark Bookmark to restore selection from.
475                  * @return {Boolean} true/false if it was successful or not.
476                  * @example
477                  * // Stores a bookmark of the current selection
478                  * var bm = tinyMCE.activeEditor.selection.getBookmark();
479                  * 
480                  * tinyMCE.activeEditor.setContent(tinyMCE.activeEditor.getContent() + 'Some new content');
481                  * 
482                  * // Restore the selection bookmark
483                  * tinyMCE.activeEditor.selection.moveToBookmark(bm);
484                  */
485                 moveToBookmark : function(bookmark) {
486                         var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;
487
488                         if (bookmark) {
489                                 if (bookmark.start) {
490                                         rng = dom.createRng();
491                                         root = dom.getRoot();
492
493                                         function setEndPoint(start) {
494                                                 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
495
496                                                 if (point) {
497                                                         offset = point[0];
498
499                                                         // Find container node
500                                                         for (node = root, i = point.length - 1; i >= 1; i--) {
501                                                                 children = node.childNodes;
502
503                                                                 if (point[i] > children.length - 1)
504                                                                         return;
505
506                                                                 node = children[point[i]];
507                                                         }
508
509                                                         // Move text offset to best suitable location
510                                                         if (node.nodeType === 3)
511                                                                 offset = Math.min(point[0], node.nodeValue.length);
512
513                                                         // Move element offset to best suitable location
514                                                         if (node.nodeType === 1)
515                                                                 offset = Math.min(point[0], node.childNodes.length);
516
517                                                         // Set offset within container node
518                                                         if (start)
519                                                                 rng.setStart(node, offset);
520                                                         else
521                                                                 rng.setEnd(node, offset);
522                                                 }
523
524                                                 return true;
525                                         };
526
527                                         if (t.tridentSel)
528                                                 return t.tridentSel.moveToBookmark(bookmark);
529
530                                         if (setEndPoint(true) && setEndPoint()) {
531                                                 t.setRng(rng);
532                                         }
533                                 } else if (bookmark.id) {
534                                         function restoreEndPoint(suffix) {
535                                                 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
536
537                                                 if (marker) {
538                                                         node = marker.parentNode;
539
540                                                         if (suffix == 'start') {
541                                                                 if (!keep) {
542                                                                         idx = dom.nodeIndex(marker);
543                                                                 } else {
544                                                                         node = marker.firstChild;
545                                                                         idx = 1;
546                                                                 }
547
548                                                                 startContainer = endContainer = node;
549                                                                 startOffset = endOffset = idx;
550                                                         } else {
551                                                                 if (!keep) {
552                                                                         idx = dom.nodeIndex(marker);
553                                                                 } else {
554                                                                         node = marker.firstChild;
555                                                                         idx = 1;
556                                                                 }
557
558                                                                 endContainer = node;
559                                                                 endOffset = idx;
560                                                         }
561
562                                                         if (!keep) {
563                                                                 prev = marker.previousSibling;
564                                                                 next = marker.nextSibling;
565
566                                                                 // Remove all marker text nodes
567                                                                 each(tinymce.grep(marker.childNodes), function(node) {
568                                                                         if (node.nodeType == 3)
569                                                                                 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
570                                                                 });
571
572                                                                 // Remove marker but keep children if for example contents where inserted into the marker
573                                                                 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature
574                                                                 while (marker = dom.get(bookmark.id + '_' + suffix))
575                                                                         dom.remove(marker, 1);
576
577                                                                 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
578                                                                 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact
579                                                                 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) {
580                                                                         idx = prev.nodeValue.length;
581                                                                         prev.appendData(next.nodeValue);
582                                                                         dom.remove(next);
583
584                                                                         if (suffix == 'start') {
585                                                                                 startContainer = endContainer = prev;
586                                                                                 startOffset = endOffset = idx;
587                                                                         } else {
588                                                                                 endContainer = prev;
589                                                                                 endOffset = idx;
590                                                                         }
591                                                                 }
592                                                         }
593                                                 }
594                                         };
595
596                                         function addBogus(node) {
597                                                 // Adds a bogus BR element for empty block elements or just a space on IE since it renders BR elements incorrectly
598                                                 if (dom.isBlock(node) && !node.innerHTML)
599                                                         node.innerHTML = !isIE ? '<br data-mce-bogus="1" />' : ' ';
600
601                                                 return node;
602                                         };
603
604                                         // Restore start/end points
605                                         restoreEndPoint('start');
606                                         restoreEndPoint('end');
607
608                                         if (startContainer) {
609                                                 rng = dom.createRng();
610                                                 rng.setStart(addBogus(startContainer), startOffset);
611                                                 rng.setEnd(addBogus(endContainer), endOffset);
612                                                 t.setRng(rng);
613                                         }
614                                 } else if (bookmark.name) {
615                                         t.select(dom.select(bookmark.name)[bookmark.index]);
616                                 } else if (bookmark.rng)
617                                         t.setRng(bookmark.rng);
618                         }
619                 },
620
621                 /**
622                  * Selects the specified element. This will place the start and end of the selection range around the element.
623                  *
624                  * @method select
625                  * @param {Element} node HMTL DOM element to select.
626                  * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
627                  * @return {Element} Selected element the same element as the one that got passed in.
628                  * @example
629                  * // Select the first paragraph in the active editor
630                  * tinyMCE.activeEditor.selection.select(tinyMCE.activeEditor.dom.select('p')[0]);
631                  */
632                 select : function(node, content) {
633                         var t = this, dom = t.dom, rng = dom.createRng(), idx;
634
635                         if (node) {
636                                 idx = dom.nodeIndex(node);
637                                 rng.setStart(node.parentNode, idx);
638                                 rng.setEnd(node.parentNode, idx + 1);
639
640                                 // Find first/last text node or BR element
641                                 if (content) {
642                                         function setPoint(node, start) {
643                                                 var walker = new tinymce.dom.TreeWalker(node, node);
644
645                                                 do {
646                                                         // Text node
647                                                         if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
648                                                                 if (start)
649                                                                         rng.setStart(node, 0);
650                                                                 else
651                                                                         rng.setEnd(node, node.nodeValue.length);
652
653                                                                 return;
654                                                         }
655
656                                                         // BR element
657                                                         if (node.nodeName == 'BR') {
658                                                                 if (start)
659                                                                         rng.setStartBefore(node);
660                                                                 else
661                                                                         rng.setEndBefore(node);
662
663                                                                 return;
664                                                         }
665                                                 } while (node = (start ? walker.next() : walker.prev()));
666                                         };
667
668                                         setPoint(node, 1);
669                                         setPoint(node);
670                                 }
671
672                                 t.setRng(rng);
673                         }
674
675                         return node;
676                 },
677
678                 /**
679                  * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
680                  *
681                  * @method isCollapsed
682                  * @return {Boolean} true/false state if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
683                  */
684                 isCollapsed : function() {
685                         var t = this, r = t.getRng(), s = t.getSel();
686
687                         if (!r || r.item)
688                                 return false;
689
690                         if (r.compareEndPoints)
691                                 return r.compareEndPoints('StartToEnd', r) === 0;
692
693                         return !s || r.collapsed;
694                 },
695
696                 /**
697                  * Collapse the selection to start or end of range.
698                  *
699                  * @method collapse
700                  * @param {Boolean} to_start Optional boolean state if to collapse to end or not. Defaults to start.
701                  */
702                 collapse : function(to_start) {
703                         var self = this, rng = self.getRng(), node;
704
705                         // Control range on IE
706                         if (rng.item) {
707                                 node = rng.item(0);
708                                 rng = self.win.document.body.createTextRange();
709                                 rng.moveToElementText(node);
710                         }
711
712                         rng.collapse(!!to_start);
713                         self.setRng(rng);
714                 },
715
716                 /**
717                  * Returns the browsers internal selection object.
718                  *
719                  * @method getSel
720                  * @return {Selection} Internal browser selection object.
721                  */
722                 getSel : function() {
723                         var t = this, w = this.win;
724
725                         return w.getSelection ? w.getSelection() : w.document.selection;
726                 },
727
728                 /**
729                  * Returns the browsers internal range object.
730                  *
731                  * @method getRng
732                  * @param {Boolean} w3c Forces a compatible W3C range on IE.
733                  * @return {Range} Internal browser range object.
734                  * @see http://www.quirksmode.org/dom/range_intro.html
735                  * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
736                  */
737                 getRng : function(w3c) {
738                         var t = this, s, r, elm, doc = t.win.document;
739
740                         // Found tridentSel object then we need to use that one
741                         if (w3c && t.tridentSel)
742                                 return t.tridentSel.getRangeAt(0);
743
744                         try {
745                                 if (s = t.getSel())
746                                         r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange());
747                         } catch (ex) {
748                                 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
749                         }
750
751                         // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
752                         if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) {
753                                 elm = doc.selection.createRange().item(0);
754                                 r = doc.createRange();
755                                 r.setStartBefore(elm);
756                                 r.setEndAfter(elm);
757                         }
758
759                         // No range found then create an empty one
760                         // This can occur when the editor is placed in a hidden container element on Gecko
761                         // Or on IE when there was an exception
762                         if (!r)
763                                 r = doc.createRange ? doc.createRange() : doc.body.createTextRange();
764
765                         if (t.selectedRange && t.explicitRange) {
766                                 if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) {
767                                         // Safari, Opera and Chrome only ever select text which causes the range to change.
768                                         // This lets us use the originally set range if the selection hasn't been changed by the user.
769                                         r = t.explicitRange;
770                                 } else {
771                                         t.selectedRange = null;
772                                         t.explicitRange = null;
773                                 }
774                         }
775
776                         return r;
777                 },
778
779                 /**
780                  * Changes the selection to the specified DOM range.
781                  *
782                  * @method setRng
783                  * @param {Range} r Range to select.
784                  */
785                 setRng : function(r) {
786                         var s, t = this;
787                         
788                         if (!t.tridentSel) {
789                                 s = t.getSel();
790
791                                 if (s) {
792                                         t.explicitRange = r;
793
794                                         try {
795                                                 s.removeAllRanges();
796                                         } catch (ex) {
797                                                 // IE9 might throw errors here don't know why
798                                         }
799
800                                         s.addRange(r);
801                                         t.selectedRange = s.getRangeAt(0);
802                                 }
803                         } else {
804                                 // Is W3C Range
805                                 if (r.cloneRange) {
806                                         t.tridentSel.addRange(r);
807                                         return;
808                                 }
809
810                                 // Is IE specific range
811                                 try {
812                                         r.select();
813                                 } catch (ex) {
814                                         // Needed for some odd IE bug #1843306
815                                 }
816                         }
817                 },
818
819                 /**
820                  * Sets the current selection to the specified DOM element.
821                  *
822                  * @method setNode
823                  * @param {Element} n Element to set as the contents of the selection.
824                  * @return {Element} Returns the element that got passed in.
825                  * @example
826                  * // Inserts a DOM node at current selection/caret location
827                  * tinyMCE.activeEditor.selection.setNode(tinyMCE.activeEditor.dom.create('img', {src : 'some.gif', title : 'some title'}));
828                  */
829                 setNode : function(n) {
830                         var t = this;
831
832                         t.setContent(t.dom.getOuterHTML(n));
833
834                         return n;
835                 },
836
837                 /**
838                  * Returns the currently selected element or the common ancestor element for both start and end of the selection.
839                  *
840                  * @method getNode
841                  * @return {Element} Currently selected element or common ancestor element.
842                  * @example
843                  * // Alerts the currently selected elements node name
844                  * alert(tinyMCE.activeEditor.selection.getNode().nodeName);
845                  */
846                 getNode : function() {
847                         var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer;
848
849                         // Range maybe lost after the editor is made visible again
850                         if (!rng)
851                                 return t.dom.getRoot();
852
853                         if (rng.setStart) {
854                                 elm = rng.commonAncestorContainer;
855
856                                 // Handle selection a image or other control like element such as anchors
857                                 if (!rng.collapsed) {
858                                         if (rng.startContainer == rng.endContainer) {
859                                                 if (rng.endOffset - rng.startOffset < 2) {
860                                                         if (rng.startContainer.hasChildNodes())
861                                                                 elm = rng.startContainer.childNodes[rng.startOffset];
862                                                 }
863                                         }
864
865                                         // If the anchor node is a element instead of a text node then return this element
866                                         //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 
867                                         //      return sel.anchorNode.childNodes[sel.anchorOffset];
868
869                                         // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
870                                         // This happens when you double click an underlined word in FireFox.
871                                         if (start.nodeType === 3 && end.nodeType === 3) {
872                                                 function skipEmptyTextNodes(n, forwards) {
873                                                         var orig = n;
874                                                         while (n && n.nodeType === 3 && n.length === 0) {
875                                                                 n = forwards ? n.nextSibling : n.previousSibling;
876                                                         }
877                                                         return n || orig;
878                                                 }
879                                                 if (start.length === rng.startOffset) {
880                                                         start = skipEmptyTextNodes(start.nextSibling, true);
881                                                 } else {
882                                                         start = start.parentNode;
883                                                 }
884                                                 if (rng.endOffset === 0) {
885                                                         end = skipEmptyTextNodes(end.previousSibling, false);
886                                                 } else {
887                                                         end = end.parentNode;
888                                                 }
889
890                                                 if (start && start === end)
891                                                         return start;
892                                         }
893                                 }
894
895                                 if (elm && elm.nodeType == 3)
896                                         return elm.parentNode;
897
898                                 return elm;
899                         }
900
901                         return rng.item ? rng.item(0) : rng.parentElement();
902                 },
903
904                 getSelectedBlocks : function(st, en) {
905                         var t = this, dom = t.dom, sb, eb, n, bl = [];
906
907                         sb = dom.getParent(st || t.getStart(), dom.isBlock);
908                         eb = dom.getParent(en || t.getEnd(), dom.isBlock);
909
910                         if (sb)
911                                 bl.push(sb);
912
913                         if (sb && eb && sb != eb) {
914                                 n = sb;
915
916                                 while ((n = n.nextSibling) && n != eb) {
917                                         if (dom.isBlock(n))
918                                                 bl.push(n);
919                                 }
920                         }
921
922                         if (eb && sb != eb)
923                                 bl.push(eb);
924
925                         return bl;
926                 },
927
928                 normalize : function() {
929                         var self = this, rng, normalized;
930
931                         // Normalize only on non IE browsers for now
932                         if (tinymce.isIE)
933                                 return;
934
935                         function normalizeEndPoint(start) {
936                                 var container, offset, walker, dom = self.dom, body = dom.getRoot(), node;
937
938                                 container = rng[(start ? 'start' : 'end') + 'Container'];
939                                 offset = rng[(start ? 'start' : 'end') + 'Offset'];
940
941                                 // If the container is a document move it to the body element
942                                 if (container.nodeType === 9) {
943                                         container = container.body;
944                                         offset = 0;
945                                 }
946
947                                 // If the container is body try move it into the closest text node or position
948                                 // TODO: Add more logic here to handle element selection cases
949                                 if (container === body) {
950                                         // Resolve the index
951                                         if (container.hasChildNodes()) {
952                                                 container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)];
953                                                 offset = 0;
954
955                                                 // Walk the DOM to find a text node to place the caret at or a BR
956                                                 node = container;
957                                                 walker = new tinymce.dom.TreeWalker(container, body);
958                                                 do {
959                                                         // Found a text node use that position
960                                                         if (node.nodeType === 3) {
961                                                                 offset = start ? 0 : node.nodeValue.length - 1;
962                                                                 container = node;
963                                                                 break;
964                                                         }
965
966                                                         // Found a BR element that we can place the caret before
967                                                         if (node.nodeName === 'BR') {
968                                                                 offset = dom.nodeIndex(node);
969                                                                 container = node.parentNode;
970                                                                 break;
971                                                         }
972                                                 } while (node = (start ? walker.next() : walker.prev()));
973
974                                                 normalized = true;
975                                         }
976                                 }
977
978                                 // Set endpoint if it was normalized
979                                 if (normalized)
980                                         rng['set' + (start ? 'Start' : 'End')](container, offset);
981                         };
982
983                         rng = self.getRng();
984
985                         // Normalize the end points
986                         normalizeEndPoint(true);
987                         
988                         if (rng.collapsed)
989                                 normalizeEndPoint();
990
991                         // Set the selection if it was normalized
992                         if (normalized) {
993                                 //console.log(self.dom.dumpRng(rng));
994                                 self.setRng(rng);
995                         }
996                 },
997
998                 destroy : function(s) {
999                         var t = this;
1000
1001                         t.win = null;
1002
1003                         // Manual destroy then remove unload handler
1004                         if (!s)
1005                                 tinymce.removeUnload(t.destroy);
1006                 },
1007
1008                 // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode
1009                 _fixIESelection : function() {
1010                         var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm;
1011
1012                         // Make HTML element unselectable since we are going to handle selection by hand
1013                         doc.documentElement.unselectable = true;
1014
1015                         // Return range from point or null if it failed
1016                         function rngFromPoint(x, y) {
1017                                 var rng = body.createTextRange();
1018
1019                                 try {
1020                                         rng.moveToPoint(x, y);
1021                                 } catch (ex) {
1022                                         // IE sometimes throws and exception, so lets just ignore it
1023                                         rng = null;
1024                                 }
1025
1026                                 return rng;
1027                         };
1028
1029                         // Fires while the selection is changing
1030                         function selectionChange(e) {
1031                                 var pointRng;
1032
1033                                 // Check if the button is down or not
1034                                 if (e.button) {
1035                                         // Create range from mouse position
1036                                         pointRng = rngFromPoint(e.x, e.y);
1037
1038                                         if (pointRng) {
1039                                                 // Check if pointRange is before/after selection then change the endPoint
1040                                                 if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
1041                                                         pointRng.setEndPoint('StartToStart', startRng);
1042                                                 else
1043                                                         pointRng.setEndPoint('EndToEnd', startRng);
1044
1045                                                 pointRng.select();
1046                                         }
1047                                 } else
1048                                         endSelection();
1049                         }
1050
1051                         // Removes listeners
1052                         function endSelection() {
1053                                 var rng = doc.selection.createRange();
1054
1055                                 // If the range is collapsed then use the last start range
1056                                 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0)
1057                                         startRng.select();
1058
1059                                 dom.unbind(doc, 'mouseup', endSelection);
1060                                 dom.unbind(doc, 'mousemove', selectionChange);
1061                                 startRng = started = 0;
1062                         };
1063
1064                         // Detect when user selects outside BODY
1065                         dom.bind(doc, ['mousedown', 'contextmenu'], function(e) {
1066                                 if (e.target.nodeName === 'HTML') {
1067                                         if (started)
1068                                                 endSelection();
1069
1070                                         // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
1071                                         htmlElm = doc.documentElement;
1072                                         if (htmlElm.scrollHeight > htmlElm.clientHeight)
1073                                                 return;
1074
1075                                         started = 1;
1076                                         // Setup start position
1077                                         startRng = rngFromPoint(e.x, e.y);
1078                                         if (startRng) {
1079                                                 // Listen for selection change events
1080                                                 dom.bind(doc, 'mouseup', endSelection);
1081                                                 dom.bind(doc, 'mousemove', selectionChange);
1082
1083                                                 dom.win.focus();
1084                                                 startRng.select();
1085                                         }
1086                                 }
1087                         });
1088                 }
1089         });
1090 })(tinymce);