123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- /* 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 = !(window.getSelection && document.createRange && document.createRange().endContainer);
- // 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 <br> 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("<br>");
- };
- 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);
- };
- }
- })();
|