1 /*!
  2  * This file is part of Aloha Editor
  3  * Author & Copyright (c) 2010 Gentics Software GmbH, aloha@gentics.com
  4  * Licensed unter the terms of http://www.aloha-editor.com/license.html
  5  */
  6 (function(window, undefined) {
  7 	"use strict";
  8 	var
  9 		jQuery = window.alohaQuery || window.jQuery, $ = jQuery,
 10 //		GENTICS = window.GENTICS,
 11 //		Aloha = window.Aloha,
 12 		DOMUtils, TextRangeUtils, selection, DOMRange, RangeIterator, DOMSelection;
 13 
 14 /*
 15  * Only execute the following code if we are in IE (check for
 16  * document.attachEvent, this is a microsoft event and therefore only available
 17  * in IE).
 18  */
 19 
 20 if(document.attachEvent && document.selection) {
 21 /*!
 22 *   DOM Ranges for Internet Explorer (m2)
 23 *
 24 *   Copyright (c) 2009 Tim Cameron Ryan
 25 *   Released under the MIT/X License
 26 *   available at http://code.google.com/p/ierange/
 27 */
 28 
 29 	/*
 30 	  Range reference:
 31 	    http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
 32 	    http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsRange.cpp
 33 	    https://developer.mozilla.org/En/DOM:Range
 34 	  Selection reference:
 35 	    http://trac.webkit.org/browser/trunk/WebCore/page/DOMSelection.cpp
 36 	  TextRange reference:
 37 	    http://msdn.microsoft.com/en-us/library/ms535872.aspx
 38 	  Other links:
 39 	    http://jorgenhorstink.nl/test/javascript/range/range.js
 40 	    http://jorgenhorstink.nl/2006/07/05/dom-range-implementation-in-ecmascript-completed/
 41 	    http://dylanschiemann.com/articles/dom2Range/dom2RangeExamples.html
 42 	*/
 43 
 44 	//[TODO] better exception support
 45 
 46 
 47 	/*
 48 	  DOM functions
 49 	 */
 50 
 51 	DOMUtils = {
 52 		findChildPosition: function (node) {
 53 			for (var i = 0; node = node.previousSibling; i++)
 54 				continue;
 55 			return i;
 56 		},
 57 		isDataNode: function (node) {
 58 			return node && node.nodeValue !== null && node.data !== null;
 59 		},
 60 		isAncestorOf: function (parent, node) {
 61 			return !DOMUtils.isDataNode(parent) &&
 62 			    (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) ||
 63 			    node.parentNode == parent);
 64 		},
 65 		isAncestorOrSelf: function (root, node) {
 66 			return DOMUtils.isAncestorOf(root, node) || root == node;
 67 		},
 68 		findClosestAncestor: function (root, node) {
 69 			if (DOMUtils.isAncestorOf(root, node))
 70 				while (node && node.parentNode != root)
 71 					node = node.parentNode;
 72 			return node;
 73 		},
 74 		getNodeLength: function (node) {
 75 			return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length;
 76 		},
 77 		splitDataNode: function (node, offset) {
 78 			if (!DOMUtils.isDataNode(node))
 79 				return false;
 80 			var newNode = node.cloneNode(false);
 81 			node.deleteData(offset, node.length);
 82 			newNode.deleteData(0, offset);
 83 			node.parentNode.insertBefore(newNode, node.nextSibling);
 84 		}
 85 	};
 86 
 87 	/*
 88 	  Text Range utilities
 89 	  functions to simplify text range manipulation in ie
 90 	 */
 91 
 92 	TextRangeUtils = {
 93 		convertToDOMRange: function (textRange, document) {
 94 			var domRange,adoptBoundary;
 95 
 96 			adoptBoundary = function(domRange, textRange, bStart) {
 97 				// iterate backwards through parent element to find anchor location
 98 				var cursorNode = document.createElement('a'),
 99 					cursor = textRange.duplicate(),
100 					parent;
101 			
102 				cursor.collapse(bStart);
103 				parent = cursor.parentElement();
104 				do {
105 					parent.insertBefore(cursorNode, cursorNode.previousSibling);
106 					cursor.moveToElementText(cursorNode);
107 				} while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling);
108 
109 				// when we exceed or meet the cursor, we've found the node
110 				if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) {
111 					// data node
112 					cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange);
113 					domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length);
114 				} else {
115 					// element
116 					domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode);
117 				}
118 				cursorNode.parentNode.removeChild(cursorNode);
119 			};
120 
121 			// return a DOM range
122 			domRange = new DOMRange(document);
123 			adoptBoundary(domRange, textRange, true);
124 			adoptBoundary(domRange, textRange, false);
125 			return domRange;
126 		},
127 
128 		convertFromDOMRange: function (domRange) {
129 			function adoptEndPoint(textRange, domRange, bStart) {
130 				// find anchor node and offset
131 				var container = domRange[bStart ? 'startContainer' : 'endContainer'],
132 					offset = domRange[bStart ? 'startOffset' : 'endOffset'], textOffset = 0,
133 					anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset],
134 					anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container,
135 					cursorNode, cursor;
136 				
137 				// visible data nodes need a text offset
138 				if (container.nodeType == 3 || container.nodeType == 4) {
139 					textOffset = offset;
140 				}
141 
142 				// create a cursor element node to position range (since we can't select text nodes)
143 				cursorNode = domRange._document.createElement('a');
144 				anchorParent.insertBefore(cursorNode, anchorNode);
145 				cursor = domRange._document.body.createTextRange();
146 				cursor.moveToElementText(cursorNode);
147 				cursorNode.parentNode.removeChild(cursorNode);
148 				// move range
149 				textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor);
150 				textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset);
151 			}
152 
153 			// return an IE text range
154 			var textRange = domRange._document.body.createTextRange();
155 			adoptEndPoint(textRange, domRange, true);
156 			adoptEndPoint(textRange, domRange, false);
157 			return textRange;
158 		}
159 	};
160 
161 	/*
162 	  DOM Range
163 	 */
164 	DOMRange = function(document) {
165 		// save document parameter
166 		this._document = document;
167 
168 		// initialize range
169 		//[TODO] this should be located at document[0], document[0]
170 		this.startContainer = this.endContainer = document.body;
171 		this.endOffset = DOMUtils.getNodeLength(document.body);
172 	};
173 
174 	DOMRange.START_TO_START = 0;
175 	DOMRange.START_TO_END = 1;
176 	DOMRange.END_TO_END = 2;
177 	DOMRange.END_TO_START = 3;
178 
179 	DOMRange.prototype = {
180 		// public properties
181 		startContainer: null,
182 		startOffset: 0,
183 		endContainer: null,
184 		endOffset: 0,
185 		commonAncestorContainer: null,
186 		collapsed: false,
187 		// private properties
188 189 		_document: null,
190 
191 		// private methods
192 		_refreshProperties: function () {
193 			// collapsed attribute
194 			this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset);
195 			// find common ancestor
196 			var node = this.startContainer;
197 			while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer))
198 				node = node.parentNode;
199 			this.commonAncestorContainer = node;
200 		},
201 
202 		// range methods
203 	//[TODO] collapse if start is after end, end is before start
204 		setStart: function(container, offset) {
205 			this.startContainer = container;
206 			this.startOffset = offset;
207 			this._refreshProperties();
208 		},
209 		setEnd: function(container, offset) {
210 			this.endContainer = container;
211 			this.endOffset = offset;
212 			this._refreshProperties();
213 		},
214 		setStartBefore: function (refNode) {
215 			// set start to beore this node
216 			this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode));
217 		},
218 		setStartAfter: function (refNode) {
219 			// select next sibling
220 			this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
221 		},
222 		setEndBefore: function (refNode) {
223 			// set end to beore this node
224 			this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode));
225 		},
226 		setEndAfter: function (refNode) {
227 			// select next sibling
228 			this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
229 		},
230 		selectNode: function (refNode) {
231 			this.setStartBefore(refNode);
232 			this.setEndAfter(refNode);
233 		},
234 		selectNodeContents: function (refNode) {
235 			this.setStart(refNode, 0);
236 			this.setEnd(refNode, DOMUtils.getNodeLength(refNode));
237 		},
238 		collapse: function (toStart) {
239 			if (toStart)
240 				this.setEnd(this.startContainer, this.startOffset);
241 			else
242 				this.setStart(this.endContainer, this.endOffset);
243 		},
244 
245 		// editing methods
246 		cloneContents: function () {
247 			// clone subtree
248 			return (function cloneSubtree(iterator) {
249 				for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
250 					node = node.cloneNode(!iterator.hasPartialSubtree());
251 					if (iterator.hasPartialSubtree())
252 						node.appendChild(cloneSubtree(iterator.getSubtreeIterator()));
253 					frag.appendChild(node);
254 				}
255 				return frag;
256 			})(new RangeIterator(this));
257 		},
258 		extractContents: function () {
259 			// cache range and move anchor points
260 			var range = this.cloneRange();
261 			if (this.startContainer != this.commonAncestorContainer)
262 				this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
263 			this.collapse(true);
264 			// extract range
265 			return (function extractSubtree(iterator) {
266 				for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
267 					if ( iterator.hasPartialSubtree() ) {
268 						node = node.cloneNode(false);
269 					}
270 					else {
271 						iterator.remove();
272 					}
273 					if (iterator.hasPartialSubtree())
274 						node.appendChild(extractSubtree(iterator.getSubtreeIterator()));
275 					frag.appendChild(node);
276 				}
277 				return frag;
278 			})(new RangeIterator(range));
279 		},
280 		deleteContents: function () {
281 			// cache range and move anchor points
282 			var range = this.cloneRange();
283 			if (this.startContainer != this.commonAncestorContainer)
284 				this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
285 			this.collapse(true);
286 			// delete range
287 			(function deleteSubtree(iterator) {
288 				while (iterator.next()) {
289 					if ( iterator.hasPartialSubtree() ) {
290 						deleteSubtree(iterator.getSubtreeIterator());
291 					}
292 					else {
293 						iterator.remove();
294 					}
295 				}
296 			})(new RangeIterator(range));
297 		},
298 		insertNode: function (newNode) {
299 			// set original anchor and insert node
300 			if (DOMUtils.isDataNode(this.startContainer)) {
301 				DOMUtils.splitDataNode(this.startContainer, this.startOffset);
302 				this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling);
303 			} else {
304 				this.startContainer.insertBefore(newNode, this.startContainer.childNodes[this.startOffset]);
305 			}
306 			// resync start anchor
307 			this.setStart(this.startContainer, this.startOffset);
308 		},
309 		surroundContents: function (newNode) {
310 			// extract and surround contents
311 			var content = this.extractContents();
312 			this.insertNode(newNode);
313 			newNode.appendChild(content);
314 			this.selectNode(newNode);
315 		},
316 
317 		// other methods
318 		compareBoundaryPoints: function (how, sourceRange) {
319 			// get anchors
320 			var containerA, offsetA, containerB, offsetB;
321 			switch (how) {
322 			    case DOMRange.START_TO_START:
323 			    case DOMRange.START_TO_END:
324 				containerA = this.startContainer;
325 				offsetA = this.startOffset;
326 				break;
327 			    case DOMRange.END_TO_END:
328 			    case DOMRange.END_TO_START:
329 				containerA = this.endContainer;
330 				offsetA = this.endOffset;
331 				break;
332 			}
333 			switch (how) {
334 			    case DOMRange.START_TO_START:
335 			    case DOMRange.END_TO_START:
336 				containerB = sourceRange.startContainer;
337 				offsetB = sourceRange.startOffset;
338 				break;
339 			    case DOMRange.START_TO_END:
340 			    case DOMRange.END_TO_END:
341 				containerB = sourceRange.endContainer;
342 				offsetB = sourceRange.endOffset;
343 				break;
344 			}
345 
346 			// compare
347 			return containerA.sourceIndex < containerB.sourceIndex ? -1 :
348 			    containerA.sourceIndex == containerB.sourceIndex ?
349 			        offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1
350 			        : 1;
351 		},
352 		cloneRange: function () {
353 			// return cloned range
354 			var range = new DOMRange(this._document);
355 			range.setStart(this.startContainer, this.startOffset);
356 			range.setEnd(this.endContainer, this.endOffset);
357 			return range;
358 		},
359 		detach: function () {
360 	//[TODO] Releases Range from use to improve performance.
361 		},
362 		toString: function () {
363 			return TextRangeUtils.convertFromDOMRange(this).text;
364 		},
365 		createContextualFragment: function (tagString) {
366 			// parse the tag string in a context node
367 			var
368 				content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false),
369 				fragment;
370 			
371 			content.innerHTML = tagString;
372 			// return a document fragment from the created node
373 			for (fragment = this._document.createDocumentFragment(); content.firstChild; )
374 				fragment.appendChild(content.firstChild);
375 			return fragment;
376 		}
377 	};
378 
379 	/*
380 	  Range iterator
381 	 */
382 	RangeIterator = function(range) {
383 384 		this.range = range;
385 		if (range.collapsed) {
386 			return;
387 		}
388 
389 		//[TODO] ensure this works
390 		// get anchors
391 		var root = range.commonAncestorContainer;
392 		this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ?
393 		range.startContainer.childNodes[range.startOffset] :
394 		DOMUtils.findClosestAncestor(root, range.startContainer);
395 		this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ?
396 		range.endContainer.childNodes[range.endOffset] :
397 		DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling;
398 	};
399 
400 	RangeIterator.prototype = {
401 		// public properties
402 		range: null,
403 		// private properties
404 		_current: null,
405 406 		_next: null,
407 		_end: null,
408 
409 		// public methods
410 		hasNext: function () {
411 			return !!this._next;
412 		},
413 		next: function () {
414 			// move to next node
415 			var current = this._current = this._next;
416 			this._next = this._current && this._current.nextSibling != this._end ?
417 			    this._current.nextSibling : null;
418 
419 			// check for partial text nodes
420 			if (DOMUtils.isDataNode(this._current)) {
421 				if (this.range.endContainer == this._current)
422 					(current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset);
423 				if (this.range.startContainer == this._current)
424 					(current = current.cloneNode(true)).deleteData(0, this.range.startOffset);
425 			}
426 			return current;
427 		},
428 		remove: function () {
429 			var end, start;
430 			// check for partial text nodes
431 			if (DOMUtils.isDataNode(this._current) &&
432 			    (this.range.startContainer == this._current || this.range.endContainer == this._current)) {
433 				start = this.range.startContainer == this._current ? this.range.startOffset : 0;
434 				end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length;
435 				this._current.deleteData(start, end - start);
436 			} else
437 				this._current.parentNode.removeChild(this._current);
438 		},
439 		hasPartialSubtree: function () {
440 			// check if this node be partially selected
441 			return !DOMUtils.isDataNode(this._current) &&
442 			    (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) ||
443 			        DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer));
444 		},
445 		getSubtreeIterator: function () {
446 			// create a new range
447 			var subRange = new DOMRange(this.range._document);
448 			subRange.selectNodeContents(this._current);
449 			// handle anchor points
450 			if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer))
451 				subRange.setStart(this.range.startContainer, this.range.startOffset);
452 			if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer))
453 				subRange.setEnd(this.range.endContainer, this.range.endOffset);
454 			// return iterator
455 			return new RangeIterator(subRange);
456 		}
457 	};
458 
459 	/*
460 	  DOM Selection
461 	 */
462 
463 	//[NOTE] This is a very shallow implementation of the Selection object, based on Webkit's
464 	// implementation and without redundant features. Complete selection manipulation is still
465 	// possible with just removeAllRanges/addRange/getRangeAt.
466 
467 	DOMSelection = function (document) {
468 		// save document parameter
469 		this._document = document;
470 
471 		// add DOM selection handler
472 		var selection = this;
473 		document.attachEvent('onselectionchange', function () { selection._selectionChangeHandler(); });
474 	};
475 
476 	DOMSelection.prototype = {
477 		// public properties
478 		rangeCount: 0,
479 		// private properties
480 		_document: null,
481 
482 		// private methods
483 		_selectionChangeHandler: function () {
484 			// check if there exists a range
485 			this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0;
486 		},
487 		_selectionExists: function (textRange) {
488 			// checks if a created text range exists or is an editable cursor
489 			return textRange.compareEndPoints('StartToEnd', textRange) !== 0 ||
490 			    textRange.parentElement().isContentEditable;
491 		},
492 
493 		// public methods
494 		addRange: function (range) {
495 			// add range or combine with existing range
496 			var selection = this._document.selection.createRange(), textRange = TextRangeUtils.convertFromDOMRange(range);
497 			if (!this._selectionExists(selection))
498 			{
499 				// select range
500 				textRange.select();
501 			}
502 			else
503 			{
504 				// only modify range if it intersects with current range
505 				if (textRange.compareEndPoints('StartToStart', selection) == -1)
506 					if (textRange.compareEndPoints('StartToEnd', selection) > -1 &&
507 					    textRange.compareEndPoints('EndToEnd', selection) == -1)
508 						selection.setEndPoint('StartToStart', textRange);
509 				else
510 					if (textRange.compareEndPoints('EndToStart', selection) < 1 &&
511 					    textRange.compareEndPoints('EndToEnd', selection) > -1)
512 						selection.setEndPoint('EndToEnd', textRange);
513 				selection.select();
514 			}
515 		},
516 		removeAllRanges: function () {
517 			// remove all ranges
518 			this._document.selection.empty();
519 520 		},
521 		getRangeAt: function (index) {
522 			// return any existing selection, or a cursor position in content editable mode
523 			var textRange = this._document.selection.createRange();
524 			if (this._selectionExists(textRange))
525 				return TextRangeUtils.convertToDOMRange(textRange, this._document);
526 			return null;
527 		},
528 		toString: function () {
529 			// get selection text
530 			return this._document.selection.createRange().text;
531 		}
532 	};
533 
534 	/*
535 	  scripting hooks
536 	 */
537 
538 	document.createRange = function () {
539 		return new DOMRange(document);
540 	};
541 
542 	selection = new DOMSelection(document);
543 		window.getSelection = function () {
544 		return selection;
545 	};
546 
547 	//[TODO] expose DOMRange/DOMSelection to window.?
548 }
549 
550 })(window);