1 /* Functionality for finding, storing, and restoring selections
 
   3  * This does not provide a generic API, just the minimal functionality
 
   4  * required by the CodeMirror system.
 
  11   select.ie_selection = document.selection && document.selection.createRangeCollection;
 
  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
 
  17   function topLevelNodeAt(node, top) {
 
  18     while (node && node.parentNode != top)
 
  19       node = node.parentNode;
 
  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);
 
  30   var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
 
  32   select.scrollToNode = function(element) {
 
  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) {
 
  44       element = element.previousSibling;
 
  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;
 
  51     var y = compensateHack * (element ? element.offsetHeight : 0), x = 0, pos = element;
 
  52     while (pos && pos.offsetParent) {
 
  54       // Don't count X offset for <br> nodes
 
  57       pos = pos.offsetParent;
 
  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;
 
  64     if (screen_x < 0 || screen_x > (win.innerWidth || html.clientWidth || 0)) {
 
  68     if (screen_y < 0 || atEnd || screen_y > (win.innerHeight || html.clientHeight || 0) - 50) {
 
  69       scroll_y = atEnd ? 1e10 : y;
 
  72     if (scroll) win.scrollTo(scroll_x, scroll_y);
 
  75   select.scrollToCursor = function(container) {
 
  76     select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild);
 
  79   // Used to prevent restoring a selection when we do not need to.
 
  80   var currentSelection = null;
 
  82   select.snapshotChanged = function() {
 
  83     if (currentSelection) currentSelection.changed = true;
 
  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;
 
  99     function replace(point) {
 
 100       if (from == point.node) {
 
 101         currentSelection.changed = true;
 
 102         if (length && point.offset > length) {
 
 103           point.offset -= length;
 
 107           point.offset += (offset || 0);
 
 111     replace(currentSelection.start);
 
 112     replace(currentSelection.end);
 
 115   select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
 
 116     if (!currentSelection) return;
 
 118     function move(point) {
 
 119       if (from == point.node && (!ifAtStart || point.offset == 0)) {
 
 120         currentSelection.changed = true;
 
 122         if (relative) point.offset = Math.max(0, point.offset + distance);
 
 123         else point.offset = distance;
 
 126     move(currentSelection.start);
 
 127     move(currentSelection.end);
 
 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);
 
 137       function nodeAfter(node) {
 
 139         while (!found && node) {
 
 140           found = node.nextSibling;
 
 141           node = node.parentNode;
 
 143         return nodeAtStartOf(found);
 
 146       function nodeAtStartOf(node) {
 
 147         while (node && node.firstChild) node = node.firstChild;
 
 148         return {node: node, offset: 0};
 
 151       var containing = range.parentElement();
 
 152       if (!isAncestor(win.document.body, containing)) return null;
 
 153       if (!containing.firstChild) return nodeAtStartOf(containing);
 
 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);
 
 164           working.moveToElementText(cur);
 
 165           working.collapse(false);
 
 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);
 
 173         working.setEndPoint("StartToEnd", range);
 
 174         return {node: cur, offset: size - working.text.length};
 
 176       return nodeAfter(containing);
 
 179     select.markSelection = function(win) {
 
 180       currentSelection = null;
 
 181       var sel = win.document.selection;
 
 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};
 
 189     select.selectMarked = function() {
 
 190       if (!currentSelection || !currentSelection.changed) return;
 
 191       var win = currentSelection.window, doc = win.document;
 
 193       function makeRange(point) {
 
 194         var range = doc.body.createTextRange(),
 
 197           range.moveToElementText(currentSelection.window.document.body);
 
 198           range.collapse(false);
 
 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;
 
 207           range.move("character", offset);
 
 210           range.moveToElementText(node);
 
 211           range.collapse(true);
 
 216       var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
 
 217       start.setEndPoint("StartToEnd", end);
 
 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;
 
 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);
 
 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;
 
 249             try{range.moveToElementText(cur);}
 
 250             catch(e){return false;}
 
 251             range.collapse(false);
 
 253           else range.moveToElementText(node.parentNode);
 
 254           if (count) range.move("character", count);
 
 257           try{range.moveToElementText(node);}
 
 258           catch(e){return false;}
 
 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)
 
 275       return container.childNodes[start] || null;
 
 278     // Place the cursor after this.start. This is only useful when
 
 279     // manually moving the cursor instead of restoring it to its old
 
 281     select.focusAfterNode = function(node, container) {
 
 282       var range = container.ownerDocument.body.createTextRange();
 
 283       range.moveToElementText(node || container);
 
 284       range.collapse(!node);
 
 288     select.somethingSelected = function(win) {
 
 289       var sel = win.document.selection;
 
 290       return sel && (sel.createRange().text != "");
 
 293     function insertAtCursor(window, html) {
 
 294       var selection = window.document.selection;
 
 296         var range = selection.createRange();
 
 297         range.pasteHTML(html);
 
 298         range.collapse(false);
 
 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>");
 
 309     select.insertTabAtCursor = function(window) {
 
 310       insertAtCursor(window, fourSpaces);
 
 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;
 
 320       var topNode = select.selectionTopNode(container, start);
 
 321       while (topNode && !isBR(topNode))
 
 322         topNode = topNode.previousSibling;
 
 324       var range = selection.createRange(), range2 = range.duplicate();
 
 325       range.collapse(start);
 
 327         range2.moveToElementText(topNode);
 
 328         range2.collapse(false);
 
 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);
 
 336       range.setEndPoint("StartToStart", range2);
 
 338       return {node: topNode, offset: range.text.length};
 
 341     select.setCursorPos = function(container, from, to) {
 
 342       function rangeAt(pos) {
 
 343         var range = container.ownerDocument.body.createTextRange();
 
 345           range.moveToElementText(container);
 
 346           range.collapse(true);
 
 349           range.moveToElementText(pos.node);
 
 350           range.collapse(false);
 
 352         range.move("character", pos.offset);
 
 356       var range = rangeAt(from);
 
 357       if (to && to != from)
 
 358         range.setEndPoint("EndToEnd", rangeAt(to));
 
 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);
 
 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}};
 
 377     // Restore a stored selection.
 
 378     select.selectCoords = function(win, coords) {
 
 381       var range1 = win.document.body.createTextRange(), range2 = range1.duplicate();
 
 382       // This can fail for various hard-to-handle reasons.
 
 384         range1.moveToPoint(coords.start.x, coords.start.y);
 
 385         range2.moveToPoint(coords.end.x, coords.end.y);
 
 386         range1.setEndPoint("EndToStart", range2);
 
 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);
 
 404         start: {node: range.startContainer, offset: range.startOffset},
 
 405         end: {node: range.endContainer, offset: range.endOffset},
 
 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;
 
 417           while (!newNode && point.node.parentNode) {
 
 418             point.node = point.node.parentNode;
 
 419             newNode = point.node.nextSibling;
 
 421           point.node = newNode;
 
 427       normalize(currentSelection.start);
 
 428       normalize(currentSelection.end);
 
 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();
 
 436       function setPoint(point, which) {
 
 438           // Some magic to generalize the setting of the start and end
 
 440           if (point.offset == 0)
 
 441             range["set" + which + "Before"](point.node);
 
 443             range["set" + which](point.node, point.offset);
 
 446           range.setStartAfter(win.document.body.lastChild || win.document.body);
 
 450       setPoint(cs.end, "End");
 
 451       setPoint(cs.start, "Start");
 
 452       selectRange(range, win);
 
 455     // Helper for selecting a range object.
 
 456     function selectRange(range, window) {
 
 457       var selection = window.getSelection();
 
 458       selection.removeAllRanges();
 
 459       selection.addRange(range);
 
 461     function selectionRange(window) {
 
 462       var selection = window.getSelection();
 
 463       if (!selection || selection.rangeCount == 0)
 
 466         return selection.getRangeAt(0);
 
 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;
 
 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]))
 
 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
 
 485       if (node.nodeType == 3){
 
 487           return topLevelNodeAt(node, container);
 
 489           return topLevelNodeBefore(node, container);
 
 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);
 
 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];
 
 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.
 
 507         if (offset == node.childNodes.length)
 
 508           return topLevelNodeAt(node, container);
 
 509         else if (offset == 0)
 
 510           return topLevelNodeBefore(node, container);
 
 512           return topLevelNodeAt(node.childNodes[offset - 1], container);
 
 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
 
 524       if (node && !node.firstChild)
 
 525         range.setEndAfter(node);
 
 527         range.setEnd(node, node.childNodes.length);
 
 529         range.setEndBefore(container.firstChild || container);
 
 530       range.collapse(false);
 
 531       selectRange(range, win);
 
 534     select.somethingSelected = function(win) {
 
 535       var range = selectionRange(win);
 
 536       return range && !range.collapsed;
 
 539     function insertNodeAtCursor(window, node) {
 
 540       var range = selectionRange(window);
 
 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);
 
 552     select.insertNewlineAtCursor = function(window) {
 
 553       insertNodeAtCursor(window, window.document.createElement("BR"));
 
 556     select.insertTabAtCursor = function(window) {
 
 557       insertNodeAtCursor(window, window.document.createTextNode(fourSpaces));
 
 560     select.cursorPos = function(container, start) {
 
 561       var range = selectionRange(window);
 
 564       var topNode = select.selectionTopNode(container, start);
 
 565       while (topNode && !isBR(topNode))
 
 566         topNode = topNode.previousSibling;
 
 568       range = range.cloneRange();
 
 569       range.collapse(start);
 
 571         range.setStartAfter(topNode);
 
 573         range.setStartBefore(container);
 
 574       return {node: topNode, offset: range.toString().length};
 
 577     select.setCursorPos = function(container, from, to) {
 
 578       var win = container.ownerDocument.defaultView,
 
 579           range = win.document.createRange();
 
 581       function setPoint(node, offset, side) {
 
 583           node = container.firstChild;
 
 585           node = node.nextSibling;
 
 591           range["set" + side + "Before"](node);
 
 596         function decompose(node) {
 
 597           if (node.nodeType == 3)
 
 600             forEach(node.childNodes, decompose);
 
 603           while (node && !backlog.length) {
 
 605             node = node.nextSibling;
 
 607           var cur = backlog.shift();
 
 608           if (!cur) return false;
 
 610           var length = cur.nodeValue.length;
 
 611           if (length >= offset) {
 
 612             range["set" + side](cur, offset);
 
 620       if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
 
 621         selectRange(range, win);