/* Functionality for finding, storing, and restoring selections * * This does not provide a generic API, just the minimal functionality * required by the CodeMirror system. */ // Namespace object. var select = {}; (function() { select.ie_selection = document.selection && document.selection.createRangeCollection; // Find the 'top-level' (defined as 'a direct child of the node // passed as the top argument') node that the given node is // contained in. Return null if the given node is not inside the top // node. function topLevelNodeAt(node, top) { while (node && node.parentNode != top) node = node.parentNode; return node; } // Find the top-level node that contains the node before this one. function topLevelNodeBefore(node, top) { while (!node.previousSibling && node.parentNode != top) node = node.parentNode; return topLevelNodeAt(node.previousSibling, top); } var fourSpaces = "\u00a0\u00a0\u00a0\u00a0"; select.scrollToNode = function(node, cursor) { if (!node) return; var element = node, body = document.body, html = document.documentElement, atEnd = !element.nextSibling || !element.nextSibling.nextSibling || !element.nextSibling.nextSibling.nextSibling; // In Opera (and recent Webkit versions), BR elements *always* // have a offsetTop property of zero. var compensateHack = 0; while (element && !element.offsetTop) { compensateHack++; element = element.previousSibling; } // atEnd is another kludge for these browsers -- if the cursor is // at the end of the document, and the node doesn't have an // offset, just scroll to the end. if (compensateHack == 0) atEnd = false; // WebKit has a bad habit of (sometimes) happily returning bogus // offsets when the document has just been changed. This seems to // always be 5/5, so we don't use those. if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5) return; var y = compensateHack * (element ? element.offsetHeight : 0), x = 0, width = (node ? node.offsetWidth : 0), pos = element; while (pos && pos.offsetParent) { y += pos.offsetTop; // Don't count X offset for
nodes if (!isBR(pos)) x += pos.offsetLeft; pos = pos.offsetParent; } var scroll_x = body.scrollLeft || html.scrollLeft || 0, scroll_y = body.scrollTop || html.scrollTop || 0, scroll = false, screen_width = window.innerWidth || html.clientWidth || 0; if (cursor || width < screen_width) { if (cursor) { var off = select.offsetInNode(node), size = nodeText(node).length; if (size) x += width * (off / size); } var screen_x = x - scroll_x; if (screen_x < 0 || screen_x > screen_width) { scroll_x = x; scroll = true; } } var screen_y = y - scroll_y; if (screen_y < 0 || atEnd || screen_y > (window.innerHeight || html.clientHeight || 0) - 50) { scroll_y = atEnd ? 1e6 : y; scroll = true; } if (scroll) window.scrollTo(scroll_x, scroll_y); }; select.scrollToCursor = function(container) { select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true); }; // Used to prevent restoring a selection when we do not need to. var currentSelection = null; select.snapshotChanged = function() { if (currentSelection) currentSelection.changed = true; }; // Find the 'leaf' node (BR or text) after the given one. function baseNodeAfter(node) { var next = node.nextSibling; if (next) { while (next.firstChild) next = next.firstChild; if (next.nodeType == 3 || isBR(next)) return next; else return baseNodeAfter(next); } else { var parent = node.parentNode; while (parent && !parent.nextSibling) parent = parent.parentNode; return parent && baseNodeAfter(parent); } } // This is called by the code in editor.js whenever it is replacing // a text node. The function sees whether the given oldNode is part // of the current selection, and updates this selection if it is. // Because nodes are often only partially replaced, the length of // the part that gets replaced has to be taken into account -- the // selection might stay in the oldNode if the newNode is smaller // than the selection's offset. The offset argument is needed in // case the selection does move to the new object, and the given // length is not the whole length of the new node (part of it might // have been used to replace another node). select.snapshotReplaceNode = function(from, to, length, offset) { if (!currentSelection) return; function replace(point) { if (from == point.node) { currentSelection.changed = true; if (length && point.offset > length) { point.offset -= length; } else { point.node = to; point.offset += (offset || 0); } } else if (select.ie_selection && point.offset == 0 && point.node == baseNodeAfter(from)) { currentSelection.changed = true; } } replace(currentSelection.start); replace(currentSelection.end); }; select.snapshotMove = function(from, to, distance, relative, ifAtStart) { if (!currentSelection) return; function move(point) { if (from == point.node && (!ifAtStart || point.offset == 0)) { currentSelection.changed = true; point.node = to; if (relative) point.offset = Math.max(0, point.offset + distance); else point.offset = distance; } } move(currentSelection.start); move(currentSelection.end); }; // Most functions are defined in two ways, one for the IE selection // model, one for the W3C one. if (select.ie_selection) { function selRange() { var sel = document.selection; if (!sel) return null; if (sel.createRange) return sel.createRange(); else return sel.createTextRange(); } function selectionNode(start) { var range = selRange(); range.collapse(start); function nodeAfter(node) { var found = null; while (!found && node) { found = node.nextSibling; node = node.parentNode; } return nodeAtStartOf(found); } function nodeAtStartOf(node) { while (node && node.firstChild) node = node.firstChild; return {node: node, offset: 0}; } var containing = range.parentElement(); if (!isAncestor(document.body, containing)) return null; if (!containing.firstChild) return nodeAtStartOf(containing); var working = range.duplicate(); working.moveToElementText(containing); working.collapse(true); for (var cur = containing.firstChild; cur; cur = cur.nextSibling) { if (cur.nodeType == 3) { var size = cur.nodeValue.length; working.move("character", size); } else { working.moveToElementText(cur); working.collapse(false); } var dir = range.compareEndPoints("StartToStart", working); if (dir == 0) return nodeAfter(cur); if (dir == 1) continue; if (cur.nodeType != 3) return nodeAtStartOf(cur); working.setEndPoint("StartToEnd", range); return {node: cur, offset: size - working.text.length}; } return nodeAfter(containing); } select.markSelection = function() { currentSelection = null; var sel = document.selection; if (!sel) return; var start = selectionNode(true), end = selectionNode(false); if (!start || !end) return; currentSelection = {start: start, end: end, changed: false}; }; select.selectMarked = function() { if (!currentSelection || !currentSelection.changed) return; function makeRange(point) { var range = document.body.createTextRange(), node = point.node; if (!node) { range.moveToElementText(document.body); range.collapse(false); } else if (node.nodeType == 3) { range.moveToElementText(node.parentNode); var offset = point.offset; while (node.previousSibling) { node = node.previousSibling; offset += (node.innerText || "").length; } range.move("character", offset); } else { range.moveToElementText(node); range.collapse(true); } return range; } var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end); start.setEndPoint("StartToEnd", end); start.select(); }; select.offsetInNode = function(node) { var range = selRange(); if (!range) return 0; var range2 = range.duplicate(); try {range2.moveToElementText(node);} catch(e){return 0;} range.setEndPoint("StartToStart", range2); return range.text.length; }; // Get the top-level node that one end of the cursor is inside or // after. Note that this returns false for 'no cursor', and null // for 'start of document'. select.selectionTopNode = function(container, start) { var range = selRange(); if (!range) return false; var range2 = range.duplicate(); range.collapse(start); var around = range.parentElement(); if (around && isAncestor(container, around)) { // Only use this node if the selection is not at its start. range2.moveToElementText(around); if (range.compareEndPoints("StartToStart", range2) == 1) return topLevelNodeAt(around, container); } // Move the start of a range to the start of a node, // compensating for the fact that you can't call // moveToElementText with text nodes. function moveToNodeStart(range, node) { if (node.nodeType == 3) { var count = 0, cur = node.previousSibling; while (cur && cur.nodeType == 3) { count += cur.nodeValue.length; cur = cur.previousSibling; } if (cur) { try{range.moveToElementText(cur);} catch(e){return false;} range.collapse(false); } else range.moveToElementText(node.parentNode); if (count) range.move("character", count); } else { try{range.moveToElementText(node);} catch(e){return false;} } return true; } // Do a binary search through the container object, comparing // the start of each node to the selection var start = 0, end = container.childNodes.length - 1; while (start < end) { var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle]; if (!node) return false; // Don't ask. IE6 manages this sometimes. if (!moveToNodeStart(range2, node)) return false; if (range.compareEndPoints("StartToStart", range2) == 1) start = middle; else end = middle - 1; } if (start == 0) { var test1 = selRange(), test2 = test1.duplicate(); try { test2.moveToElementText(container); } catch(exception) { return null; } if (test1.compareEndPoints("StartToStart", test2) == 0) return null; } return container.childNodes[start] || null; }; // Place the cursor after this.start. This is only useful when // manually moving the cursor instead of restoring it to its old // position. select.focusAfterNode = function(node, container) { var range = document.body.createTextRange(); range.moveToElementText(node || container); range.collapse(!node); range.select(); }; select.somethingSelected = function() { var range = selRange(); return range && (range.text != ""); }; function insertAtCursor(html) { var range = selRange(); if (range) { range.pasteHTML(html); range.collapse(false); range.select(); } } // Used to normalize the effect of the enter key, since browsers // do widely different things when pressing enter in designMode. select.insertNewlineAtCursor = function() { insertAtCursor("
"); }; select.insertTabAtCursor = function() { insertAtCursor(fourSpaces); }; // Get the BR node at the start of the line on which the cursor // currently is, and the offset into the line. Returns null as // node if cursor is on first line. select.cursorPos = function(container, start) { var range = selRange(); if (!range) return null; var topNode = select.selectionTopNode(container, start); while (topNode && !isBR(topNode)) topNode = topNode.previousSibling; var range2 = range.duplicate(); range.collapse(start); if (topNode) { range2.moveToElementText(topNode); range2.collapse(false); } else { // When nothing is selected, we can get all kinds of funky errors here. try { range2.moveToElementText(container); } catch (e) { return null; } range2.collapse(true); } range.setEndPoint("StartToStart", range2); return {node: topNode, offset: range.text.length}; }; select.setCursorPos = function(container, from, to) { function rangeAt(pos) { var range = document.body.createTextRange(); if (!pos.node) { range.moveToElementText(container); range.collapse(true); } else { range.moveToElementText(pos.node); range.collapse(false); } range.move("character", pos.offset); return range; } var range = rangeAt(from); if (to && to != from) range.setEndPoint("EndToEnd", rangeAt(to)); range.select(); } // Some hacks for storing and re-storing the selection when the editor loses and regains focus. select.getBookmark = function (container) { var from = select.cursorPos(container, true), to = select.cursorPos(container, false); if (from && to) return {from: from, to: to}; }; // Restore a stored selection. select.setBookmark = function(container, mark) { if (!mark) return; select.setCursorPos(container, mark.from, mark.to); }; } // W3C model else { // Find the node right at the cursor, not one of its // ancestors with a suitable offset. This goes down the DOM tree // until a 'leaf' is reached (or is it *up* the DOM tree?). function innerNode(node, offset) { while (node.nodeType != 3 && !isBR(node)) { var newNode = node.childNodes[offset] || node.nextSibling; offset = 0; while (!newNode && node.parentNode) { node = node.parentNode; newNode = node.nextSibling; } node = newNode; if (!newNode) break; } return {node: node, offset: offset}; } // Store start and end nodes, and offsets within these, and refer // back to the selection object from those nodes, so that this // object can be updated when the nodes are replaced before the // selection is restored. select.markSelection = function () { var selection = window.getSelection(); if (!selection || selection.rangeCount == 0) return (currentSelection = null); var range = selection.getRangeAt(0); currentSelection = { start: innerNode(range.startContainer, range.startOffset), end: innerNode(range.endContainer, range.endOffset), changed: false }; }; select.selectMarked = function () { var cs = currentSelection; // on webkit-based browsers, it is apparently possible that the // selection gets reset even when a node that is not one of the // endpoints get messed with. the most common situation where // this occurs is when a selection is deleted or overwitten. we // check for that here. function focusIssue() { if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) { var selection = window.getSelection(); if (!selection || selection.rangeCount == 0) return true; var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset); return cs.start.node != point.node || cs.start.offset != point.offset; } } if (!cs || !(cs.changed || (webkit && focusIssue()))) return; var range = document.createRange(); function setPoint(point, which) { if (point.node) { // Some magic to generalize the setting of the start and end // of a range. if (point.offset == 0) range["set" + which + "Before"](point.node); else range["set" + which](point.node, point.offset); } else { range.setStartAfter(document.body.lastChild || document.body); } } setPoint(cs.end, "End"); setPoint(cs.start, "Start"); selectRange(range); }; // Helper for selecting a range object. function selectRange(range) { var selection = window.getSelection(); if (!selection) return; selection.removeAllRanges(); selection.addRange(range); } function selectionRange() { var selection = window.getSelection(); if (!selection || selection.rangeCount == 0) return false; else return selection.getRangeAt(0); } // Finding the top-level node at the cursor in the W3C is, as you // can see, quite an involved process. select.selectionTopNode = function(container, start) { var range = selectionRange(); if (!range) return false; var node = start ? range.startContainer : range.endContainer; var offset = start ? range.startOffset : range.endOffset; // Work around (yet another) bug in Opera's selection model. if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 && container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset])) offset--; // For text nodes, we look at the node itself if the cursor is // inside, or at the node before it if the cursor is at the // start. if (node.nodeType == 3){ if (offset > 0) return topLevelNodeAt(node, container); else return topLevelNodeBefore(node, container); } // Occasionally, browsers will return the HTML node as // selection. If the offset is 0, we take the start of the frame // ('after null'), otherwise, we take the last node. else if (node.nodeName.toUpperCase() == "HTML") { return (offset == 1 ? null : container.lastChild); } // If the given node is our 'container', we just look up the // correct node by using the offset. else if (node == container) { return (offset == 0) ? null : node.childNodes[offset - 1]; } // In any other case, we have a regular node. If the cursor is // at the end of the node, we use the node itself, if it is at // the start, we use the node before it, and in any other // case, we look up the child before the cursor and use that. else { if (offset == node.childNodes.length) return topLevelNodeAt(node, container); else if (offset == 0) return topLevelNodeBefore(node, container); else return topLevelNodeAt(node.childNodes[offset - 1], container); } }; select.focusAfterNode = function(node, container) { var range = document.createRange(); range.setStartBefore(container.firstChild || container); // In Opera, setting the end of a range at the end of a line // (before a BR) will cause the cursor to appear on the next // line, so we set the end inside of the start node when // possible. if (node && !node.firstChild) range.setEndAfter(node); else if (node) range.setEnd(node, node.childNodes.length); else range.setEndBefore(container.firstChild || container); range.collapse(false); selectRange(range); }; select.somethingSelected = function() { var range = selectionRange(); return range && !range.collapsed; }; select.offsetInNode = function(node) { var range = selectionRange(); if (!range) return 0; range = range.cloneRange(); range.setStartBefore(node); return range.toString().length; }; select.insertNodeAtCursor = function(node) { var range = selectionRange(); if (!range) return; range.deleteContents(); range.insertNode(node); webkitLastLineHack(document.body); // work around weirdness where Opera will magically insert a new // BR node when a BR node inside a span is moved around. makes // sure the BR ends up outside of spans. if (window.opera && isBR(node) && isSpan(node.parentNode)) { var next = node.nextSibling, p = node.parentNode, outer = p.parentNode; outer.insertBefore(node, p.nextSibling); var textAfter = ""; for (; next && next.nodeType == 3; next = next.nextSibling) { textAfter += next.nodeValue; removeElement(next); } outer.insertBefore(makePartSpan(textAfter, document), node.nextSibling); } range = document.createRange(); range.selectNode(node); range.collapse(false); selectRange(range); } select.insertNewlineAtCursor = function() { select.insertNodeAtCursor(document.createElement("BR")); }; select.insertTabAtCursor = function() { select.insertNodeAtCursor(document.createTextNode(fourSpaces)); }; select.cursorPos = function(container, start) { var range = selectionRange(); if (!range) return; var topNode = select.selectionTopNode(container, start); while (topNode && !isBR(topNode)) topNode = topNode.previousSibling; range = range.cloneRange(); range.collapse(start); if (topNode) range.setStartAfter(topNode); else range.setStartBefore(container); var text = range.toString(); return {node: topNode, offset: text.length}; }; select.setCursorPos = function(container, from, to) { var range = document.createRange(); function setPoint(node, offset, side) { if (offset == 0 && node && !node.nextSibling) { range["set" + side + "After"](node); return true; } if (!node) node = container.firstChild; else node = node.nextSibling; if (!node) return; if (offset == 0) { range["set" + side + "Before"](node); return true; } var backlog = [] function decompose(node) { if (node.nodeType == 3) backlog.push(node); else forEach(node.childNodes, decompose); } while (true) { while (node && !backlog.length) { decompose(node); node = node.nextSibling; } var cur = backlog.shift(); if (!cur) return false; var length = cur.nodeValue.length; if (length >= offset) { range["set" + side](cur, offset); return true; } offset -= length; } } to = to || from; if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start")) selectRange(range); }; } })();