etherpad-lite/static/js/virtual_lines.js
2012-01-22 09:54:28 -08:00

388 lines
11 KiB
JavaScript

/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function makeVirtualLineView(lineNode)
{
// how much to jump forward or backward at once in a charSeeker before
// constructing a DOM node and checking the coordinates (which takes a
// significant fraction of a millisecond). From the
// coordinates and the approximate line height we can estimate how
// many lines we have moved. We risk being off if the number of lines
// we move is on the order of the line height in pixels. Fortunately,
// when the user boosts the font-size they increase both.
var maxCharIncrement = 20;
var seekerAtEnd = null;
function getNumChars()
{
return lineNode.textContent.length;
}
function getNumVirtualLines()
{
if (!seekerAtEnd)
{
var seeker = makeCharSeeker();
seeker.forwardByWhile(maxCharIncrement);
seekerAtEnd = seeker;
}
return seekerAtEnd.getVirtualLine() + 1;
}
function getVLineAndOffsetForChar(lineChar)
{
var seeker = makeCharSeeker();
seeker.forwardByWhile(maxCharIncrement, null, lineChar);
var theLine = seeker.getVirtualLine();
seeker.backwardByWhile(8, function()
{
return seeker.getVirtualLine() == theLine;
});
seeker.forwardByWhile(1, function()
{
return seeker.getVirtualLine() != theLine;
});
var lineStartChar = seeker.getOffset();
return {
vline: theLine,
offset: (lineChar - lineStartChar)
};
}
function getCharForVLineAndOffset(vline, offset)
{
// returns revised vline and offset as well as absolute char index within line.
// if offset is beyond end of line, for example, will give new offset at end of line.
var seeker = makeCharSeeker();
// go to start of line
seeker.binarySearch(function()
{
return seeker.getVirtualLine() >= vline;
});
var lineStart = seeker.getOffset();
var theLine = seeker.getVirtualLine();
// go to offset, overshooting the virtual line only if offset is too large for it
seeker.forwardByWhile(maxCharIncrement, null, lineStart + offset);
// get back into line
seeker.backwardByWhile(1, function()
{
return seeker.getVirtualLine() != theLine;
}, lineStart);
var lineChar = seeker.getOffset();
var theOffset = lineChar - lineStart;
// handle case of last virtual line; should be able to be at end of it
if (theOffset < offset && theLine == (getNumVirtualLines() - 1))
{
var lineLen = getNumChars();
theOffset += lineLen - lineChar;
lineChar = lineLen;
}
return {
vline: theLine,
offset: theOffset,
lineChar: lineChar
};
}
return {
getNumVirtualLines: getNumVirtualLines,
getVLineAndOffsetForChar: getVLineAndOffsetForChar,
getCharForVLineAndOffset: getCharForVLineAndOffset,
makeCharSeeker: function()
{
return makeCharSeeker();
}
};
function deepFirstChildTextNode(nd)
{
nd = nd.firstChild;
while (nd && nd.firstChild) nd = nd.firstChild;
if (nd.data) return nd;
return null;
}
function makeCharSeeker( /*lineNode*/ )
{
function charCoords(tnode, i)
{
var container = tnode.parentNode;
// treat space specially; a space at the end of a virtual line
// will have weird coordinates
var isSpace = (tnode.nodeValue.charAt(i) === " ");
if (isSpace)
{
if (i == 0)
{
if (container.previousSibling && deepFirstChildTextNode(container.previousSibling))
{
tnode = deepFirstChildTextNode(container.previousSibling);
i = tnode.length - 1;
container = tnode.parentNode;
}
else
{
return {
top: container.offsetTop,
left: container.offsetLeft
};
}
}
else
{
i--; // use previous char
}
}
var charWrapper = document.createElement("SPAN");
// wrap the character
var tnodeText = tnode.nodeValue;
var frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode(tnodeText.substring(0, i)));
charWrapper.appendChild(document.createTextNode(tnodeText.substr(i, 1)));
frag.appendChild(charWrapper);
frag.appendChild(document.createTextNode(tnodeText.substring(i + 1)));
container.replaceChild(frag, tnode);
var result = {
top: charWrapper.offsetTop,
left: charWrapper.offsetLeft + (isSpace ? charWrapper.offsetWidth : 0),
height: charWrapper.offsetHeight
};
while (container.firstChild) container.removeChild(container.firstChild);
container.appendChild(tnode);
return result;
}
var lineText = lineNode.textContent;
var lineLength = lineText.length;
var curNode = null;
var curChar = 0;
var curCharWithinNode = 0
var curTop;
var curLeft;
var approxLineHeight;
var whichLine = 0;
function nextNode()
{
var n = curNode;
if (!n) n = lineNode.firstChild;
else n = n.nextSibling;
while (n && !deepFirstChildTextNode(n))
{
n = n.nextSibling;
}
return n;
}
function prevNode()
{
var n = curNode;
if (!n) n = lineNode.lastChild;
else n = n.previousSibling;
while (n && !deepFirstChildTextNode(n))
{
n = n.previousSibling;
}
return n;
}
var seeker;
if (lineLength > 0)
{
curNode = nextNode();
var firstCharData = charCoords(deepFirstChildTextNode(curNode), 0);
approxLineHeight = firstCharData.height;
curTop = firstCharData.top;
curLeft = firstCharData.left;
function updateCharData(tnode, i)
{
var coords = charCoords(tnode, i);
whichLine += Math.round((coords.top - curTop) / approxLineHeight);
curTop = coords.top;
curLeft = coords.left;
}
seeker = {
forward: function(numChars)
{
var oldChar = curChar;
var newChar = curChar + numChars;
if (newChar > (lineLength - 1)) newChar = lineLength - 1;
while (curChar < newChar)
{
var curNodeLength = deepFirstChildTextNode(curNode).length;
var toGo = curNodeLength - curCharWithinNode;
if (curChar + toGo > newChar || !nextNode())
{
// going to next node would be too far
var n = newChar - curChar;
if (n >= toGo) n = toGo - 1;
curChar += n;
curCharWithinNode += n;
break;
}
else
{
// go to next node
curChar += toGo;
curCharWithinNode = 0;
curNode = nextNode();
}
}
updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
return curChar - oldChar;
},
backward: function(numChars)
{
var oldChar = curChar;
var newChar = curChar - numChars;
if (newChar < 0) newChar = 0;
while (curChar > newChar)
{
if (curChar - curCharWithinNode <= newChar || !prevNode())
{
// going to prev node would be too far
var n = curChar - newChar;
if (n > curCharWithinNode) n = curCharWithinNode;
curChar -= n;
curCharWithinNode -= n;
break;
}
else
{
// go to prev node
curChar -= curCharWithinNode + 1;
curNode = prevNode();
curCharWithinNode = deepFirstChildTextNode(curNode).length - 1;
}
}
updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
return oldChar - curChar;
},
getVirtualLine: function()
{
return whichLine;
},
getLeftCoord: function()
{
return curLeft;
}
};
}
else
{
curLeft = lineNode.offsetLeft;
seeker = {
forward: function(numChars)
{
return 0;
},
backward: function(numChars)
{
return 0;
},
getVirtualLine: function()
{
return 0;
},
getLeftCoord: function()
{
return curLeft;
}
};
}
seeker.getOffset = function()
{
return curChar;
};
seeker.getLineLength = function()
{
return lineLength;
};
seeker.toString = function()
{
return "seeker[curChar: " + curChar + "(" + lineText.charAt(curChar) + "), left: " + seeker.getLeftCoord() + ", vline: " + seeker.getVirtualLine() + "]";
};
function moveByWhile(isBackward, amount, optCondFunc, optCharLimit)
{
var charsMovedLast = null;
var hasCondFunc = ((typeof optCondFunc) == "function");
var condFunc = optCondFunc;
var hasCharLimit = ((typeof optCharLimit) == "number");
var charLimit = optCharLimit;
while (charsMovedLast !== 0 && ((!hasCondFunc) || condFunc()))
{
var toMove = amount;
if (hasCharLimit)
{
var untilLimit = (isBackward ? curChar - charLimit : charLimit - curChar);
if (untilLimit < toMove) toMove = untilLimit;
}
if (toMove < 0) break;
charsMovedLast = (isBackward ? seeker.backward(toMove) : seeker.forward(toMove));
}
}
seeker.forwardByWhile = function(amount, optCondFunc, optCharLimit)
{
moveByWhile(false, amount, optCondFunc, optCharLimit);
}
seeker.backwardByWhile = function(amount, optCondFunc, optCharLimit)
{
moveByWhile(true, amount, optCondFunc, optCharLimit);
}
seeker.binarySearch = function(condFunc)
{
// returns index of boundary between false chars and true chars;
// positions seeker at first true char, or else last char
var trueFunc = condFunc;
var falseFunc = function()
{
return !condFunc();
};
seeker.forwardByWhile(20, falseFunc);
seeker.backwardByWhile(20, trueFunc);
seeker.forwardByWhile(10, falseFunc);
seeker.backwardByWhile(5, trueFunc);
seeker.forwardByWhile(1, falseFunc);
return seeker.getOffset() + (condFunc() ? 0 : 1);
}
return seeker;
}
}
exports.makeVirtualLineView = makeVirtualLineView;