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