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(node, cursor) {
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) {
45 element = element.previousSibling;
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;
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)
58 var y = compensateHack * (element ? element.offsetHeight : 0), x = 0,
59 width = (node ? node.offsetWidth : 0), pos = element;
60 while (pos && pos.offsetParent) {
62 // Don't count X offset for <br> nodes
65 pos = pos.offsetParent;
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;
72 if (cursor || width < screen_width) {
74 var off = select.offsetInNode(win, node), size = nodeText(node).length;
75 if (size) x += width * (off / size);
77 var screen_x = x - scroll_x;
78 if (screen_x < 0 || screen_x > screen_width) {
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;
88 if (scroll) win.scrollTo(scroll_x, scroll_y);
91 select.scrollToCursor = function(container) {
92 select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true);
95 // Used to prevent restoring a selection when we do not need to.
96 var currentSelection = null;
98 select.snapshotChanged = function() {
99 if (currentSelection) currentSelection.changed = true;
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;
115 function replace(point) {
116 if (from == point.node) {
117 currentSelection.changed = true;
118 if (length && point.offset > length) {
119 point.offset -= length;
123 point.offset += (offset || 0);
127 replace(currentSelection.start);
128 replace(currentSelection.end);
131 select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
132 if (!currentSelection) return;
134 function move(point) {
135 if (from == point.node && (!ifAtStart || point.offset == 0)) {
136 currentSelection.changed = true;
138 if (relative) point.offset = Math.max(0, point.offset + distance);
139 else point.offset = distance;
142 move(currentSelection.start);
143 move(currentSelection.end);
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);
153 function nodeAfter(node) {
155 while (!found && node) {
156 found = node.nextSibling;
157 node = node.parentNode;
159 return nodeAtStartOf(found);
162 function nodeAtStartOf(node) {
163 while (node && node.firstChild) node = node.firstChild;
164 return {node: node, offset: 0};
167 var containing = range.parentElement();
168 if (!isAncestor(win.document.body, containing)) return null;
169 if (!containing.firstChild) return nodeAtStartOf(containing);
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);
180 working.moveToElementText(cur);
181 working.collapse(false);
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);
189 working.setEndPoint("StartToEnd", range);
190 return {node: cur, offset: size - working.text.length};
192 return nodeAfter(containing);
195 select.markSelection = function(win) {
196 currentSelection = null;
197 var sel = win.document.selection;
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};
205 select.selectMarked = function() {
206 if (!currentSelection || !currentSelection.changed) return;
207 var win = currentSelection.window, doc = win.document;
209 function makeRange(point) {
210 var range = doc.body.createTextRange(),
213 range.moveToElementText(currentSelection.window.document.body);
214 range.collapse(false);
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;
223 range.move("character", offset);
226 range.moveToElementText(node);
227 range.collapse(true);
232 var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
233 start.setEndPoint("StartToEnd", end);
237 select.offsetInNode = function(win, node) {
238 var sel = win.document.selection;
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;
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;
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);
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;
274 try{range.moveToElementText(cur);}
275 catch(e){return false;}
276 range.collapse(false);
278 else range.moveToElementText(node.parentNode);
279 if (count) range.move("character", count);
282 try{range.moveToElementText(node);}
283 catch(e){return false;}
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)
300 return container.childNodes[start] || null;
303 // Place the cursor after this.start. This is only useful when
304 // manually moving the cursor instead of restoring it to its old
306 select.focusAfterNode = function(node, container) {
307 var range = container.ownerDocument.body.createTextRange();
308 range.moveToElementText(node || container);
309 range.collapse(!node);
313 select.somethingSelected = function(win) {
314 var sel = win.document.selection;
315 return sel && (sel.createRange().text != "");
318 function insertAtCursor(window, html) {
319 var selection = window.document.selection;
321 var range = selection.createRange();
322 range.pasteHTML(html);
323 range.collapse(false);
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>");
334 select.insertTabAtCursor = function(window) {
335 insertAtCursor(window, fourSpaces);
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;
345 var topNode = select.selectionTopNode(container, start);
346 while (topNode && !isBR(topNode))
347 topNode = topNode.previousSibling;
349 var range = selection.createRange(), range2 = range.duplicate();
350 range.collapse(start);
352 range2.moveToElementText(topNode);
353 range2.collapse(false);
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);
361 range.setEndPoint("StartToStart", range2);
363 return {node: topNode, offset: range.text.length};
366 select.setCursorPos = function(container, from, to) {
367 function rangeAt(pos) {
368 var range = container.ownerDocument.body.createTextRange();
370 range.moveToElementText(container);
371 range.collapse(true);
374 range.moveToElementText(pos.node);
375 range.collapse(false);
377 range.move("character", pos.offset);
381 var range = rangeAt(from);
382 if (to && to != from)
383 range.setEndPoint("EndToEnd", rangeAt(to));
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};
393 // Restore a stored selection.
394 select.setBookmark = function(container, mark) {
396 select.setCursorPos(container, mark.from, mark.to);
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;
408 while (!newNode && node.parentNode) {
409 node = node.parentNode;
410 newNode = node.nextSibling;
415 return {node: node, offset: offset};
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);
429 start: innerNode(range.startContainer, range.startOffset),
430 end: innerNode(range.endContainer, range.endOffset),
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;
451 if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
452 var win = cs.window, range = win.document.createRange();
454 function setPoint(point, which) {
456 // Some magic to generalize the setting of the start and end
458 if (point.offset == 0)
459 range["set" + which + "Before"](point.node);
461 range["set" + which](point.node, point.offset);
464 range.setStartAfter(win.document.body.lastChild || win.document.body);
468 setPoint(cs.end, "End");
469 setPoint(cs.start, "Start");
470 selectRange(range, win);
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);
480 function selectionRange(window) {
481 var selection = window.getSelection();
482 if (!selection || selection.rangeCount == 0)
485 return selection.getRangeAt(0);
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;
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]))
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
504 if (node.nodeType == 3){
506 return topLevelNodeAt(node, container);
508 return topLevelNodeBefore(node, container);
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);
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];
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.
526 if (offset == node.childNodes.length)
527 return topLevelNodeAt(node, container);
528 else if (offset == 0)
529 return topLevelNodeBefore(node, container);
531 return topLevelNodeAt(node.childNodes[offset - 1], container);
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
543 if (node && !node.firstChild)
544 range.setEndAfter(node);
546 range.setEnd(node, node.childNodes.length);
548 range.setEndBefore(container.firstChild || container);
549 range.collapse(false);
550 selectRange(range, win);
553 select.somethingSelected = function(win) {
554 var range = selectionRange(win);
555 return range && !range.collapsed;
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;
566 function insertNodeAtCursor(window, node) {
567 var range = selectionRange(window);
570 range.deleteContents();
571 range.insertNode(node);
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);
580 for (; next && next.nodeType == 3; next = next.nextSibling) {
581 textAfter += next.nodeValue;
584 outer.insertBefore(makePartSpan(textAfter, window.document), node.nextSibling);
586 range = window.document.createRange();
587 range.selectNode(node);
588 range.collapse(false);
589 selectRange(range, window);
592 select.insertNewlineAtCursor = function(window) {
594 document.execCommand('insertLineBreak');
596 insertNodeAtCursor(window, window.document.createElement("BR"));
599 select.insertTabAtCursor = function(window) {
600 insertNodeAtCursor(window, window.document.createTextNode(fourSpaces));
603 select.cursorPos = function(container, start) {
604 var range = selectionRange(window);
607 var topNode = select.selectionTopNode(container, start);
608 while (topNode && !isBR(topNode))
609 topNode = topNode.previousSibling;
611 range = range.cloneRange();
612 range.collapse(start);
614 range.setStartAfter(topNode);
616 range.setStartBefore(container);
618 return {node: topNode, offset: range.toString().length};
621 select.setCursorPos = function(container, from, to) {
622 var win = container.ownerDocument.defaultView,
623 range = win.document.createRange();
625 function setPoint(node, offset, side) {
626 if (offset == 0 && node && !node.nextSibling) {
627 range["set" + side + "After"](node);
632 node = container.firstChild;
634 node = node.nextSibling;
639 range["set" + side + "Before"](node);
644 function decompose(node) {
645 if (node.nodeType == 3)
648 forEach(node.childNodes, decompose);
651 while (node && !backlog.length) {
653 node = node.nextSibling;
655 var cur = backlog.shift();
656 if (!cur) return false;
658 var length = cur.nodeValue.length;
659 if (length >= offset) {
660 range["set" + side](cur, offset);
668 if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
669 selectRange(range, win);