01fe9960154a56a0f4262b3623342e7ace0270a4
[redakcja.git] / src / redakcja / static / js / lib / codemirror-0.8 / select.js
1 /* Functionality for finding, storing, and restoring selections
2  *
3  * This does not provide a generic API, just the minimal functionality
4  * required by the CodeMirror system.
5  */
6
7 // Namespace object.
8 var select = {};
9
10 (function() {
11   select.ie_selection = document.selection && document.selection.createRangeCollection;
12
13   // Find the 'top-level' (defined as 'a direct child of the node
14   // passed as the top argument') node that the given node is
15   // contained in. Return null if the given node is not inside the top
16   // node.
17   function topLevelNodeAt(node, top) {
18     while (node && node.parentNode != top)
19       node = node.parentNode;
20     return node;
21   }
22
23   // Find the top-level node that contains the node before this one.
24   function topLevelNodeBefore(node, top) {
25     while (!node.previousSibling && node.parentNode != top)
26       node = node.parentNode;
27     return topLevelNodeAt(node.previousSibling, top);
28   }
29
30   var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
31
32   select.scrollToNode = function(node, cursor) {
33     if (!node) return;
34     var element = node,
35         doc = element.ownerDocument, body = doc.body,
36         win = (doc.defaultView || doc.parentWindow),
37         html = doc.documentElement,
38         atEnd = !element.nextSibling || !element.nextSibling.nextSibling
39                 || !element.nextSibling.nextSibling.nextSibling;
40     // In Opera (and recent Webkit versions), BR elements *always*
41     // have a offsetTop property of zero.
42     var compensateHack = 0;
43     while (element && !element.offsetTop) {
44       compensateHack++;
45       element = element.previousSibling;
46     }
47     // atEnd is another kludge for these browsers -- if the cursor is
48     // at the end of the document, and the node doesn't have an
49     // offset, just scroll to the end.
50     if (compensateHack == 0) atEnd = false;
51
52     // WebKit has a bad habit of (sometimes) happily returning bogus
53     // offsets when the document has just been changed. This seems to
54     // always be 5/5, so we don't use those.
55     if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5)
56       return;
57
58     var y = compensateHack * (element ? element.offsetHeight : 0), x = 0,
59         width = (node ? node.offsetWidth : 0), pos = element;
60     while (pos && pos.offsetParent) {
61       y += pos.offsetTop;
62       // Don't count X offset for <br> nodes
63       if (!isBR(pos))
64         x += pos.offsetLeft;
65       pos = pos.offsetParent;
66     }
67
68     var scroll_x = body.scrollLeft || html.scrollLeft || 0,
69         scroll_y = body.scrollTop || html.scrollTop || 0,
70         scroll = false, screen_width = win.innerWidth || html.clientWidth || 0;
71
72     if (cursor || width < screen_width) {
73       if (cursor) {
74         var off = select.offsetInNode(win, node), size = nodeText(node).length;
75         if (size) x += width * (off / size);
76       }
77       var screen_x = x - scroll_x;
78       if (screen_x < 0 || screen_x > screen_width) {
79         scroll_x = x;
80         scroll = true;
81       }
82     }
83     var screen_y = y - scroll_y;
84     if (screen_y < 0 || atEnd || screen_y > (win.innerHeight || html.clientHeight || 0) - 50) {
85       scroll_y = atEnd ? 1e6 : y;
86       scroll = true;
87     }
88     if (scroll) win.scrollTo(scroll_x, scroll_y);
89   };
90
91   select.scrollToCursor = function(container) {
92     select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true);
93   };
94
95   // Used to prevent restoring a selection when we do not need to.
96   var currentSelection = null;
97
98   select.snapshotChanged = function() {
99     if (currentSelection) currentSelection.changed = true;
100   };
101
102   // This is called by the code in editor.js whenever it is replacing
103   // a text node. The function sees whether the given oldNode is part
104   // of the current selection, and updates this selection if it is.
105   // Because nodes are often only partially replaced, the length of
106   // the part that gets replaced has to be taken into account -- the
107   // selection might stay in the oldNode if the newNode is smaller
108   // than the selection's offset. The offset argument is needed in
109   // case the selection does move to the new object, and the given
110   // length is not the whole length of the new node (part of it might
111   // have been used to replace another node).
112   select.snapshotReplaceNode = function(from, to, length, offset) {
113     if (!currentSelection) return;
114
115     function replace(point) {
116       if (from == point.node) {
117         currentSelection.changed = true;
118         if (length && point.offset > length) {
119           point.offset -= length;
120         }
121         else {
122           point.node = to;
123           point.offset += (offset || 0);
124         }
125       }
126     }
127     replace(currentSelection.start);
128     replace(currentSelection.end);
129   };
130
131   select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
132     if (!currentSelection) return;
133
134     function move(point) {
135       if (from == point.node && (!ifAtStart || point.offset == 0)) {
136         currentSelection.changed = true;
137         point.node = to;
138         if (relative) point.offset = Math.max(0, point.offset + distance);
139         else point.offset = distance;
140       }
141     }
142     move(currentSelection.start);
143     move(currentSelection.end);
144   };
145
146   // Most functions are defined in two ways, one for the IE selection
147   // model, one for the W3C one.
148   if (select.ie_selection) {
149     function selectionNode(win, start) {
150       var range = win.document.selection.createRange();
151       range.collapse(start);
152
153       function nodeAfter(node) {
154         var found = null;
155         while (!found && node) {
156           found = node.nextSibling;
157           node = node.parentNode;
158         }
159         return nodeAtStartOf(found);
160       }
161
162       function nodeAtStartOf(node) {
163         while (node && node.firstChild) node = node.firstChild;
164         return {node: node, offset: 0};
165       }
166
167       var containing = range.parentElement();
168       if (!isAncestor(win.document.body, containing)) return null;
169       if (!containing.firstChild) return nodeAtStartOf(containing);
170
171       var working = range.duplicate();
172       working.moveToElementText(containing);
173       working.collapse(true);
174       for (var cur = containing.firstChild; cur; cur = cur.nextSibling) {
175         if (cur.nodeType == 3) {
176           var size = cur.nodeValue.length;
177           working.move("character", size);
178         }
179         else {
180           working.moveToElementText(cur);
181           working.collapse(false);
182         }
183
184         var dir = range.compareEndPoints("StartToStart", working);
185         if (dir == 0) return nodeAfter(cur);
186         if (dir == 1) continue;
187         if (cur.nodeType != 3) return nodeAtStartOf(cur);
188
189         working.setEndPoint("StartToEnd", range);
190         return {node: cur, offset: size - working.text.length};
191       }
192       return nodeAfter(containing);
193     }
194
195     select.markSelection = function(win) {
196       currentSelection = null;
197       var sel = win.document.selection;
198       if (!sel) return;
199       var start = selectionNode(win, true),
200           end = selectionNode(win, false);
201       if (!start || !end) return;
202       currentSelection = {start: start, end: end, window: win, changed: false};
203     };
204
205     select.selectMarked = function() {
206       if (!currentSelection || !currentSelection.changed) return;
207       var win = currentSelection.window, doc = win.document;
208
209       function makeRange(point) {
210         var range = doc.body.createTextRange(),
211             node = point.node;
212         if (!node) {
213           range.moveToElementText(currentSelection.window.document.body);
214           range.collapse(false);
215         }
216         else if (node.nodeType == 3) {
217           range.moveToElementText(node.parentNode);
218           var offset = point.offset;
219           while (node.previousSibling) {
220             node = node.previousSibling;
221             offset += (node.innerText || "").length;
222           }
223           range.move("character", offset);
224         }
225         else {
226           range.moveToElementText(node);
227           range.collapse(true);
228         }
229         return range;
230       }
231
232       var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
233       start.setEndPoint("StartToEnd", end);
234       start.select();
235     };
236
237     select.offsetInNode = function(win, node) {
238       var sel = win.document.selection;
239       if (!sel) return 0;
240       var range = sel.createRange(), range2 = range.duplicate();
241       try {range2.moveToElementText(node);} catch(e){return 0;}
242       range.setEndPoint("StartToStart", range2);
243       return range.text.length;
244     };
245
246     // Get the top-level node that one end of the cursor is inside or
247     // after. Note that this returns false for 'no cursor', and null
248     // for 'start of document'.
249     select.selectionTopNode = function(container, start) {
250       var selection = container.ownerDocument.selection;
251       if (!selection) return false;
252
253       var range = selection.createRange(), range2 = range.duplicate();
254       range.collapse(start);
255       var around = range.parentElement();
256       if (around && isAncestor(container, around)) {
257         // Only use this node if the selection is not at its start.
258         range2.moveToElementText(around);
259         if (range.compareEndPoints("StartToStart", range2) == 1)
260           return topLevelNodeAt(around, container);
261       }
262
263       // Move the start of a range to the start of a node,
264       // compensating for the fact that you can't call
265       // moveToElementText with text nodes.
266       function moveToNodeStart(range, node) {
267         if (node.nodeType == 3) {
268           var count = 0, cur = node.previousSibling;
269           while (cur && cur.nodeType == 3) {
270             count += cur.nodeValue.length;
271             cur = cur.previousSibling;
272           }
273           if (cur) {
274             try{range.moveToElementText(cur);}
275             catch(e){return false;}
276             range.collapse(false);
277           }
278           else range.moveToElementText(node.parentNode);
279           if (count) range.move("character", count);
280         }
281         else {
282           try{range.moveToElementText(node);}
283           catch(e){return false;}
284         }
285         return true;
286       }
287
288       // Do a binary search through the container object, comparing
289       // the start of each node to the selection
290       var start = 0, end = container.childNodes.length - 1;
291       while (start < end) {
292         var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle];
293         if (!node) return false; // Don't ask. IE6 manages this sometimes.
294         if (!moveToNodeStart(range2, node)) return false;
295         if (range.compareEndPoints("StartToStart", range2) == 1)
296           start = middle;
297         else
298           end = middle - 1;
299       }
300       return container.childNodes[start] || null;
301     };
302
303     // Place the cursor after this.start. This is only useful when
304     // manually moving the cursor instead of restoring it to its old
305     // position.
306     select.focusAfterNode = function(node, container) {
307       var range = container.ownerDocument.body.createTextRange();
308       range.moveToElementText(node || container);
309       range.collapse(!node);
310       range.select();
311     };
312
313     select.somethingSelected = function(win) {
314       var sel = win.document.selection;
315       return sel && (sel.createRange().text != "");
316     };
317
318     function insertAtCursor(window, html) {
319       var selection = window.document.selection;
320       if (selection) {
321         var range = selection.createRange();
322         range.pasteHTML(html);
323         range.collapse(false);
324         range.select();
325       }
326     }
327
328     // Used to normalize the effect of the enter key, since browsers
329     // do widely different things when pressing enter in designMode.
330     select.insertNewlineAtCursor = function(window) {
331       insertAtCursor(window, "<br>");
332     };
333
334     select.insertTabAtCursor = function(window) {
335       insertAtCursor(window, fourSpaces);
336     };
337
338     // Get the BR node at the start of the line on which the cursor
339     // currently is, and the offset into the line. Returns null as
340     // node if cursor is on first line.
341     select.cursorPos = function(container, start) {
342       var selection = container.ownerDocument.selection;
343       if (!selection) return null;
344
345       var topNode = select.selectionTopNode(container, start);
346       while (topNode && !isBR(topNode))
347         topNode = topNode.previousSibling;
348
349       var range = selection.createRange(), range2 = range.duplicate();
350       range.collapse(start);
351       if (topNode) {
352         range2.moveToElementText(topNode);
353         range2.collapse(false);
354       }
355       else {
356         // When nothing is selected, we can get all kinds of funky errors here.
357         try { range2.moveToElementText(container); }
358         catch (e) { return null; }
359         range2.collapse(true);
360       }
361       range.setEndPoint("StartToStart", range2);
362
363       return {node: topNode, offset: range.text.length};
364     };
365
366     select.setCursorPos = function(container, from, to) {
367       function rangeAt(pos) {
368         var range = container.ownerDocument.body.createTextRange();
369         if (!pos.node) {
370           range.moveToElementText(container);
371           range.collapse(true);
372         }
373         else {
374           range.moveToElementText(pos.node);
375           range.collapse(false);
376         }
377         range.move("character", pos.offset);
378         return range;
379       }
380
381       var range = rangeAt(from);
382       if (to && to != from)
383         range.setEndPoint("EndToEnd", rangeAt(to));
384       range.select();
385     }
386
387     // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
388     select.getBookmark = function (container) {
389       var from = select.cursorPos(container, true), to = select.cursorPos(container, false);
390       if (from && to) return {from: from, to: to};
391     };
392
393     // Restore a stored selection.
394     select.setBookmark = function(container, mark) {
395       if (!mark) return;
396       select.setCursorPos(container, mark.from, mark.to);
397     };
398   }
399   // W3C model
400   else {
401     // Find the node right at the cursor, not one of its
402     // ancestors with a suitable offset. This goes down the DOM tree
403     // until a 'leaf' is reached (or is it *up* the DOM tree?).
404     function innerNode(node, offset) {
405       while (node.nodeType != 3 && !isBR(node)) {
406         var newNode = node.childNodes[offset] || node.nextSibling;
407         offset = 0;
408         while (!newNode && node.parentNode) {
409           node = node.parentNode;
410           newNode = node.nextSibling;
411         }
412         node = newNode;
413         if (!newNode) break;
414       }
415       return {node: node, offset: offset};
416     }
417
418     // Store start and end nodes, and offsets within these, and refer
419     // back to the selection object from those nodes, so that this
420     // object can be updated when the nodes are replaced before the
421     // selection is restored.
422     select.markSelection = function (win) {
423       var selection = win.getSelection();
424       if (!selection || selection.rangeCount == 0)
425         return (currentSelection = null);
426       var range = selection.getRangeAt(0);
427
428       currentSelection = {
429         start: innerNode(range.startContainer, range.startOffset),
430         end: innerNode(range.endContainer, range.endOffset),
431         window: win,
432         changed: false
433       };
434     };
435
436     select.selectMarked = function () {
437       var cs = currentSelection;
438       // on webkit-based browsers, it is apparently possible that the
439       // selection gets reset even when a node that is not one of the
440       // endpoints get messed with. the most common situation where
441       // this occurs is when a selection is deleted or overwitten. we
442       // check for that here.
443       function focusIssue() {
444         if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) {
445           var selection = cs.window.getSelection();
446           if (!selection || selection.rangeCount == 0) return true;
447           var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset);
448           return cs.start.node != point.node || cs.start.offset != point.offset;
449         }
450       }
451       if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
452       var win = cs.window, range = win.document.createRange();
453
454       function setPoint(point, which) {
455         if (point.node) {
456           // Some magic to generalize the setting of the start and end
457           // of a range.
458           if (point.offset == 0)
459             range["set" + which + "Before"](point.node);
460           else
461             range["set" + which](point.node, point.offset);
462         }
463         else {
464           range.setStartAfter(win.document.body.lastChild || win.document.body);
465         }
466       }
467
468       setPoint(cs.end, "End");
469       setPoint(cs.start, "Start");
470       selectRange(range, win);
471     };
472
473     // Helper for selecting a range object.
474     function selectRange(range, window) {
475       var selection = window.getSelection();
476       if (!selection) return;
477       selection.removeAllRanges();
478       selection.addRange(range);
479     }
480     function selectionRange(window) {
481       var selection = window.getSelection();
482       if (!selection || selection.rangeCount == 0)
483         return false;
484       else
485         return selection.getRangeAt(0);
486     }
487
488     // Finding the top-level node at the cursor in the W3C is, as you
489     // can see, quite an involved process.
490     select.selectionTopNode = function(container, start) {
491       var range = selectionRange(container.ownerDocument.defaultView);
492       if (!range) return false;
493
494       var node = start ? range.startContainer : range.endContainer;
495       var offset = start ? range.startOffset : range.endOffset;
496       // Work around (yet another) bug in Opera's selection model.
497       if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
498           container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset]))
499         offset--;
500
501       // For text nodes, we look at the node itself if the cursor is
502       // inside, or at the node before it if the cursor is at the
503       // start.
504       if (node.nodeType == 3){
505         if (offset > 0)
506           return topLevelNodeAt(node, container);
507         else
508           return topLevelNodeBefore(node, container);
509       }
510       // Occasionally, browsers will return the HTML node as
511       // selection. If the offset is 0, we take the start of the frame
512       // ('after null'), otherwise, we take the last node.
513       else if (node.nodeName.toUpperCase() == "HTML") {
514         return (offset == 1 ? null : container.lastChild);
515       }
516       // If the given node is our 'container', we just look up the
517       // correct node by using the offset.
518       else if (node == container) {
519         return (offset == 0) ? null : node.childNodes[offset - 1];
520       }
521       // In any other case, we have a regular node. If the cursor is
522       // at the end of the node, we use the node itself, if it is at
523       // the start, we use the node before it, and in any other
524       // case, we look up the child before the cursor and use that.
525       else {
526         if (offset == node.childNodes.length)
527           return topLevelNodeAt(node, container);
528         else if (offset == 0)
529           return topLevelNodeBefore(node, container);
530         else
531           return topLevelNodeAt(node.childNodes[offset - 1], container);
532       }
533     };
534
535     select.focusAfterNode = function(node, container) {
536       var win = container.ownerDocument.defaultView,
537           range = win.document.createRange();
538       range.setStartBefore(container.firstChild || container);
539       // In Opera, setting the end of a range at the end of a line
540       // (before a BR) will cause the cursor to appear on the next
541       // line, so we set the end inside of the start node when
542       // possible.
543       if (node && !node.firstChild)
544         range.setEndAfter(node);
545       else if (node)
546         range.setEnd(node, node.childNodes.length);
547       else
548         range.setEndBefore(container.firstChild || container);
549       range.collapse(false);
550       selectRange(range, win);
551     };
552
553     select.somethingSelected = function(win) {
554       var range = selectionRange(win);
555       return range && !range.collapsed;
556     };
557
558     select.offsetInNode = function(win, node) {
559       var range = selectionRange(win);
560       if (!range) return 0;
561       range = range.cloneRange();
562       range.setStartBefore(node);
563       return range.toString().length;
564     };
565
566     function insertNodeAtCursor(window, node) {
567       var range = selectionRange(window);
568       if (!range) return;
569
570       range.deleteContents();
571       range.insertNode(node);
572
573       // work around weirdness where Opera will magically insert a new
574       // BR node when a BR node inside a span is moved around. makes
575       // sure the BR ends up outside of spans.
576       if (window.opera && isBR(node) && isSpan(node.parentNode)) {
577         var next = node.nextSibling, p = node.parentNode, outer = p.parentNode;
578         outer.insertBefore(node, p.nextSibling);
579         var textAfter = "";
580         for (; next && next.nodeType == 3; next = next.nextSibling) {
581           textAfter += next.nodeValue;
582           removeElement(next);
583         }
584         outer.insertBefore(makePartSpan(textAfter, window.document), node.nextSibling);
585       }
586       range = window.document.createRange();
587       range.selectNode(node);
588       range.collapse(false);
589       selectRange(range, window);
590     }
591
592     select.insertNewlineAtCursor = function(window) {
593       if (webkit)
594         document.execCommand('insertLineBreak');
595       else
596         insertNodeAtCursor(window, window.document.createElement("BR"));
597     };
598
599     select.insertTabAtCursor = function(window) {
600       insertNodeAtCursor(window, window.document.createTextNode(fourSpaces));
601     };
602
603     select.cursorPos = function(container, start) {
604       var range = selectionRange(window);
605       if (!range) return;
606
607       var topNode = select.selectionTopNode(container, start);
608       while (topNode && !isBR(topNode))
609         topNode = topNode.previousSibling;
610
611       range = range.cloneRange();
612       range.collapse(start);
613       if (topNode)
614         range.setStartAfter(topNode);
615       else
616         range.setStartBefore(container);
617
618       return {node: topNode, offset: range.toString().length};
619     };
620
621     select.setCursorPos = function(container, from, to) {
622       var win = container.ownerDocument.defaultView,
623           range = win.document.createRange();
624
625       function setPoint(node, offset, side) {
626         if (offset == 0 && node && !node.nextSibling) {
627           range["set" + side + "After"](node);
628           return true;
629         }
630
631         if (!node)
632           node = container.firstChild;
633         else
634           node = node.nextSibling;
635
636         if (!node) return;
637
638         if (offset == 0) {
639           range["set" + side + "Before"](node);
640           return true;
641         }
642
643         var backlog = []
644         function decompose(node) {
645           if (node.nodeType == 3)
646             backlog.push(node);
647           else
648             forEach(node.childNodes, decompose);
649         }
650         while (true) {
651           while (node && !backlog.length) {
652             decompose(node);
653             node = node.nextSibling;
654           }
655           var cur = backlog.shift();
656           if (!cur) return false;
657
658           var length = cur.nodeValue.length;
659           if (length >= offset) {
660             range["set" + side](cur, offset);
661             return true;
662           }
663           offset -= length;
664         }
665       }
666
667       to = to || from;
668       if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
669         selectRange(range, win);
670     };
671   }
672 })();