Skip to content

Commit

Permalink
Optimized IE TextRange-to-Range by using a binary search and by limit…
Browse files Browse the repository at this point in the history
…ing the scope of the search for the end boundary if the start boundary lies within the same element.
  • Loading branch information
timdown committed Jun 15, 2012
1 parent 6dc1d12 commit 3e0a970
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 85 deletions.
4 changes: 2 additions & 2 deletions src/js/core/domrange.js
Expand Up @@ -208,11 +208,11 @@ rangy.createModule("DomRange", function(api, module) {
// Check for partially selected text nodes
if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
if (current === this.ec) {
log.info("*** CLONING END");
//log.info("*** CLONING END");
(current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
}
if (this._current === this.sc) {
log.info("*** CLONING START");
//log.info("*** CLONING START");
(current = current.cloneNode(true)).deleteData(0, this.so);
}
}
Expand Down
116 changes: 49 additions & 67 deletions src/js/core/wrappedrange.js
Expand Up @@ -55,7 +55,7 @@ rangy.createModule("WrappedRange", function(api, module) {
// an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
// grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
// for inputs and images, plus optimizations.
function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
var workingRange = textRange.duplicate();

workingRange.collapse(isStart);
Expand All @@ -78,79 +78,48 @@ rangy.createModule("WrappedRange", function(api, module) {

var workingNode = dom.getDocument(containerElement).createElement("span");

// Workaround for HTML5 Shiv's insane violation of document.createElement(). See issue 104.
// Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML 5 Shiv
// issue 64: https://github.com/aFarkas/html5shiv/issues/64
if (workingNode.parentNode) {
workingNode.parentNode.removeChild(workingNode);
}

var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
var previousNode, nextNode, boundaryPosition, boundaryNode;

// Move the working range through the container's children, starting at the end and working backwards, until the
// working range reaches or goes past the boundary we're interested in
/*
log.warn("workingNode at position " + dom.getNodeIndex(workingNode) + " in " +
dom.inspectNode(containerElement) + ", previous sibling is " +
dom.inspectNode(workingNode.previousSibling) + ", next sibling is " +
dom.inspectNode(workingNode.nextSibling));
*/

if (api.config.useBinarySearch) {
var nodeIndex, newNodeIndex, start = 0, end = containerElement.childNodes.length;
var limit = 0;
while (limit++ < 40) {
/*
newNodeIndex = Math.floor((start + end) / 2);
if (newNodeIndex > nodeIndex) {
newNodeIndex++;
}
nodeIndex = newNodeIndex;
*/

var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
var childNodeCount = containerElement.childNodes.length;
var end = childNodeCount;

// Check end first. Code within the loop assumes that the endth child node of the container is definitely
// after the range boundary.
var nodeIndex = end;

while (true) {
log.debug("nodeIndex is " + nodeIndex + ", start: " + start + ", end: " + end);
if (nodeIndex == childNodeCount) {
containerElement.appendChild(workingNode);
} else {
containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
workingRange.moveToElementText(workingNode);
comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
log.debug("*** node index: " + nodeIndex + ", start: " + start + ", end: " + end + ", comparison: " + comparison);
if (comparison == 0) {
}
workingRange.moveToElementText(workingNode);
comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
if (comparison == 0 || start == end) {
break;
} else if (comparison == -1) {
if (end == start + 1) {
// We know the endth child node is after the range boundary, so we must be done.
break;
} else if (comparison == 1) {
end = (end == start + 1) ? start : nodeIndex;
} else {
start = (end == start + 1) ? end : nodeIndex;
start = nodeIndex
}
} else {
end = (end == start + 1) ? start : nodeIndex;
}
if (limit >= 40) {
throw new Error("Limit reached");
}

var binaryResult = dom.getNodeIndex(workingNode);
log.debug("*** BINARY SEARCH GOT node index " + binaryResult);

workingNode.parentNode.removeChild(workingNode);

do {
containerElement.insertBefore(workingNode, workingNode.previousSibling);
workingRange.moveToElementText(workingNode);
log.debug("*** node index: " + dom.getNodeIndex(workingNode) + ", comparison: " + comparison);
} while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
workingNode.previousSibling);

var linearResult = dom.getNodeIndex(workingNode);
log.debug("*** LINEAR SEARCH GOT node index " + linearResult + ", comparison: " + comparison);

if (linearResult != binaryResult) {
throw new Error("Linear and binary do not agree (linear: " + linearResult + ", binary: " + binaryResult + ")");
}

} else {
do {
containerElement.insertBefore(workingNode, workingNode.previousSibling);
workingRange.moveToElementText(workingNode);
} while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
workingNode.previousSibling);
nodeIndex = Math.floor((start + end) / 2);
containerElement.removeChild(workingNode);
}

log.debug("*** GOT node index " + dom.getNodeIndex(workingNode));
log.debug("*** GOT node index " + nodeIndex);

// We've now reached or gone past the boundary of the text range we're interested in
// so have identified the node we want
Expand Down Expand Up @@ -230,7 +199,13 @@ rangy.createModule("WrappedRange", function(api, module) {
// Clean up
workingNode.parentNode.removeChild(workingNode);

return boundaryPosition;
return {
boundaryPosition: boundaryPosition,
nodeInfo: {
nodeIndex: nodeIndex,
containerElement: containerElement
}
};
}

// Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
Expand Down Expand Up @@ -569,17 +544,24 @@ rangy.createModule("WrappedRange", function(api, module) {
WrappedRange.prototype = new DomRange(document);

WrappedRange.prototype.refresh = function() {
var start, end;
var start, end, startBoundary;

// TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
var rangeContainerElement = getTextRangeContainerElement(this.textRange);

if (textRangeIsCollapsed(this.textRange)) {
end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
true).boundaryPosition;
} else {
log.warn("Refreshing Range from TextRange. parent element: " + dom.inspectNode(rangeContainerElement) + ", parentElement(): " + dom.inspectNode(this.textRange.parentElement()));
start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
log.debug("Refreshing Range from TextRange. parent element: " + dom.inspectNode(rangeContainerElement) + ", parentElement(): " + dom.inspectNode(this.textRange.parentElement()));
startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
start = startBoundary.boundaryPosition;

// An optimization used here is that if the start and end boundaries have teh same parent element, the
// search scope for the end boundary can be limited to exclude the portion of the element that precedes
// the start boundary
end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
startBoundary.nodeInfo).boundaryPosition;
}

this.setStart(start.node, start.offset);
Expand Down
23 changes: 7 additions & 16 deletions test/textrangeperformancetests.js
Expand Up @@ -3,12 +3,12 @@ rangy.config.preferTextRange = true;
xn.test.suite("Range miscellaneous", function(s) {
rangy.init();

var elementCount = 200;
var testCount = 10;
var elementCount = 1000;
var testCount = 20;

function setUp(t) {
t.testEl = document.createElement("div");
t.testEl.innerHTML = new Array(elementCount + 1).join("One<b>two</b>");
t.testEl.innerHTML = new Array(elementCount + 1).join("One two<b>three four</b>");
document.body.appendChild(t.testEl);
var textRange = document.body.createTextRange();
textRange.moveToElementText(t.testEl);
Expand All @@ -26,6 +26,10 @@ xn.test.suite("Range miscellaneous", function(s) {
textRange.collapse(true);
textRange.moveEnd("character", end);
textRange.moveStart("character", start);

if (Math.random() < 0.3) {
textRange.collapse(true);
}
t.textRanges[i] = textRange;
}
}
Expand All @@ -42,21 +46,8 @@ xn.test.suite("Range miscellaneous", function(s) {
}
}, setUp, tearDown);

s.test("TextRange to Range speed test (linear search)", function(t) {
rangy.config.useBinarySearch = false;
rangy.init();
//t.assertEquals(t.testEl.childNodes.length, 2 * elementCount);
for (var i = 0, len = t.textRanges.length, sel; i < len; ++i) {
t.textRanges[i].select();
sel = rangy.getSelection();
t.assertEquals(t.textRanges[i].text, sel.toString());
}
}, setUp, tearDown);

s.test("TextRange to Range speed test (binary search)", function(t) {
rangy.config.useBinarySearch = true;
rangy.init();
//t.assertEquals(t.testEl.childNodes.length, 2 * elementCount);
for (var i = 0, len = t.textRanges.length, sel; i < len; ++i) {
t.textRanges[i].select();
sel = rangy.getSelection();
Expand Down

0 comments on commit 3e0a970

Please sign in to comment.