/** * TridentSelection.js * * Copyright 2009, Moxiecode Systems AB * Released under LGPL License. * * License: http://tinymce.moxiecode.com/license * Contributing: http://tinymce.moxiecode.com/contributing */ (function() { function Selection(selection) { var self = this, dom = selection.dom, TRUE = true, FALSE = false; function getPosition(rng, start) { var checkRng, startIndex = 0, endIndex, inside, children, child, offset, index, position = -1, parent; // Setup test range, collapse it and get the parent checkRng = rng.duplicate(); checkRng.collapse(start); parent = checkRng.parentElement(); // Check if the selection is within the right document if (parent.ownerDocument !== selection.dom.doc) return; // IE will report non editable elements as it's parent so look for an editable one while (parent.contentEditable === "false") { parent = parent.parentNode; } // If parent doesn't have any children then return that we are inside the element if (!parent.hasChildNodes()) { return {node : parent, inside : 1}; } // Setup node list and endIndex children = parent.children; endIndex = children.length - 1; // Perform a binary search for the position while (startIndex <= endIndex) { index = Math.floor((startIndex + endIndex) / 2); // Move selection to node and compare the ranges child = children[index]; checkRng.moveToElementText(child); position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); // Before/after or an exact match if (position > 0) { endIndex = index - 1; } else if (position < 0) { startIndex = index + 1; } else { return {node : child}; } } // Check if child position is before or we didn't find a position if (position < 0) { // No element child was found use the parent element and the offset inside that if (!child) { checkRng.moveToElementText(parent); checkRng.collapse(true); child = parent; inside = true; } else checkRng.collapse(false); checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng); // Fix for edge case:
..
ab|c
if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) { checkRng = rng.duplicate(); checkRng.collapse(start); offset = -1; while (parent == checkRng.parentElement()) { if (checkRng.move('character', -1) == 0) break; offset++; } } offset = offset || checkRng.text.replace('\r\n', ' ').length; } else { // Child position is after the selection endpoint checkRng.collapse(true); checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng); // Get the length of the text to find where the endpoint is relative to it's container offset = checkRng.text.replace('\r\n', ' ').length; } return {node : child, position : position, offset : offset, inside : inside}; }; // Returns a W3C DOM compatible range object by using the IE Range API function getRange() { var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; // If selection is outside the current document just return an empty range element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); if (element.ownerDocument != dom.doc) return domRange; collapsed = selection.isCollapsed(); // Handle control selection if (ieRange.item) { domRange.setStart(element.parentNode, dom.nodeIndex(element)); domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); return domRange; } function findEndPoint(start) { var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; container = endPoint.node; offset = endPoint.offset; if (endPoint.inside && !container.hasChildNodes()) { domRange[start ? 'setStart' : 'setEnd'](container, 0); return; } if (offset === undef) { domRange[start ? 'setStartBefore' : 'setEndAfter'](container); return; } if (endPoint.position < 0) { sibling = endPoint.inside ? container.firstChild : container.nextSibling; if (!sibling) { domRange[start ? 'setStartAfter' : 'setEndAfter'](container); return; } if (!offset) { if (sibling.nodeType == 3) domRange[start ? 'setStart' : 'setEnd'](sibling, 0); else domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); return; } // Find the text node and offset while (sibling) { nodeValue = sibling.nodeValue; textNodeOffset += nodeValue.length; // We are at or passed the position we where looking for if (textNodeOffset >= offset) { container = sibling; textNodeOffset -= offset; textNodeOffset = nodeValue.length - textNodeOffset; break; } sibling = sibling.nextSibling; } } else { // Find the text node and offset sibling = container.previousSibling; if (!sibling) return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); // If there isn't any text to loop then use the first position if (!offset) { if (container.nodeType == 3) domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); else domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); return; } while (sibling) { textNodeOffset += sibling.nodeValue.length; // We are at or passed the position we where looking for if (textNodeOffset >= offset) { container = sibling; textNodeOffset -= offset; break; } sibling = sibling.previousSibling; } } domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); }; try { // Find start point findEndPoint(true); // Find end point if needed if (!collapsed) findEndPoint(); } catch (ex) { // IE has a nasty bug where text nodes might throw "invalid argument" when you // access the nodeValue or other properties of text nodes. This seems to happend when // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. if (ex.number == -2147024809) { // Get the current selection bookmark = self.getBookmark(2); // Get start element tmpRange = ieRange.duplicate(); tmpRange.collapse(true); element = tmpRange.parentElement(); // Get end element if (!collapsed) { tmpRange = ieRange.duplicate(); tmpRange.collapse(false); element2 = tmpRange.parentElement(); element2.innerHTML = element2.innerHTML; } // Remove the broken elements element.innerHTML = element.innerHTML; // Restore the selection self.moveToBookmark(bookmark); // Since the range has moved we need to re-get it ieRange = selection.getRng(); // Find start point findEndPoint(true); // Find end point if needed if (!collapsed) findEndPoint(); } else throw ex; // Throw other errors } return domRange; }; this.getBookmark = function(type) { var rng = selection.getRng(), start, end, bookmark = {}; function getIndexes(node) { var node, parent, root, children, i, indexes = []; parent = node.parentNode; root = dom.getRoot().parentNode; while (parent != root) { children = parent.children; i = children.length; while (i--) { if (node === children[i]) { indexes.push(i); break; } } node = parent; parent = parent.parentNode; } return indexes; }; function getBookmarkEndPoint(start) { var position; position = getPosition(rng, start); if (position) { return { position : position.position, offset : position.offset, indexes : getIndexes(position.node), inside : position.inside }; } }; // Non ubstructive bookmark if (type === 2) { // Handle text selection if (!rng.item) { bookmark.start = getBookmarkEndPoint(true); if (!selection.isCollapsed()) bookmark.end = getBookmarkEndPoint(); } else bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; } return bookmark; }; this.moveToBookmark = function(bookmark) { var rng, body = dom.doc.body; function resolveIndexes(indexes) { var node, i, idx, children; node = dom.getRoot(); for (i = indexes.length - 1; i >= 0; i--) { children = node.children; idx = indexes[i]; if (idx <= children.length - 1) { node = children[idx]; } } return node; }; function setBookmarkEndPoint(start) { var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; if (endPoint) { moveLeft = endPoint.position > 0; moveRng = body.createTextRange(); moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); offset = endPoint.offset; if (offset !== undef) { moveRng.collapse(endPoint.inside || moveLeft); moveRng.moveStart('character', moveLeft ? -offset : offset); } else moveRng.collapse(start); rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); if (start) rng.collapse(true); } }; if (bookmark.start) { if (bookmark.start.ctrl) { rng = body.createControlRange(); rng.addElement(resolveIndexes(bookmark.start.indexes)); rng.select(); } else { rng = body.createTextRange(); setBookmarkEndPoint(true); setBookmarkEndPoint(); rng.select(); } } }; this.addRange = function(rng) { var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; function setEndPoint(start) { var container, offset, marker, tmpRng, nodes; marker = dom.create('a'); container = start ? startContainer : endContainer; offset = start ? startOffset : endOffset; tmpRng = ieRng.duplicate(); if (container == doc || container == doc.documentElement) { container = body; offset = 0; } if (container.nodeType == 3) { container.parentNode.insertBefore(marker, container); tmpRng.moveToElementText(marker); tmpRng.moveStart('character', offset); dom.remove(marker); ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); } else { nodes = container.childNodes; if (nodes.length) { if (offset >= nodes.length) { dom.insertAfter(marker, nodes[nodes.length - 1]); } else { container.insertBefore(marker, nodes[offset]); } tmpRng.moveToElementText(marker); } else { // Empty node selection for example
|
marker = doc.createTextNode('\uFEFF'); container.appendChild(marker); tmpRng.moveToElementText(marker.parentNode); tmpRng.collapse(TRUE); } ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); dom.remove(marker); } } // Setup some shorter versions startContainer = rng.startContainer; startOffset = rng.startOffset; endContainer = rng.endContainer; endOffset = rng.endOffset; ieRng = body.createTextRange(); // If single element selection then try making a control selection out of it if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) { if (startOffset == endOffset - 1) { try { ctrlRng = body.createControlRange(); ctrlRng.addElement(startContainer.childNodes[startOffset]); ctrlRng.select(); return; } catch (ex) { // Ignore } } } // Set start/end point of selection setEndPoint(true); setEndPoint(); // Select the new range and scroll it into view ieRng.select(); }; // Expose range method this.getRangeAt = getRange; }; // Expose the selection object tinymce.dom.TridentSelection = Selection; })();