src/pyams_skin/resources/js/ext/tinymce/dev/classes/dom/Selection.js
changeset 566 a1707c607eec
parent 565 318533413200
child 567 bca1726b1d85
equal deleted inserted replaced
565:318533413200 566:a1707c607eec
     1 /**
       
     2  * Selection.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  * This class handles text and control selection it's an crossbrowser utility class.
       
    13  * Consult the TinyMCE Wiki API for more details and examples on how to use this class.
       
    14  *
       
    15  * @class tinymce.dom.Selection
       
    16  * @example
       
    17  * // Getting the currently selected node for the active editor
       
    18  * alert(tinymce.activeEditor.selection.getNode().nodeName);
       
    19  */
       
    20 define("tinymce/dom/Selection", [
       
    21 	"tinymce/dom/TreeWalker",
       
    22 	"tinymce/dom/TridentSelection",
       
    23 	"tinymce/dom/ControlSelection",
       
    24 	"tinymce/dom/RangeUtils",
       
    25 	"tinymce/dom/BookmarkManager",
       
    26 	"tinymce/Env",
       
    27 	"tinymce/util/Tools"
       
    28 ], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, BookmarkManager, Env, Tools) {
       
    29 	var each = Tools.each, trim = Tools.trim;
       
    30 	var isIE = Env.ie;
       
    31 
       
    32 	/**
       
    33 	 * Constructs a new selection instance.
       
    34 	 *
       
    35 	 * @constructor
       
    36 	 * @method Selection
       
    37 	 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
       
    38 	 * @param {Window} win Window to bind the selection object to.
       
    39 	 * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
       
    40 	 */
       
    41 	function Selection(dom, win, serializer, editor) {
       
    42 		var self = this;
       
    43 
       
    44 		self.dom = dom;
       
    45 		self.win = win;
       
    46 		self.serializer = serializer;
       
    47 		self.editor = editor;
       
    48 		self.bookmarkManager = new BookmarkManager(self);
       
    49 		self.controlSelection = new ControlSelection(self, editor);
       
    50 
       
    51 		// No W3C Range support
       
    52 		if (!self.win.getSelection) {
       
    53 			self.tridentSel = new TridentSelection(self);
       
    54 		}
       
    55 	}
       
    56 
       
    57 	Selection.prototype = {
       
    58 		/**
       
    59 		 * Move the selection cursor range to the specified node and offset.
       
    60 		 * If there is no node specified it will move it to the first suitable location within the body.
       
    61 		 *
       
    62 		 * @method setCursorLocation
       
    63 		 * @param {Node} node Optional node to put the cursor in.
       
    64 		 * @param {Number} offset Optional offset from the start of the node to put the cursor at.
       
    65 		 */
       
    66 		setCursorLocation: function(node, offset) {
       
    67 			var self = this, rng = self.dom.createRng();
       
    68 
       
    69 			if (!node) {
       
    70 				self._moveEndPoint(rng, self.editor.getBody(), true);
       
    71 				self.setRng(rng);
       
    72 			} else {
       
    73 				rng.setStart(node, offset);
       
    74 				rng.setEnd(node, offset);
       
    75 				self.setRng(rng);
       
    76 				self.collapse(false);
       
    77 			}
       
    78 		},
       
    79 
       
    80 		/**
       
    81 		 * Returns the selected contents using the DOM serializer passed in to this class.
       
    82 		 *
       
    83 		 * @method getContent
       
    84 		 * @param {Object} s Optional settings class with for example output format text or html.
       
    85 		 * @return {String} Selected contents in for example HTML format.
       
    86 		 * @example
       
    87 		 * // Alerts the currently selected contents
       
    88 		 * alert(tinymce.activeEditor.selection.getContent());
       
    89 		 *
       
    90 		 * // Alerts the currently selected contents as plain text
       
    91 		 * alert(tinymce.activeEditor.selection.getContent({format: 'text'}));
       
    92 		 */
       
    93 		getContent: function(args) {
       
    94 			var self = this, rng = self.getRng(), tmpElm = self.dom.create("body");
       
    95 			var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment;
       
    96 
       
    97 			args = args || {};
       
    98 			whiteSpaceBefore = whiteSpaceAfter = '';
       
    99 			args.get = true;
       
   100 			args.format = args.format || 'html';
       
   101 			args.selection = true;
       
   102 			self.editor.fire('BeforeGetContent', args);
       
   103 
       
   104 			if (args.format == 'text') {
       
   105 				return self.isCollapsed() ? '' : (rng.text || (se.toString ? se.toString() : ''));
       
   106 			}
       
   107 
       
   108 			if (rng.cloneContents) {
       
   109 				fragment = rng.cloneContents();
       
   110 
       
   111 				if (fragment) {
       
   112 					tmpElm.appendChild(fragment);
       
   113 				}
       
   114 			} else if (rng.item !== undefined || rng.htmlText !== undefined) {
       
   115 				// IE will produce invalid markup if elements are present that
       
   116 				// it doesn't understand like custom elements or HTML5 elements.
       
   117 				// Adding a BR in front of the contents and then remoiving it seems to fix it though.
       
   118 				tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText);
       
   119 				tmpElm.removeChild(tmpElm.firstChild);
       
   120 			} else {
       
   121 				tmpElm.innerHTML = rng.toString();
       
   122 			}
       
   123 
       
   124 			// Keep whitespace before and after
       
   125 			if (/^\s/.test(tmpElm.innerHTML)) {
       
   126 				whiteSpaceBefore = ' ';
       
   127 			}
       
   128 
       
   129 			if (/\s+$/.test(tmpElm.innerHTML)) {
       
   130 				whiteSpaceAfter = ' ';
       
   131 			}
       
   132 
       
   133 			args.getInner = true;
       
   134 
       
   135 			args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter;
       
   136 			self.editor.fire('GetContent', args);
       
   137 
       
   138 			return args.content;
       
   139 		},
       
   140 
       
   141 		/**
       
   142 		 * Sets the current selection to the specified content. If any contents is selected it will be replaced
       
   143 		 * with the contents passed in to this function. If there is no selection the contents will be inserted
       
   144 		 * where the caret is placed in the editor/page.
       
   145 		 *
       
   146 		 * @method setContent
       
   147 		 * @param {String} content HTML contents to set could also be other formats depending on settings.
       
   148 		 * @param {Object} args Optional settings object with for example data format.
       
   149 		 * @example
       
   150 		 * // Inserts some HTML contents at the current selection
       
   151 		 * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>');
       
   152 		 */
       
   153 		setContent: function(content, args) {
       
   154 			var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
       
   155 
       
   156 			args = args || {format: 'html'};
       
   157 			args.set = true;
       
   158 			args.selection = true;
       
   159 			content = args.content = content;
       
   160 
       
   161 			// Dispatch before set content event
       
   162 			if (!args.no_events) {
       
   163 				self.editor.fire('BeforeSetContent', args);
       
   164 			}
       
   165 
       
   166 			content = args.content;
       
   167 
       
   168 			if (rng.insertNode) {
       
   169 				// Make caret marker since insertNode places the caret in the beginning of text after insert
       
   170 				content += '<span id="__caret">_</span>';
       
   171 
       
   172 				// Delete and insert new node
       
   173 				if (rng.startContainer == doc && rng.endContainer == doc) {
       
   174 					// WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
       
   175 					doc.body.innerHTML = content;
       
   176 				} else {
       
   177 					rng.deleteContents();
       
   178 
       
   179 					if (doc.body.childNodes.length === 0) {
       
   180 						doc.body.innerHTML = content;
       
   181 					} else {
       
   182 						// createContextualFragment doesn't exists in IE 9 DOMRanges
       
   183 						if (rng.createContextualFragment) {
       
   184 							rng.insertNode(rng.createContextualFragment(content));
       
   185 						} else {
       
   186 							// Fake createContextualFragment call in IE 9
       
   187 							frag = doc.createDocumentFragment();
       
   188 							temp = doc.createElement('div');
       
   189 
       
   190 							frag.appendChild(temp);
       
   191 							temp.outerHTML = content;
       
   192 
       
   193 							rng.insertNode(frag);
       
   194 						}
       
   195 					}
       
   196 				}
       
   197 
       
   198 				// Move to caret marker
       
   199 				caretNode = self.dom.get('__caret');
       
   200 
       
   201 				// Make sure we wrap it compleatly, Opera fails with a simple select call
       
   202 				rng = doc.createRange();
       
   203 				rng.setStartBefore(caretNode);
       
   204 				rng.setEndBefore(caretNode);
       
   205 				self.setRng(rng);
       
   206 
       
   207 				// Remove the caret position
       
   208 				self.dom.remove('__caret');
       
   209 
       
   210 				try {
       
   211 					self.setRng(rng);
       
   212 				} catch (ex) {
       
   213 					// Might fail on Opera for some odd reason
       
   214 				}
       
   215 			} else {
       
   216 				if (rng.item) {
       
   217 					// Delete content and get caret text selection
       
   218 					doc.execCommand('Delete', false, null);
       
   219 					rng = self.getRng();
       
   220 				}
       
   221 
       
   222 				// Explorer removes spaces from the beginning of pasted contents
       
   223 				if (/^\s+/.test(content)) {
       
   224 					rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
       
   225 					self.dom.remove('__mce_tmp');
       
   226 				} else {
       
   227 					rng.pasteHTML(content);
       
   228 				}
       
   229 			}
       
   230 
       
   231 			// Dispatch set content event
       
   232 			if (!args.no_events) {
       
   233 				self.editor.fire('SetContent', args);
       
   234 			}
       
   235 		},
       
   236 
       
   237 		/**
       
   238 		 * Returns the start element of a selection range. If the start is in a text
       
   239 		 * node the parent element will be returned.
       
   240 		 *
       
   241 		 * @method getStart
       
   242 		 * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
       
   243 		 * @return {Element} Start element of selection range.
       
   244 		 */
       
   245 		getStart: function(real) {
       
   246 			var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;
       
   247 
       
   248 			if (rng.duplicate || rng.item) {
       
   249 				// Control selection, return first item
       
   250 				if (rng.item) {
       
   251 					return rng.item(0);
       
   252 				}
       
   253 
       
   254 				// Get start element
       
   255 				checkRng = rng.duplicate();
       
   256 				checkRng.collapse(1);
       
   257 				startElement = checkRng.parentElement();
       
   258 				if (startElement.ownerDocument !== self.dom.doc) {
       
   259 					startElement = self.dom.getRoot();
       
   260 				}
       
   261 
       
   262 				// Check if range parent is inside the start element, then return the inner parent element
       
   263 				// This will fix issues when a single element is selected, IE would otherwise return the wrong start element
       
   264 				parentElement = node = rng.parentElement();
       
   265 				while ((node = node.parentNode)) {
       
   266 					if (node == startElement) {
       
   267 						startElement = parentElement;
       
   268 						break;
       
   269 					}
       
   270 				}
       
   271 
       
   272 				return startElement;
       
   273 			} else {
       
   274 				startElement = rng.startContainer;
       
   275 
       
   276 				if (startElement.nodeType == 1 && startElement.hasChildNodes()) {
       
   277 					if (!real || !rng.collapsed) {
       
   278 						startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
       
   279 					}
       
   280 				}
       
   281 
       
   282 				if (startElement && startElement.nodeType == 3) {
       
   283 					return startElement.parentNode;
       
   284 				}
       
   285 
       
   286 				return startElement;
       
   287 			}
       
   288 		},
       
   289 
       
   290 		/**
       
   291 		 * Returns the end element of a selection range. If the end is in a text
       
   292 		 * node the parent element will be returned.
       
   293 		 *
       
   294 		 * @method getEnd
       
   295 		 * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
       
   296 		 * @return {Element} End element of selection range.
       
   297 		 */
       
   298 		getEnd: function(real) {
       
   299 			var self = this, rng = self.getRng(), endElement, endOffset;
       
   300 
       
   301 			if (rng.duplicate || rng.item) {
       
   302 				if (rng.item) {
       
   303 					return rng.item(0);
       
   304 				}
       
   305 
       
   306 				rng = rng.duplicate();
       
   307 				rng.collapse(0);
       
   308 				endElement = rng.parentElement();
       
   309 				if (endElement.ownerDocument !== self.dom.doc) {
       
   310 					endElement = self.dom.getRoot();
       
   311 				}
       
   312 
       
   313 				if (endElement && endElement.nodeName == 'BODY') {
       
   314 					return endElement.lastChild || endElement;
       
   315 				}
       
   316 
       
   317 				return endElement;
       
   318 			} else {
       
   319 				endElement = rng.endContainer;
       
   320 				endOffset = rng.endOffset;
       
   321 
       
   322 				if (endElement.nodeType == 1 && endElement.hasChildNodes()) {
       
   323 					if (!real || !rng.collapsed) {
       
   324 						endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
       
   325 					}
       
   326 				}
       
   327 
       
   328 				if (endElement && endElement.nodeType == 3) {
       
   329 					return endElement.parentNode;
       
   330 				}
       
   331 
       
   332 				return endElement;
       
   333 			}
       
   334 		},
       
   335 
       
   336 		/**
       
   337 		 * Returns a bookmark location for the current selection. This bookmark object
       
   338 		 * can then be used to restore the selection after some content modification to the document.
       
   339 		 *
       
   340 		 * @method getBookmark
       
   341 		 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
       
   342 		 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
       
   343 		 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
       
   344 		 * @example
       
   345 		 * // Stores a bookmark of the current selection
       
   346 		 * var bm = tinymce.activeEditor.selection.getBookmark();
       
   347 		 *
       
   348 		 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
       
   349 		 *
       
   350 		 * // Restore the selection bookmark
       
   351 		 * tinymce.activeEditor.selection.moveToBookmark(bm);
       
   352 		 */
       
   353 		getBookmark: function(type, normalized) {
       
   354 			return this.bookmarkManager.getBookmark(type, normalized);
       
   355 		},
       
   356 
       
   357 		/**
       
   358 		 * Restores the selection to the specified bookmark.
       
   359 		 *
       
   360 		 * @method moveToBookmark
       
   361 		 * @param {Object} bookmark Bookmark to restore selection from.
       
   362 		 * @return {Boolean} true/false if it was successful or not.
       
   363 		 * @example
       
   364 		 * // Stores a bookmark of the current selection
       
   365 		 * var bm = tinymce.activeEditor.selection.getBookmark();
       
   366 		 *
       
   367 		 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
       
   368 		 *
       
   369 		 * // Restore the selection bookmark
       
   370 		 * tinymce.activeEditor.selection.moveToBookmark(bm);
       
   371 		 */
       
   372 		moveToBookmark: function(bookmark) {
       
   373 			return this.bookmarkManager.moveToBookmark(bookmark);
       
   374 		},
       
   375 
       
   376 		/**
       
   377 		 * Selects the specified element. This will place the start and end of the selection range around the element.
       
   378 		 *
       
   379 		 * @method select
       
   380 		 * @param {Element} node HMTL DOM element to select.
       
   381 		 * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
       
   382 		 * @return {Element} Selected element the same element as the one that got passed in.
       
   383 		 * @example
       
   384 		 * // Select the first paragraph in the active editor
       
   385 		 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
       
   386 		 */
       
   387 		select: function(node, content) {
       
   388 			var self = this, dom = self.dom, rng = dom.createRng(), idx;
       
   389 
       
   390 			// Clear stored range set by FocusManager
       
   391 			self.lastFocusBookmark = null;
       
   392 
       
   393 			if (node) {
       
   394 				if (!content && self.controlSelection.controlSelect(node)) {
       
   395 					return;
       
   396 				}
       
   397 
       
   398 				idx = dom.nodeIndex(node);
       
   399 				rng.setStart(node.parentNode, idx);
       
   400 				rng.setEnd(node.parentNode, idx + 1);
       
   401 
       
   402 				// Find first/last text node or BR element
       
   403 				if (content) {
       
   404 					self._moveEndPoint(rng, node, true);
       
   405 					self._moveEndPoint(rng, node);
       
   406 				}
       
   407 
       
   408 				self.setRng(rng);
       
   409 			}
       
   410 
       
   411 			return node;
       
   412 		},
       
   413 
       
   414 		/**
       
   415 		 * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
       
   416 		 *
       
   417 		 * @method isCollapsed
       
   418 		 * @return {Boolean} true/false state if the selection range is collapsed or not.
       
   419 		 * Collapsed means if it's a caret or a larger selection.
       
   420 		 */
       
   421 		isCollapsed: function() {
       
   422 			var self = this, rng = self.getRng(), sel = self.getSel();
       
   423 
       
   424 			if (!rng || rng.item) {
       
   425 				return false;
       
   426 			}
       
   427 
       
   428 			if (rng.compareEndPoints) {
       
   429 				return rng.compareEndPoints('StartToEnd', rng) === 0;
       
   430 			}
       
   431 
       
   432 			return !sel || rng.collapsed;
       
   433 		},
       
   434 
       
   435 		/**
       
   436 		 * Collapse the selection to start or end of range.
       
   437 		 *
       
   438 		 * @method collapse
       
   439 		 * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to start.
       
   440 		 */
       
   441 		collapse: function(toStart) {
       
   442 			var self = this, rng = self.getRng(), node;
       
   443 
       
   444 			// Control range on IE
       
   445 			if (rng.item) {
       
   446 				node = rng.item(0);
       
   447 				rng = self.win.document.body.createTextRange();
       
   448 				rng.moveToElementText(node);
       
   449 			}
       
   450 
       
   451 			rng.collapse(!!toStart);
       
   452 			self.setRng(rng);
       
   453 		},
       
   454 
       
   455 		/**
       
   456 		 * Returns the browsers internal selection object.
       
   457 		 *
       
   458 		 * @method getSel
       
   459 		 * @return {Selection} Internal browser selection object.
       
   460 		 */
       
   461 		getSel: function() {
       
   462 			var win = this.win;
       
   463 
       
   464 			return win.getSelection ? win.getSelection() : win.document.selection;
       
   465 		},
       
   466 
       
   467 		/**
       
   468 		 * Returns the browsers internal range object.
       
   469 		 *
       
   470 		 * @method getRng
       
   471 		 * @param {Boolean} w3c Forces a compatible W3C range on IE.
       
   472 		 * @return {Range} Internal browser range object.
       
   473 		 * @see http://www.quirksmode.org/dom/range_intro.html
       
   474 		 * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
       
   475 		 */
       
   476 		getRng: function(w3c) {
       
   477 			var self = this, selection, rng, elm, doc = self.win.document, ieRng;
       
   478 
       
   479 			function tryCompareBoundaryPoints(how, sourceRange, destinationRange) {
       
   480 				try {
       
   481 					return sourceRange.compareBoundaryPoints(how, destinationRange);
       
   482 				} catch (ex) {
       
   483 					// Gecko throws wrong document exception if the range points
       
   484 					// to nodes that where removed from the dom #6690
       
   485 					// Browsers should mutate existing DOMRange instances so that they always point
       
   486 					// to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
       
   487 					// For performance reasons just return -1
       
   488 					return -1;
       
   489 				}
       
   490 			}
       
   491 
       
   492 			// Use last rng passed from FocusManager if it's available this enables
       
   493 			// calls to editor.selection.getStart() to work when caret focus is lost on IE
       
   494 			if (!w3c && self.lastFocusBookmark) {
       
   495 				var bookmark = self.lastFocusBookmark;
       
   496 
       
   497 				// Convert bookmark to range IE 11 fix
       
   498 				if (bookmark.startContainer) {
       
   499 					rng = doc.createRange();
       
   500 					rng.setStart(bookmark.startContainer, bookmark.startOffset);
       
   501 					rng.setEnd(bookmark.endContainer, bookmark.endOffset);
       
   502 				} else {
       
   503 					rng = bookmark;
       
   504 				}
       
   505 
       
   506 				return rng;
       
   507 			}
       
   508 
       
   509 			// Found tridentSel object then we need to use that one
       
   510 			if (w3c && self.tridentSel) {
       
   511 				return self.tridentSel.getRangeAt(0);
       
   512 			}
       
   513 
       
   514 			try {
       
   515 				if ((selection = self.getSel())) {
       
   516 					if (selection.rangeCount > 0) {
       
   517 						rng = selection.getRangeAt(0);
       
   518 					} else {
       
   519 						rng = selection.createRange ? selection.createRange() : doc.createRange();
       
   520 					}
       
   521 				}
       
   522 			} catch (ex) {
       
   523 				// IE throws unspecified error here if TinyMCE is placed in a frame/iframe
       
   524 			}
       
   525 
       
   526 			// We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
       
   527 			// IE 11 doesn't support the selection object so we check for that as well
       
   528 			if (isIE && rng && rng.setStart && doc.selection) {
       
   529 				try {
       
   530 					// IE will sometimes throw an exception here
       
   531 					ieRng = doc.selection.createRange();
       
   532 				} catch (ex) {
       
   533 
       
   534 				}
       
   535 
       
   536 				if (ieRng && ieRng.item) {
       
   537 					elm = ieRng.item(0);
       
   538 					rng = doc.createRange();
       
   539 					rng.setStartBefore(elm);
       
   540 					rng.setEndAfter(elm);
       
   541 				}
       
   542 			}
       
   543 
       
   544 			// No range found then create an empty one
       
   545 			// This can occur when the editor is placed in a hidden container element on Gecko
       
   546 			// Or on IE when there was an exception
       
   547 			if (!rng) {
       
   548 				rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
       
   549 			}
       
   550 
       
   551 			// If range is at start of document then move it to start of body
       
   552 			if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
       
   553 				elm = self.dom.getRoot();
       
   554 				rng.setStart(elm, 0);
       
   555 				rng.setEnd(elm, 0);
       
   556 			}
       
   557 
       
   558 			if (self.selectedRange && self.explicitRange) {
       
   559 				if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 &&
       
   560 					tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) {
       
   561 					// Safari, Opera and Chrome only ever select text which causes the range to change.
       
   562 					// This lets us use the originally set range if the selection hasn't been changed by the user.
       
   563 					rng = self.explicitRange;
       
   564 				} else {
       
   565 					self.selectedRange = null;
       
   566 					self.explicitRange = null;
       
   567 				}
       
   568 			}
       
   569 
       
   570 			return rng;
       
   571 		},
       
   572 
       
   573 		/**
       
   574 		 * Changes the selection to the specified DOM range.
       
   575 		 *
       
   576 		 * @method setRng
       
   577 		 * @param {Range} rng Range to select.
       
   578 		 */
       
   579 		setRng: function(rng, forward) {
       
   580 			var self = this, sel;
       
   581 
       
   582 			if (!rng) {
       
   583 				return;
       
   584 			}
       
   585 
       
   586 			// Is IE specific range
       
   587 			if (rng.select) {
       
   588 				try {
       
   589 					rng.select();
       
   590 				} catch (ex) {
       
   591 					// Needed for some odd IE bug #1843306
       
   592 				}
       
   593 
       
   594 				return;
       
   595 			}
       
   596 
       
   597 			if (!self.tridentSel) {
       
   598 				sel = self.getSel();
       
   599 
       
   600 				if (sel) {
       
   601 					self.explicitRange = rng;
       
   602 
       
   603 					try {
       
   604 						sel.removeAllRanges();
       
   605 						sel.addRange(rng);
       
   606 					} catch (ex) {
       
   607 						// IE might throw errors here if the editor is within a hidden container and selection is changed
       
   608 					}
       
   609 
       
   610 					// Forward is set to false and we have an extend function
       
   611 					if (forward === false && sel.extend) {
       
   612 						sel.collapse(rng.endContainer, rng.endOffset);
       
   613 						sel.extend(rng.startContainer, rng.startOffset);
       
   614 					}
       
   615 
       
   616 					// adding range isn't always successful so we need to check range count otherwise an exception can occur
       
   617 					self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
       
   618 				}
       
   619 			} else {
       
   620 				// Is W3C Range fake range on IE
       
   621 				if (rng.cloneRange) {
       
   622 					try {
       
   623 						self.tridentSel.addRange(rng);
       
   624 						return;
       
   625 					} catch (ex) {
       
   626 						//IE9 throws an error here if called before selection is placed in the editor
       
   627 					}
       
   628 				}
       
   629 			}
       
   630 		},
       
   631 
       
   632 		/**
       
   633 		 * Sets the current selection to the specified DOM element.
       
   634 		 *
       
   635 		 * @method setNode
       
   636 		 * @param {Element} elm Element to set as the contents of the selection.
       
   637 		 * @return {Element} Returns the element that got passed in.
       
   638 		 * @example
       
   639 		 * // Inserts a DOM node at current selection/caret location
       
   640 		 * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'}));
       
   641 		 */
       
   642 		setNode: function(elm) {
       
   643 			var self = this;
       
   644 
       
   645 			self.setContent(self.dom.getOuterHTML(elm));
       
   646 
       
   647 			return elm;
       
   648 		},
       
   649 
       
   650 		/**
       
   651 		 * Returns the currently selected element or the common ancestor element for both start and end of the selection.
       
   652 		 *
       
   653 		 * @method getNode
       
   654 		 * @return {Element} Currently selected element or common ancestor element.
       
   655 		 * @example
       
   656 		 * // Alerts the currently selected elements node name
       
   657 		 * alert(tinymce.activeEditor.selection.getNode().nodeName);
       
   658 		 */
       
   659 		getNode: function() {
       
   660 			var self = this, rng = self.getRng(), elm;
       
   661 			var startContainer = rng.startContainer, endContainer = rng.endContainer;
       
   662 			var startOffset = rng.startOffset, endOffset = rng.endOffset, root = self.dom.getRoot();
       
   663 
       
   664 			function skipEmptyTextNodes(node, forwards) {
       
   665 				var orig = node;
       
   666 
       
   667 				while (node && node.nodeType === 3 && node.length === 0) {
       
   668 					node = forwards ? node.nextSibling : node.previousSibling;
       
   669 				}
       
   670 
       
   671 				return node || orig;
       
   672 			}
       
   673 
       
   674 			// Range maybe lost after the editor is made visible again
       
   675 			if (!rng) {
       
   676 				return root;
       
   677 			}
       
   678 
       
   679 			if (rng.setStart) {
       
   680 				elm = rng.commonAncestorContainer;
       
   681 
       
   682 				// Handle selection a image or other control like element such as anchors
       
   683 				if (!rng.collapsed) {
       
   684 					if (startContainer == endContainer) {
       
   685 						if (endOffset - startOffset < 2) {
       
   686 							if (startContainer.hasChildNodes()) {
       
   687 								elm = startContainer.childNodes[startOffset];
       
   688 							}
       
   689 						}
       
   690 					}
       
   691 
       
   692 					// If the anchor node is a element instead of a text node then return this element
       
   693 					//if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
       
   694 					//	return sel.anchorNode.childNodes[sel.anchorOffset];
       
   695 
       
   696 					// Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
       
   697 					// This happens when you double click an underlined word in FireFox.
       
   698 					if (startContainer.nodeType === 3 && endContainer.nodeType === 3) {
       
   699 						if (startContainer.length === startOffset) {
       
   700 							startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
       
   701 						} else {
       
   702 							startContainer = startContainer.parentNode;
       
   703 						}
       
   704 
       
   705 						if (endOffset === 0) {
       
   706 							endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
       
   707 						} else {
       
   708 							endContainer = endContainer.parentNode;
       
   709 						}
       
   710 
       
   711 						if (startContainer && startContainer === endContainer) {
       
   712 							return startContainer;
       
   713 						}
       
   714 					}
       
   715 				}
       
   716 
       
   717 				if (elm && elm.nodeType == 3) {
       
   718 					return elm.parentNode;
       
   719 				}
       
   720 
       
   721 				return elm;
       
   722 			}
       
   723 
       
   724 			elm = rng.item ? rng.item(0) : rng.parentElement();
       
   725 
       
   726 			// IE 7 might return elements outside the iframe
       
   727 			if (elm.ownerDocument !== self.win.document) {
       
   728 				elm = root;
       
   729 			}
       
   730 
       
   731 			return elm;
       
   732 		},
       
   733 
       
   734 		getSelectedBlocks: function(startElm, endElm) {
       
   735 			var self = this, dom = self.dom, node, root, selectedBlocks = [];
       
   736 
       
   737 			root = dom.getRoot();
       
   738 			startElm = dom.getParent(startElm || self.getStart(), dom.isBlock);
       
   739 			endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock);
       
   740 
       
   741 			if (startElm && startElm != root) {
       
   742 				selectedBlocks.push(startElm);
       
   743 			}
       
   744 
       
   745 			if (startElm && endElm && startElm != endElm) {
       
   746 				node = startElm;
       
   747 
       
   748 				var walker = new TreeWalker(startElm, root);
       
   749 				while ((node = walker.next()) && node != endElm) {
       
   750 					if (dom.isBlock(node)) {
       
   751 						selectedBlocks.push(node);
       
   752 					}
       
   753 				}
       
   754 			}
       
   755 
       
   756 			if (endElm && startElm != endElm && endElm != root) {
       
   757 				selectedBlocks.push(endElm);
       
   758 			}
       
   759 
       
   760 			return selectedBlocks;
       
   761 		},
       
   762 
       
   763 		isForward: function() {
       
   764 			var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;
       
   765 
       
   766 			// No support for selection direction then always return true
       
   767 			if (!sel || !sel.anchorNode || !sel.focusNode) {
       
   768 				return true;
       
   769 			}
       
   770 
       
   771 			anchorRange = dom.createRng();
       
   772 			anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
       
   773 			anchorRange.collapse(true);
       
   774 
       
   775 			focusRange = dom.createRng();
       
   776 			focusRange.setStart(sel.focusNode, sel.focusOffset);
       
   777 			focusRange.collapse(true);
       
   778 
       
   779 			return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
       
   780 		},
       
   781 
       
   782 		normalize: function() {
       
   783 			var self = this, rng = self.getRng();
       
   784 
       
   785 			if (Env.range && new RangeUtils(self.dom).normalize(rng)) {
       
   786 				self.setRng(rng, self.isForward());
       
   787 			}
       
   788 
       
   789 			return rng;
       
   790 		},
       
   791 
       
   792 		/**
       
   793 		 * Executes callback when the current selection starts/stops matching the specified selector. The current
       
   794 		 * state will be passed to the callback as it's first argument.
       
   795 		 *
       
   796 		 * @method selectorChanged
       
   797 		 * @param {String} selector CSS selector to check for.
       
   798 		 * @param {function} callback Callback with state and args when the selector is matches or not.
       
   799 		 */
       
   800 		selectorChanged: function(selector, callback) {
       
   801 			var self = this, currentSelectors;
       
   802 
       
   803 			if (!self.selectorChangedData) {
       
   804 				self.selectorChangedData = {};
       
   805 				currentSelectors = {};
       
   806 
       
   807 				self.editor.on('NodeChange', function(e) {
       
   808 					var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
       
   809 
       
   810 					// Check for new matching selectors
       
   811 					each(self.selectorChangedData, function(callbacks, selector) {
       
   812 						each(parents, function(node) {
       
   813 							if (dom.is(node, selector)) {
       
   814 								if (!currentSelectors[selector]) {
       
   815 									// Execute callbacks
       
   816 									each(callbacks, function(callback) {
       
   817 										callback(true, {node: node, selector: selector, parents: parents});
       
   818 									});
       
   819 
       
   820 									currentSelectors[selector] = callbacks;
       
   821 								}
       
   822 
       
   823 								matchedSelectors[selector] = callbacks;
       
   824 								return false;
       
   825 							}
       
   826 						});
       
   827 					});
       
   828 
       
   829 					// Check if current selectors still match
       
   830 					each(currentSelectors, function(callbacks, selector) {
       
   831 						if (!matchedSelectors[selector]) {
       
   832 							delete currentSelectors[selector];
       
   833 
       
   834 							each(callbacks, function(callback) {
       
   835 								callback(false, {node: node, selector: selector, parents: parents});
       
   836 							});
       
   837 						}
       
   838 					});
       
   839 				});
       
   840 			}
       
   841 
       
   842 			// Add selector listeners
       
   843 			if (!self.selectorChangedData[selector]) {
       
   844 				self.selectorChangedData[selector] = [];
       
   845 			}
       
   846 
       
   847 			self.selectorChangedData[selector].push(callback);
       
   848 
       
   849 			return self;
       
   850 		},
       
   851 
       
   852 		getScrollContainer: function() {
       
   853 			var scrollContainer, node = this.dom.getRoot();
       
   854 
       
   855 			while (node && node.nodeName != 'BODY') {
       
   856 				if (node.scrollHeight > node.clientHeight) {
       
   857 					scrollContainer = node;
       
   858 					break;
       
   859 				}
       
   860 
       
   861 				node = node.parentNode;
       
   862 			}
       
   863 
       
   864 			return scrollContainer;
       
   865 		},
       
   866 
       
   867 		scrollIntoView: function(elm) {
       
   868 			var y, viewPort, self = this, dom = self.dom, root = dom.getRoot(), viewPortY, viewPortH;
       
   869 
       
   870 			function getPos(elm) {
       
   871 				var x = 0, y = 0;
       
   872 
       
   873 				var offsetParent = elm;
       
   874 				while (offsetParent && offsetParent.nodeType) {
       
   875 					x += offsetParent.offsetLeft || 0;
       
   876 					y += offsetParent.offsetTop || 0;
       
   877 					offsetParent = offsetParent.offsetParent;
       
   878 				}
       
   879 
       
   880 				return {x: x, y: y};
       
   881 			}
       
   882 
       
   883 			if (root.nodeName != 'BODY') {
       
   884 				var scrollContainer = self.getScrollContainer();
       
   885 				if (scrollContainer) {
       
   886 					y = getPos(elm).y - getPos(scrollContainer).y;
       
   887 					viewPortH = scrollContainer.clientHeight;
       
   888 					viewPortY = scrollContainer.scrollTop;
       
   889 					if (y < viewPortY || y + 25 > viewPortY + viewPortH) {
       
   890 						scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25;
       
   891 					}
       
   892 
       
   893 					return;
       
   894 				}
       
   895 			}
       
   896 
       
   897 			viewPort = dom.getViewPort(self.editor.getWin());
       
   898 			y = dom.getPos(elm).y;
       
   899 			viewPortY = viewPort.y;
       
   900 			viewPortH = viewPort.h;
       
   901 			if (y < viewPort.y || y + 25 > viewPortY + viewPortH) {
       
   902 				self.editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25);
       
   903 			}
       
   904 		},
       
   905 
       
   906 		placeCaretAt: function(clientX, clientY) {
       
   907 			var doc = this.editor.getDoc(), rng, point;
       
   908 
       
   909 			if (doc.caretPositionFromPoint) {
       
   910 				point = doc.caretPositionFromPoint(clientX, clientY);
       
   911 				rng = doc.createRange();
       
   912 				rng.setStart(point.offsetNode, point.offset);
       
   913 				rng.collapse(true);
       
   914 			} else if (doc.caretRangeFromPoint) {
       
   915 				rng = doc.caretRangeFromPoint(clientX, clientY);
       
   916 			} else if (doc.body.createTextRange) {
       
   917 				rng = doc.body.createTextRange();
       
   918 
       
   919 				try {
       
   920 					rng.moveToPoint(clientX, clientY);
       
   921 					rng.collapse(true);
       
   922 				} catch (ex) {
       
   923 					rng.collapse(clientY < doc.body.clientHeight);
       
   924 				}
       
   925 			}
       
   926 
       
   927 			this.setRng(rng);
       
   928 		},
       
   929 
       
   930 		_moveEndPoint: function(rng, node, start) {
       
   931 			var root = node, walker = new TreeWalker(node, root);
       
   932 			var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements();
       
   933 
       
   934 			do {
       
   935 				// Text node
       
   936 				if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) {
       
   937 					if (start) {
       
   938 						rng.setStart(node, 0);
       
   939 					} else {
       
   940 						rng.setEnd(node, node.nodeValue.length);
       
   941 					}
       
   942 
       
   943 					return;
       
   944 				}
       
   945 
       
   946 				// BR/IMG/INPUT elements but not table cells
       
   947 				if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) {
       
   948 					if (start) {
       
   949 						rng.setStartBefore(node);
       
   950 					} else {
       
   951 						if (node.nodeName == 'BR') {
       
   952 							rng.setEndBefore(node);
       
   953 						} else {
       
   954 							rng.setEndAfter(node);
       
   955 						}
       
   956 					}
       
   957 
       
   958 					return;
       
   959 				}
       
   960 
       
   961 				// Found empty text block old IE can place the selection inside those
       
   962 				if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) {
       
   963 					if (start) {
       
   964 						rng.setStart(node, 0);
       
   965 					} else {
       
   966 						rng.setEnd(node, 0);
       
   967 					}
       
   968 
       
   969 					return;
       
   970 				}
       
   971 			} while ((node = (start ? walker.next() : walker.prev())));
       
   972 
       
   973 			// Failed to find any text node or other suitable location then move to the root of body
       
   974 			if (root.nodeName == 'BODY') {
       
   975 				if (start) {
       
   976 					rng.setStart(root, 0);
       
   977 				} else {
       
   978 					rng.setEnd(root, root.childNodes.length);
       
   979 				}
       
   980 			}
       
   981 		},
       
   982 
       
   983 		destroy: function() {
       
   984 			this.win = null;
       
   985 			this.controlSelection.destroy();
       
   986 		}
       
   987 	};
       
   988 
       
   989 	return Selection;
       
   990 });