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