// One rep.line(div) can be broken in more than one line in the browser.
// This function is useful to get the caret position of the line as
// is represented by the browser
exports.getPosition = function ()
{
var rect, line;
var editor = $('#innerdocbody')[0];
var range = getSelectionRange();
var isSelectionInsideTheEditor = range && $(range.endContainer).closest('body')[0].id === 'innerdocbody';
if(isSelectionInsideTheEditor){
// when we have the caret in an empty line, e.g. a line with only a
,
// getBoundingClientRect() returns all dimensions value as 0
var selectionIsInTheBeginningOfLine = range.endOffset > 0;
if (selectionIsInTheBeginningOfLine) {
var clonedRange = createSelectionRange(range);
line = getPositionOfElementOrSelection(clonedRange);
clonedRange.detach()
}
// when there's a
or any element that has no height, we can't get
// the dimension of the element where the caret is
if(!rect || rect.height === 0){
var clonedRange = createSelectionRange(range);
// as we can't get the element height, we create a text node to get the dimensions
// on the position
var shadowCaret = $(document.createTextNode("|"));
clonedRange.insertNode(shadowCaret[0]);
clonedRange.selectNode(shadowCaret[0]);
line = getPositionOfElementOrSelection(clonedRange);
clonedRange.detach()
shadowCaret.remove();
}
}
return line;
}
var createSelectionRange = function (range) {
clonedRange = range.cloneRange();
// we set the selection start and end to avoid error when user selects a text bigger than
// the viewport height and uses the arrow keys to expand the selection. In this particular
// case is necessary to know where the selections ends because both edges of the selection
// is out of the viewport but we only use the end of it to calculate if it needs to scroll
clonedRange.setStart(range.endContainer, range.endOffset);
clonedRange.setEnd(range.endContainer, range.endOffset);
return clonedRange;
}
var getPositionOfRepLineAtOffset = function (node, offset) {
// it is not a text node, so we cannot make a selection
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
return getPositionOfElementOrSelection(node);
}
while (node.length === 0 && node.nextSibling) {
node = node.nextSibling;
}
var newRange = new Range();
newRange.setStart(node, offset);
newRange.setEnd(node, offset);
var linePosition = getPositionOfElementOrSelection(newRange);
newRange.detach(); // performance sake
return linePosition;
}
function getPositionOfElementOrSelection(element) {
var rect = element.getBoundingClientRect();
var linePosition = {
bottom: rect.bottom,
height: rect.height,
top: rect.top
}
return linePosition;
}
// here we have two possibilities:
// [1] the line before the caret line has the same type, so both of them has the same margin, padding
// height, etc. So, we can use the caret line to make calculation necessary to know where is the top
// of the previous line
// [2] the line before is part of another rep line. It's possible this line has different margins
// height. So we have to get the exactly position of the line
exports.getPositionTopOfPreviousBrowserLine = function(caretLinePosition, rep) {
var previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
var isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
// the caret is in the beginning of a rep line, so the previous browser line
// is the last line browser line of the a rep line
if (isCaretLineFirstBrowserLine) { //[2]
var lineBeforeCaretLine = rep.selStart[0] - 1;
var firstLineVisibleBeforeCaretLine = getPreviousVisibleLine(lineBeforeCaretLine, rep);
var linePosition = getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep);
previousLineTop = linePosition.top;
}
return previousLineTop;
}
function caretLineIsFirstBrowserLine(caretLineTop, rep)
{
var caretRepLine = rep.selStart[0];
var lineNode = rep.lines.atIndex(caretRepLine).lineNode;
var firstRootNode = getFirstRootChildNode(lineNode);
// to get the position of the node we get the position of the first char
var positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1);
return positionOfFirstRootNode.top === caretLineTop;
}
// find the first root node, usually it is a text node
function getFirstRootChildNode(node)
{
if(!node.firstChild){
return node;
}else{
return getFirstRootChildNode(node.firstChild);
}
}
function getPreviousVisibleLine(line, rep)
{
if (line < 0) {
return 0;
}else if (isLineVisible(line, rep)) {
return line;
}else{
return getPreviousVisibleLine(line - 1, rep);
}
}
function getDimensionOfLastBrowserLineOfRepLine(line, rep)
{
var lineNode = rep.lines.atIndex(line).lineNode;
var lastRootChildNode = getLastRootChildNode(lineNode);
// we get the position of the line in the last char of it
var lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition;
}
function getLastRootChildNode(node)
{
if(!node.lastChild){
return {
node: node,
length: node.length
};
}else{
return getLastRootChildNode(node.lastChild);
}
}
// here we have two possibilities:
// [1] The next line is part of the same rep line of the caret line, so we have the same dimensions.
// So, we can use the caret line to calculate the bottom of the line.
// [2] the next line is part of another rep line. It's possible this line has different dimensions, so we
// have to get the exactly dimension of it
exports.getBottomOfNextBrowserLine = function(caretLinePosition, rep)
{
var nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; //[1]
var isCaretLineLastBrowserLine = caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
// the caret is at the end of a rep line, so we can get the next browser line dimension
// using the position of the first char of the next rep line
if(isCaretLineLastBrowserLine){ //[2]
var nextLineAfterCaretLine = rep.selStart[0] + 1;
var firstNextLineVisibleAfterCaretLine = getNextVisibleLine(nextLineAfterCaretLine, rep);
var linePosition = getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep);
nextLineBottom = linePosition.bottom;
}
return nextLineBottom;
}
function caretLineIsLastBrowserLineOfRepLine(caretLineTop, rep)
{
var caretRepLine = rep.selStart[0];
var lineNode = rep.lines.atIndex(caretRepLine).lineNode;
var lastRootChildNode = getLastRootChildNode(lineNode);
// we take a rep line and get the position of the last char of it
var lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition.top === caretLineTop;
}
function getPreviousVisibleLine(line, rep)
{
var firstLineOfPad = 0;
if (line <= firstLineOfPad) {
return firstLineOfPad;
}else if (isLineVisible(line,rep)) {
return line;
}else{
return getPreviousVisibleLine(line - 1, rep);
}
}
exports.getPreviousVisibleLine = getPreviousVisibleLine;
function getNextVisibleLine(line, rep)
{
var lastLineOfThePad = rep.lines.length() - 1;
if (line >= lastLineOfThePad) {
return lastLineOfThePad;
}else if (isLineVisible(line,rep)) {
return line;
}else{
return getNextVisibleLine(line + 1, rep);
}
}
exports.getNextVisibleLine = getNextVisibleLine;
function isLineVisible(line, rep)
{
return rep.lines.atIndex(line).lineNode.offsetHeight > 0;
}
function getDimensionOfFirstBrowserLineOfRepLine(line, rep)
{
var lineNode = rep.lines.atIndex(line).lineNode;
var firstRootChildNode = getFirstRootChildNode(lineNode);
// we can get the position of the line, getting the position of the first char of the rep line
var firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1);
return firstRootChildNodePosition;
}
function getSelectionRange()
{
var selection;
if (!window.getSelection) {
return;
}
selection = window.getSelection();
if (selection.rangeCount > 0) {
return selection.getRangeAt(0);
} else {
return null;
}
}