diff options
author | Stefan <stefan@stefans-entwicklerecke.de> | 2018-02-03 12:57:42 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-03 12:57:42 +0100 |
commit | 89ad3cb809c9069325cebb30f67b887abf582028 (patch) | |
tree | 6a7bc87024e315846f8ae66d3da42968d9f9dd13 /src/static/js/caretPosition.js | |
parent | 32027134cbe4e37ced89091bf05e9fd07980ca12 (diff) | |
parent | 1e25e7fc773eb360b2fed52c39c3a14bc2a443ff (diff) | |
download | etherpad-lite-master.zip |
Diffstat (limited to 'src/static/js/caretPosition.js')
-rw-r--r-- | src/static/js/caretPosition.js | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/src/static/js/caretPosition.js b/src/static/js/caretPosition.js new file mode 100644 index 00000000..bc3fd007 --- /dev/null +++ b/src/static/js/caretPosition.js @@ -0,0 +1,241 @@ +// 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 <br>, + // 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 <br> 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; + } +} |