/**
* 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:
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 && parent.nodeType !== 9) {
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;
})();