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