d513ba5fac94254314f50d2f98c5eed1c65849fc
[redakcja.git] / redakcja / 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 (pos.nodeName != "BR")
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){}
251             range.collapse(false);
252           }
253           else range.moveToElementText(node.parentNode);
254           if (count) range.move("character", count);
255         }
256         else range.moveToElementText(node);
257       }
258
259       // Do a binary search through the container object, comparing
260       // the start of each node to the selection
261       var start = 0, end = container.childNodes.length - 1;
262       while (start < end) {
263         var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle];
264         if (!node) return false; // Don't ask. IE6 manages this sometimes.
265         moveToNodeStart(range2, node);
266         if (range.compareEndPoints("StartToStart", range2) == 1)
267           start = middle;
268         else
269           end = middle - 1;
270       }
271       return container.childNodes[start] || null;
272     };
273
274     // Place the cursor after this.start. This is only useful when
275     // manually moving the cursor instead of restoring it to its old
276     // position.
277     select.focusAfterNode = function(node, container) {
278       var range = container.ownerDocument.body.createTextRange();
279       range.moveToElementText(node || container);
280       range.collapse(!node);
281       range.select();
282     };
283
284     select.somethingSelected = function(win) {
285       var sel = win.document.selection;
286       return sel && (sel.createRange().text != "");
287     };
288
289     function insertAtCursor(window, html) {
290       var selection = window.document.selection;
291       if (selection) {
292         var range = selection.createRange();
293         range.pasteHTML(html);
294         range.collapse(false);
295         range.select();
296       }
297     }
298
299     // Used to normalize the effect of the enter key, since browsers
300     // do widely different things when pressing enter in designMode.
301     select.insertNewlineAtCursor = function(window) {
302       insertAtCursor(window, "<br>");
303     };
304
305     select.insertTabAtCursor = function(window) {
306       insertAtCursor(window, fourSpaces);
307     };
308
309     // Get the BR node at the start of the line on which the cursor
310     // currently is, and the offset into the line. Returns null as
311     // node if cursor is on first line.
312     select.cursorPos = function(container, start) {
313       var selection = container.ownerDocument.selection;
314       if (!selection) return null;
315
316       var topNode = select.selectionTopNode(container, start);
317       while (topNode && topNode.nodeName != "BR")
318         topNode = topNode.previousSibling;
319
320       var range = selection.createRange(), range2 = range.duplicate();
321       range.collapse(start);
322       if (topNode) {
323         range2.moveToElementText(topNode);
324         range2.collapse(false);
325       }
326       else {
327         // When nothing is selected, we can get all kinds of funky errors here.
328         try { range2.moveToElementText(container); }
329         catch (e) { return null; }
330         range2.collapse(true);
331       }
332       range.setEndPoint("StartToStart", range2);
333
334       return {node: topNode, offset: range.text.length};
335     };
336
337     select.setCursorPos = function(container, from, to) {
338       function rangeAt(pos) {
339         var range = container.ownerDocument.body.createTextRange();
340         if (!pos.node) {
341           range.moveToElementText(container);
342           range.collapse(true);
343         }
344         else {
345           range.moveToElementText(pos.node);
346           range.collapse(false);
347         }
348         range.move("character", pos.offset);
349         return range;
350       }
351
352       var range = rangeAt(from);
353       if (to && to != from)
354         range.setEndPoint("EndToEnd", rangeAt(to));
355       range.select();
356     }
357
358     // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
359     select.selectionCoords = function (win) {
360       var selection = win.document.selection;
361       if (!selection) return null;
362       var start = selection.createRange(), end = start.duplicate();
363       start.collapse(true);
364       end.collapse(false);
365
366       var body = win.document.body;
367       return {start: {x: start.boundingLeft + body.scrollLeft - 1,
368                       y: start.boundingTop + body.scrollTop},
369               end: {x: end.boundingLeft + body.scrollLeft - 1,
370                     y: end.boundingTop + body.scrollTop}};
371     };
372
373     // Restore a stored selection.
374     select.selectCoords = function(win, coords) {
375       if (!coords) return;
376
377       var range1 = win.document.body.createTextRange(), range2 = range1.duplicate();
378       // This can fail for various hard-to-handle reasons.
379       try {
380         range1.moveToPoint(coords.start.x, coords.start.y);
381         range2.moveToPoint(coords.end.x, coords.end.y);
382         range1.setEndPoint("EndToStart", range2);
383         range1.select();
384       } catch(e) {}
385     };
386   }
387   // W3C model
388   else {
389     // Store start and end nodes, and offsets within these, and refer
390     // back to the selection object from those nodes, so that this
391     // object can be updated when the nodes are replaced before the
392     // selection is restored.
393     select.markSelection = function (win) {
394       var selection = win.getSelection();
395       if (!selection || selection.rangeCount == 0)
396         return (currentSelection = null);
397       var range = selection.getRangeAt(0);
398
399       currentSelection = {
400         start: {node: range.startContainer, offset: range.startOffset},
401         end: {node: range.endContainer, offset: range.endOffset},
402         window: win,
403         changed: false
404       };
405
406       // We want the nodes right at the cursor, not one of their
407       // ancestors with a suitable offset. This goes down the DOM tree
408       // until a 'leaf' is reached (or is it *up* the DOM tree?).
409       function normalize(point){
410         while (point.node.nodeType != 3 && point.node.nodeName != "BR") {
411           var newNode = point.node.childNodes[point.offset] || point.node.nextSibling;
412           point.offset = 0;
413           while (!newNode && point.node.parentNode) {
414             point.node = point.node.parentNode;
415             newNode = point.node.nextSibling;
416           }
417           point.node = newNode;
418           if (!newNode)
419             break;
420         }
421       }
422
423       normalize(currentSelection.start);
424       normalize(currentSelection.end);
425     };
426
427     select.selectMarked = function () {
428       if (!currentSelection || !currentSelection.changed) return;
429       var win = currentSelection.window, range = win.document.createRange();
430
431       function setPoint(point, which) {
432         if (point.node) {
433           // Some magic to generalize the setting of the start and end
434           // of a range.
435           if (point.offset == 0)
436             range["set" + which + "Before"](point.node);
437           else
438             range["set" + which](point.node, point.offset);
439         }
440         else {
441           range.setStartAfter(win.document.body.lastChild || win.document.body);
442         }
443       }
444
445       setPoint(currentSelection.end, "End");
446       setPoint(currentSelection.start, "Start");
447       selectRange(range, win);
448     };
449
450     // Helper for selecting a range object.
451     function selectRange(range, window) {
452       var selection = window.getSelection();
453       selection.removeAllRanges();
454       selection.addRange(range);
455     };
456     function selectionRange(window) {
457       var selection = window.getSelection();
458       if (!selection || selection.rangeCount == 0)
459         return false;
460       else
461         return selection.getRangeAt(0);
462     }
463
464     // Finding the top-level node at the cursor in the W3C is, as you
465     // can see, quite an involved process.
466     select.selectionTopNode = function(container, start) {
467       var range = selectionRange(container.ownerDocument.defaultView);
468       if (!range) return false;
469
470       var node = start ? range.startContainer : range.endContainer;
471       var offset = start ? range.startOffset : range.endOffset;
472       // Work around (yet another) bug in Opera's selection model.
473       if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
474           container.childNodes[range.startOffset] && container.childNodes[range.startOffset].nodeName == "BR")
475         offset--;
476
477       // For text nodes, we look at the node itself if the cursor is
478       // inside, or at the node before it if the cursor is at the
479       // start.
480       if (node.nodeType == 3){
481         if (offset > 0)
482           return topLevelNodeAt(node, container);
483         else
484           return topLevelNodeBefore(node, container);
485       }
486       // Occasionally, browsers will return the HTML node as
487       // selection. If the offset is 0, we take the start of the frame
488       // ('after null'), otherwise, we take the last node.
489       else if (node.nodeName == "HTML") {
490         return (offset == 1 ? null : container.lastChild);
491       }
492       // If the given node is our 'container', we just look up the
493       // correct node by using the offset.
494       else if (node == container) {
495         return (offset == 0) ? null : node.childNodes[offset - 1];
496       }
497       // In any other case, we have a regular node. If the cursor is
498       // at the end of the node, we use the node itself, if it is at
499       // the start, we use the node before it, and in any other
500       // case, we look up the child before the cursor and use that.
501       else {
502         if (offset == node.childNodes.length)
503           return topLevelNodeAt(node, container);
504         else if (offset == 0)
505           return topLevelNodeBefore(node, container);
506         else
507           return topLevelNodeAt(node.childNodes[offset - 1], container);
508       }
509     };
510
511     select.focusAfterNode = function(node, container) {
512       var win = container.ownerDocument.defaultView,
513           range = win.document.createRange();
514       range.setStartBefore(container.firstChild || container);
515       // In Opera, setting the end of a range at the end of a line
516       // (before a BR) will cause the cursor to appear on the next
517       // line, so we set the end inside of the start node when
518       // possible.
519       if (node && !node.firstChild)
520         range.setEndAfter(node);
521       else if (node)
522         range.setEnd(node, node.childNodes.length);
523       else
524         range.setEndBefore(container.firstChild || container);
525       range.collapse(false);
526       selectRange(range, win);
527     };
528
529     select.somethingSelected = function(win) {
530       var range = selectionRange(win);
531       return range && !range.collapsed;
532     };
533
534     function insertNodeAtCursor(window, node) {
535       var range = selectionRange(window);
536       if (!range) return;
537
538       range.deleteContents();
539       range.insertNode(node);
540       webkitLastLineHack(window.document.body);
541       range = window.document.createRange();
542       range.selectNode(node);
543       range.collapse(false);
544       selectRange(range, window);
545     }
546
547     select.insertNewlineAtCursor = function(window) {
548       insertNodeAtCursor(window, window.document.createElement("BR"));
549     };
550
551     select.insertTabAtCursor = function(window) {
552       insertNodeAtCursor(window, window.document.createTextNode(fourSpaces));
553     };
554
555     select.cursorPos = function(container, start) {
556       var range = selectionRange(window);
557       if (!range) return;
558
559       var topNode = select.selectionTopNode(container, start);
560       while (topNode && topNode.nodeName != "BR")
561         topNode = topNode.previousSibling;
562
563       range = range.cloneRange();
564       range.collapse(start);
565       if (topNode)
566         range.setStartAfter(topNode);
567       else
568         range.setStartBefore(container);
569       return {node: topNode, offset: range.toString().length};
570     };
571
572     select.setCursorPos = function(container, from, to) {
573       var win = container.ownerDocument.defaultView,
574           range = win.document.createRange();
575
576       function setPoint(node, offset, side) {
577         if (!node)
578           node = container.firstChild;
579         else
580           node = node.nextSibling;
581
582         if (!node)
583           return;
584
585         if (offset == 0) {
586           range["set" + side + "Before"](node);
587           return true;
588         }
589
590         var backlog = []
591         function decompose(node) {
592           if (node.nodeType == 3)
593             backlog.push(node);
594           else
595             forEach(node.childNodes, decompose);
596         }
597         while (true) {
598           while (node && !backlog.length) {
599             decompose(node);
600             node = node.nextSibling;
601           }
602           var cur = backlog.shift();
603           if (!cur) return false;
604
605           var length = cur.nodeValue.length;
606           if (length >= offset) {
607             range["set" + side](cur, offset);
608             return true;
609           }
610           offset -= length;
611         }
612       }
613
614       to = to || from;
615       if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
616         selectRange(range, win);
617     };
618   }
619 })();