summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/node/handler/PadMessageHandler.js9
-rw-r--r--src/node/utils/Settings.js27
-rw-r--r--src/static/js/ace2_inner.js229
-rw-r--r--src/static/js/caretPosition.js241
-rw-r--r--src/static/js/scroll.js366
5 files changed, 716 insertions, 156 deletions
diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
index b7ec7cb2..060bca7b 100644
--- a/src/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -1216,6 +1216,15 @@ function handleClientReady(client, message)
"parts": plugins.parts,
},
"indentationOnNewLine": settings.indentationOnNewLine,
+ "scrollWhenFocusLineIsOutOfViewport": {
+ "percentage" : {
+ "editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
+ "editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
+ },
+ "duration": settings.scrollWhenFocusLineIsOutOfViewport.duration,
+ "scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
+ "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
+ },
"initialChangesets": [] // FIXME: REMOVE THIS SHIT
}
diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js
index 660b7afb..cf7fea80 100644
--- a/src/node/utils/Settings.js
+++ b/src/node/utils/Settings.js
@@ -247,6 +247,33 @@ exports.users = {};
*/
exports.showSettingsInAdminPage = true;
+/*
+* By default, when caret is moved out of viewport, it scrolls the minimum height needed to make this
+* line visible.
+*/
+exports.scrollWhenFocusLineIsOutOfViewport = {
+ /*
+ * Percentage of viewport height to be additionally scrolled.
+ */
+ "percentage": {
+ "editionAboveViewport": 0,
+ "editionBelowViewport": 0
+ },
+ /*
+ * Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation
+ */
+ "duration": 0,
+ /*
+ * Flag to control if it should scroll when user places the caret in the last line of the viewport
+ */
+ /*
+ * Percentage of viewport height to be additionally scrolled when user presses arrow up
+ * in the line of the top of the viewport.
+ */
+ "percentageToScrollWhenUserPressesArrowUp": 0,
+ "scrollWhenCaretIsInTheLastLineOfViewport": false
+};
+
//checks if abiword is avaiable
exports.abiwordAvailable = function()
{
diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index 424bacf5..83f947ba 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -20,7 +20,6 @@
* limitations under the License.
*/
var _, $, jQuery, plugins, Ace2Common;
-
var browser = require('./browser');
if(browser.msie){
// Honestly fuck IE royally.
@@ -61,6 +60,7 @@ function Ace2Inner(){
var SkipList = require('./skiplist');
var undoModule = require('./undomodule').undoModule;
var AttributeManager = require('./AttributeManager');
+ var Scroll = require('./scroll');
var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
// changed to false
@@ -82,6 +82,7 @@ function Ace2Inner(){
var disposed = false;
var editorInfo = parent.editorInfo;
+
var iframe = window.frameElement;
var outerWin = iframe.ace_outerWin;
iframe.ace_outerWin = null; // prevent IE 6 memory leak
@@ -89,6 +90,8 @@ function Ace2Inner(){
var lineMetricsDiv = sideDiv.nextSibling;
initLineNumbers();
+ var scroll = Scroll.init(outerWin);
+
var outsideKeyDown = noop;
var outsideKeyPress = function(){return true;};
@@ -424,7 +427,7 @@ function Ace2Inner(){
var undoWorked = false;
try
{
- if (evt.eventType == "setup" || evt.eventType == "importText" || evt.eventType == "setBaseText")
+ if (isPadLoading(evt.eventType))
{
undoModule.clearHistory();
}
@@ -1208,7 +1211,7 @@ function Ace2Inner(){
updateLineNumbers(); // update line numbers if any time left
if (isTimeUp()) return;
- var visibleRange = getVisibleCharRange();
+ var visibleRange = scroll.getVisibleCharRange(rep);
var docRange = [0, rep.lines.totalWidth()];
//console.log("%o %o", docRange, visibleRange);
finishedImportantWork = true;
@@ -1670,7 +1673,7 @@ function Ace2Inner(){
});
//p.mark("relex");
- //rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; });
+ //rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; });
//var isTimeUp = newTimeLimit(100);
// do DOM inserts
p.mark("insert");
@@ -2914,6 +2917,15 @@ function Ace2Inner(){
documentAttributeManager: documentAttributeManager,
});
+ // we scroll when user places the caret at the last line of the pad
+ // when this settings is enabled
+ var docTextChanged = currentCallStack.docTextChanged;
+ if(!docTextChanged){
+ var isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type);
+ var innerHeight = getInnerHeight();
+ scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight);
+ }
+
return true;
//console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
//String(!!rep.selFocusAtStart));
@@ -2922,6 +2934,11 @@ function Ace2Inner(){
//console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
}
+ function isPadLoading(eventType)
+ {
+ return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText');
+ }
+
function doCreateDomLine(nonEmpty)
{
if (browser.msie && (!nonEmpty))
@@ -3277,50 +3294,36 @@ function Ace2Inner(){
return false;
}
- function getLineEntryTopBottom(entry, destObj)
- {
- var dom = entry.lineNode;
- var top = dom.offsetTop;
- var height = dom.offsetHeight;
- var obj = (destObj || {});
- obj.top = top;
- obj.bottom = (top + height);
- return obj;
- }
-
function getViewPortTopBottom()
{
- var theTop = getScrollY();
+ var theTop = scroll.getScrollY();
var doc = outerWin.document;
- var height = doc.documentElement.clientHeight;
+ var height = doc.documentElement.clientHeight; // includes padding
+
+ // we have to get the exactly height of the viewport. So it has to subtract all the values which changes
+ // the viewport height (E.g. padding, position top)
+ var viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable();
return {
top: theTop,
- bottom: (theTop + height)
+ bottom: (theTop + height - viewportExtraSpacesAndPosition)
};
}
- function getVisibleLineRange()
+
+ function getEditorPositionTop()
{
- var viewport = getViewPortTopBottom();
- //console.log("viewport top/bottom: %o", viewport);
- var obj = {};
- var start = rep.lines.search(function(e)
- {
- return getLineEntryTopBottom(e, obj).bottom > viewport.top;
- });
- var end = rep.lines.search(function(e)
- {
- return getLineEntryTopBottom(e, obj).top >= viewport.bottom;
- });
- if (end < start) end = start; // unlikely
- //console.log(start+","+end);
- return [start, end];
+ var editor = parent.document.getElementsByTagName('iframe');
+ var editorPositionTop = editor[0].offsetTop;
+ return editorPositionTop;
}
- function getVisibleCharRange()
+ // ep_page_view adds padding-top, which makes the viewport smaller
+ function getPaddingTopAddedWhenPageViewIsEnable()
{
- var lineRange = getVisibleLineRange();
- return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
+ var rootDocument = parent.parent.document;
+ var aceOuter = rootDocument.getElementsByName("ace_outer");
+ var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top"));
+ return aceOuterPaddingTop;
}
function handleCut(evt)
@@ -3966,12 +3969,12 @@ function Ace2Inner(){
doDeleteKey();
specialHandled = true;
}
- if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ setScrollY(0); } // Control Home send to Y = 0
+ if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ scroll.setScrollY(0); } // Control Home send to Y = 0
if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){
evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS
- var oldVisibleLineRange = getVisibleLineRange();
+ var oldVisibleLineRange = scroll.getVisibleLineRange(rep);
var topOffset = rep.selStart[0] - oldVisibleLineRange[0];
if(topOffset < 0 ){
topOffset = 0;
@@ -3981,7 +3984,7 @@ function Ace2Inner(){
var isPageUp = evt.which === 33;
scheduler.setTimeout(function(){
- var newVisibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
+ var newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10
var linesCount = rep.lines.length(); // total count of lines in pad IE 10
var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now?
@@ -4014,56 +4017,26 @@ function Ace2Inner(){
// sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
// so use focusNode.offsetTop value.
if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
- setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
+ scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
}, 200);
}
- /* Attempt to apply some sanity to cursor handling in Chrome after a copy / paste event
- We have to do this the way we do because rep. doesn't hold the value for keyheld events IE if the user
- presses and holds the arrow key .. Sorry if this is ugly, blame Chrome's weird handling of viewports after new content is added*/
- if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40) && browser.chrome){
- var viewport = getViewPortTopBottom();
- var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current
- var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
- var lineHeight = $(myselection.focusNode.parentNode).parent("div").height(); // get the line height of the caret line
- // top.console.log("offsetTop", myselection.focusNode.parentNode.parentNode.offsetTop);
- try {
- lineHeight = $(myselection.focusNode).height() // needed for how chrome handles line heights of null objects
- // console.log("lineHeight now", lineHeight);
- }catch(e){}
- var caretOffsetTopBottom = caretOffsetTop + lineHeight;
- var visibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
-
- if(caretOffsetTop){ // sometimes caretOffsetTop bugs out and returns 0, not sure why, possible Chrome bug? Either way if it does we don't wanna mess with it
- // top.console.log(caretOffsetTop, viewport.top, caretOffsetTopBottom, viewport.bottom);
- var caretIsNotVisible = (caretOffsetTop < viewport.top || caretOffsetTopBottom >= viewport.bottom); // Is the Caret Visible to the user?
- // Expect some weird behavior caretOffsetTopBottom is greater than viewport.bottom on a keypress down
- var offsetTopSamePlace = caretOffsetTop == viewport.top; // sometimes moving key left & up leaves the caret at the same point as the viewport.top, technically the caret is visible but it's not fully visible so we should move to it
- if(offsetTopSamePlace && (evt.which == 37 || evt.which == 38)){
- var newY = caretOffsetTop;
- setScrollY(newY);
- }
- if(caretIsNotVisible){ // is the cursor no longer visible to the user?
- // top.console.log("Caret is NOT visible to the user");
- // top.console.log(caretOffsetTop,viewport.top,caretOffsetTopBottom,viewport.bottom);
- // Oh boy the caret is out of the visible area, I need to scroll the browser window to lineNum.
- if(evt.which == 37 || evt.which == 38){ // If left or up arrow
- var newY = caretOffsetTop; // That was easy!
- }
- if(evt.which == 39 || evt.which == 40){ // if down or right arrow
- // only move the viewport if we're at the bottom of the viewport, if we hit down any other time the viewport shouldn't change
- // NOTE: This behavior only fires if Chrome decides to break the page layout after a paste, it's annoying but nothing I can do
- var selection = getSelection();
- // top.console.log("line #", rep.selStart[0]); // the line our caret is on
- // top.console.log("firstvisible", visibleLineRange[0]); // the first visiblel ine
- // top.console.log("lastVisible", visibleLineRange[1]); // the last visible line
- // top.console.log(rep.selStart[0], visibleLineRange[1], rep.selStart[0], visibleLineRange[0]);
- var newY = viewport.top + lineHeight;
- }
- if(newY){
- setScrollY(newY); // set the scrollY offset of the viewport on the document
- }
+ // scroll to viewport when user presses arrow keys and caret is out of the viewport
+ if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)){
+ // we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed
+ // this makes the scroll smooth
+ if(!continuouslyPressingArrowKey(type)){
+ // We use getSelection() instead of rep to get the caret position. This avoids errors like when
+ // the caret position is not synchronized with the rep. For example, when an user presses arrow
+ // down to scroll the pad without releasing the key. When the key is released the rep is not
+ // synchronized, so we don't get the right node where caret is.
+ var selection = getSelection();
+
+ if(selection){
+ var arrowUp = evt.which === 38;
+ var innerHeight = getInnerHeight();
+ scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight);
}
}
}
@@ -4121,6 +4094,19 @@ function Ace2Inner(){
var thisKeyDoesntTriggerNormalize = false;
+ var arrowKeyWasReleased = true;
+ function continuouslyPressingArrowKey(type) {
+ var firstTimeKeyIsContinuouslyPressed = false;
+
+ if (type == 'keyup') arrowKeyWasReleased = true;
+ else if (type == 'keydown' && arrowKeyWasReleased) {
+ firstTimeKeyIsContinuouslyPressed = true;
+ arrowKeyWasReleased = false;
+ }
+
+ return !firstTimeKeyIsContinuouslyPressed;
+ }
+
function doUndoRedo(which)
{
// precond: normalized DOM
@@ -4837,9 +4823,6 @@ function Ace2Inner(){
setIfNecessary(root.style, "height", "");
}
}
- // if near edge, scroll to edge
- var scrollX = getScrollX();
- var scrollY = getScrollY();
var win = outerWin;
var r = 20;
@@ -4848,52 +4831,6 @@ function Ace2Inner(){
$(sideDiv).addClass('sidedivdelayed');
}
- function getScrollXY()
- {
- var win = outerWin;
- var odoc = outerWin.document;
- if (typeof(win.pageYOffset) == "number")
- {
- return {
- x: win.pageXOffset,
- y: win.pageYOffset
- };
- }
- var docel = odoc.documentElement;
- if (docel && typeof(docel.scrollTop) == "number")
- {
- return {
- x: docel.scrollLeft,
- y: docel.scrollTop
- };
- }
- }
-
- function getScrollX()
- {
- return getScrollXY().x;
- }
-
- function getScrollY()
- {
- return getScrollXY().y;
- }
-
- function setScrollX(x)
- {
- outerWin.scrollTo(x, getScrollY());
- }
-
- function setScrollY(y)
- {
- outerWin.scrollTo(getScrollX(), y);
- }
-
- function setScrollXY(x, y)
- {
- outerWin.scrollTo(x, y);
- }
-
var _teardownActions = [];
function teardown()
@@ -5214,26 +5151,6 @@ function Ace2Inner(){
return odoc.documentElement.clientWidth;
}
- function scrollNodeVerticallyIntoView(node)
- {
- // requires element (non-text) node;
- // if node extends above top of viewport or below bottom of viewport (or top of scrollbar),
- // scroll it the minimum distance needed to be completely in view.
- var win = outerWin;
- var odoc = outerWin.document;
- var distBelowTop = node.offsetTop + iframePadTop - win.scrollY;
- var distAboveBottom = win.scrollY + getInnerHeight() - (node.offsetTop + iframePadTop + node.offsetHeight);
-
- if (distBelowTop < 0)
- {
- win.scrollBy(0, distBelowTop);
- }
- else if (distAboveBottom < 0)
- {
- win.scrollBy(0, -distAboveBottom);
- }
- }
-
function scrollXHorizontallyIntoView(pixelX)
{
var win = outerWin;
@@ -5255,8 +5172,8 @@ function Ace2Inner(){
{
if (!rep.selStart) return;
fixView();
- var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
- scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode);
+ var innerHeight = getInnerHeight();
+ scroll.scrollNodeVerticallyIntoView(rep, innerHeight);
if (!doesWrap)
{
var browserSelection = getSelection();
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;
+ }
+}
diff --git a/src/static/js/scroll.js b/src/static/js/scroll.js
new file mode 100644
index 00000000..a53dc38c
--- /dev/null
+++ b/src/static/js/scroll.js
@@ -0,0 +1,366 @@
+/*
+ This file handles scroll on edition or when user presses arrow keys.
+ In this file we have two representations of line (browser and rep line).
+ Rep Line = a line in the way is represented by Etherpad(rep) (each <div> is a line)
+ Browser Line = each vertical line. A <div> can be break into more than one
+ browser line.
+*/
+var caretPosition = require('/caretPosition');
+
+function Scroll(outerWin) {
+ // scroll settings
+ this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport;
+
+ // DOM reference
+ this.outerWin = outerWin;
+ this.doc = this.outerWin.document;
+ this.rootDocument = parent.parent.document;
+}
+
+Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary = function (rep, isScrollableEvent, innerHeight)
+{
+ // are we placing the caret on the line at the bottom of viewport?
+ // And if so, do we need to scroll the editor, as defined on the settings.json?
+ var shouldScrollWhenCaretIsAtBottomOfViewport = this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
+ if (shouldScrollWhenCaretIsAtBottomOfViewport) {
+ // avoid scrolling when selection includes multiple lines -- user can potentially be selecting more lines
+ // than it fits on viewport
+ var multipleLinesSelected = rep.selStart[0] !== rep.selEnd[0];
+
+ // avoid scrolling when pad loads
+ if (isScrollableEvent && !multipleLinesSelected && this._isCaretAtTheBottomOfViewport(rep)) {
+ // when scrollWhenFocusLineIsOutOfViewport.percentage is 0, pixelsToScroll is 0
+ var pixelsToScroll = this._getPixelsRelativeToPercentageOfViewport(innerHeight);
+ this._scrollYPage(pixelsToScroll);
+ }
+ }
+}
+
+Scroll.prototype.scrollWhenPressArrowKeys = function(arrowUp, rep, innerHeight)
+{
+ // if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous
+ // rep line on the top of the viewport
+ if(this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)){
+ var pixelsToScroll = this._getPixelsToScrollWhenUserPressesArrowUp(innerHeight);
+
+ // by default, the browser scrolls to the middle of the viewport. To avoid the twist made
+ // when we apply a second scroll, we made it immediately (without animation)
+ this._scrollYPageWithoutAnimation(-pixelsToScroll);
+ }else{
+ this.scrollNodeVerticallyIntoView(rep, innerHeight);
+ }
+}
+
+// Some plugins might set a minimum height to the editor (ex: ep_page_view), so checking
+// if (caretLine() === rep.lines.length() - 1) is not enough. We need to check if there are
+// other lines after caretLine(), and all of them are out of viewport.
+Scroll.prototype._isCaretAtTheBottomOfViewport = function(rep)
+{
+ // computing a line position using getBoundingClientRect() is expensive.
+ // (obs: getBoundingClientRect() is called on caretPosition.getPosition())
+ // To avoid that, we only call this function when it is possible that the
+ // caret is in the bottom of viewport
+ var caretLine = rep.selStart[0];
+ var lineAfterCaretLine = caretLine + 1;
+ var firstLineVisibleAfterCaretLine = caretPosition.getNextVisibleLine(lineAfterCaretLine, rep);
+ var caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
+ var lineAfterCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
+ if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
+ // check if the caret is in the bottom of the viewport
+ var caretLinePosition = caretPosition.getPosition();
+ var viewportBottom = this._getViewPortTopBottom().bottom;
+ var nextLineBottom = caretPosition.getBottomOfNextBrowserLine(caretLinePosition, rep);
+ var nextLineIsBelowViewportBottom = nextLineBottom > viewportBottom;
+ return nextLineIsBelowViewportBottom;
+ }
+ return false;
+}
+
+Scroll.prototype._isLinePartiallyVisibleOnViewport = function(lineNumber, rep)
+{
+ var lineNode = rep.lines.atIndex(lineNumber);
+ var linePosition = this._getLineEntryTopBottom(lineNode);
+ var lineTop = linePosition.top;
+ var lineBottom = linePosition.bottom;
+ var viewport = this._getViewPortTopBottom();
+ var viewportBottom = viewport.bottom;
+ var viewportTop = viewport.top;
+
+ var topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
+ var bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
+ var topOfLineIsBelowViewportTop = lineTop >= viewportTop;
+ var topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
+ var bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
+ var bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
+
+ return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
+ (topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
+ (bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
+}
+
+Scroll.prototype._getViewPortTopBottom = function()
+{
+ var theTop = this.getScrollY();
+ var doc = this.doc;
+ var height = doc.documentElement.clientHeight; // includes padding
+
+ // we have to get the exactly height of the viewport. So it has to subtract all the values which changes
+ // the viewport height (E.g. padding, position top)
+ var viewportExtraSpacesAndPosition = this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
+ return {
+ top: theTop,
+ bottom: (theTop + height - viewportExtraSpacesAndPosition)
+ };
+}
+
+Scroll.prototype._getEditorPositionTop = function()
+{
+ var editor = parent.document.getElementsByTagName('iframe');
+ var editorPositionTop = editor[0].offsetTop;
+ return editorPositionTop;
+}
+
+// ep_page_view adds padding-top, which makes the viewport smaller
+Scroll.prototype._getPaddingTopAddedWhenPageViewIsEnable = function()
+{
+ var aceOuter = this.rootDocument.getElementsByName("ace_outer");
+ var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top"));
+ return aceOuterPaddingTop;
+}
+
+Scroll.prototype._getScrollXY = function()
+{
+ var win = this.outerWin;
+ var odoc = this.doc;
+ if (typeof(win.pageYOffset) == "number")
+ {
+ return {
+ x: win.pageXOffset,
+ y: win.pageYOffset
+ };
+ }
+ var docel = odoc.documentElement;
+ if (docel && typeof(docel.scrollTop) == "number")
+ {
+ return {
+ x: docel.scrollLeft,
+ y: docel.scrollTop
+ };
+ }
+}
+
+Scroll.prototype.getScrollX = function()
+{
+ return this._getScrollXY().x;
+}
+
+Scroll.prototype.getScrollY = function()
+{
+ return this._getScrollXY().y;
+}
+
+Scroll.prototype.setScrollX = function(x)
+{
+ this.outerWin.scrollTo(x, this.getScrollY());
+}
+
+Scroll.prototype.setScrollY = function(y)
+{
+ this.outerWin.scrollTo(this.getScrollX(), y);
+}
+
+Scroll.prototype.setScrollXY = function(x, y)
+{
+ this.outerWin.scrollTo(x, y);
+}
+
+Scroll.prototype._isCaretAtTheTopOfViewport = function(rep)
+{
+ var caretLine = rep.selStart[0];
+ var linePrevCaretLine = caretLine - 1;
+ var firstLineVisibleBeforeCaretLine = caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep);
+ var caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
+ var lineBeforeCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
+ if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
+ var caretLinePosition = caretPosition.getPosition(); // get the position of the browser line
+ var viewportPosition = this._getViewPortTopBottom();
+ var viewportTop = viewportPosition.top;
+ var viewportBottom = viewportPosition.bottom;
+ var caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop;
+ var caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom;
+ var caretLineIsInsideOfViewport = caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
+ if (caretLineIsInsideOfViewport) {
+ var prevLineTop = caretPosition.getPositionTopOfPreviousBrowserLine(caretLinePosition, rep);
+ var previousLineIsAboveViewportTop = prevLineTop < viewportTop;
+ return previousLineIsAboveViewportTop;
+ }
+ }
+ return false;
+}
+
+// By default, when user makes an edition in a line out of viewport, this line goes
+// to the edge of viewport. This function gets the extra pixels necessary to get the
+// caret line in a position X relative to Y% viewport.
+Scroll.prototype._getPixelsRelativeToPercentageOfViewport = function(innerHeight, aboveOfViewport)
+{
+ var pixels = 0;
+ var scrollPercentageRelativeToViewport = this._getPercentageToScroll(aboveOfViewport);
+ if(scrollPercentageRelativeToViewport > 0 && scrollPercentageRelativeToViewport <= 1){
+ pixels = parseInt(innerHeight * scrollPercentageRelativeToViewport);
+ }
+ return pixels;
+}
+
+// we use different percentages when change selection. It depends on if it is
+// either above the top or below the bottom of the page
+Scroll.prototype._getPercentageToScroll = function(aboveOfViewport)
+{
+ var percentageToScroll = this.scrollSettings.percentage.editionBelowViewport;
+ if(aboveOfViewport){
+ percentageToScroll = this.scrollSettings.percentage.editionAboveViewport;
+ }
+ return percentageToScroll;
+}
+
+Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function(innerHeight)
+{
+ var pixels = 0;
+ var percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
+ if(percentageToScrollUp > 0 && percentageToScrollUp <= 1){
+ pixels = parseInt(innerHeight * percentageToScrollUp);
+ }
+ return pixels;
+}
+
+Scroll.prototype._scrollYPage = function(pixelsToScroll)
+{
+ var durationOfAnimationToShowFocusline = this.scrollSettings.duration;
+ if(durationOfAnimationToShowFocusline){
+ this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
+ }else{
+ this._scrollYPageWithoutAnimation(pixelsToScroll);
+ }
+}
+
+Scroll.prototype._scrollYPageWithoutAnimation = function(pixelsToScroll)
+{
+ this.outerWin.scrollBy(0, pixelsToScroll);
+}
+
+Scroll.prototype._scrollYPageWithAnimation = function(pixelsToScroll, durationOfAnimationToShowFocusline)
+{
+ var outerDocBody = this.doc.getElementById("outerdocbody");
+
+ // it works on later versions of Chrome
+ var $outerDocBody = $(outerDocBody);
+ this._triggerScrollWithAnimation($outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
+
+ // it works on Firefox and earlier versions of Chrome
+ var $outerDocBodyParent = $outerDocBody.parent();
+ this._triggerScrollWithAnimation($outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
+}
+
+// using a custom queue and clearing it, we avoid creating a queue of scroll animations. So if this function
+// is called twice quickly, only the last one runs.
+Scroll.prototype._triggerScrollWithAnimation = function($elem, pixelsToScroll, durationOfAnimationToShowFocusline)
+{
+ // clear the queue of animation
+ $elem.stop("scrollanimation");
+ $elem.animate({
+ scrollTop: '+=' + pixelsToScroll
+ }, {
+ duration: durationOfAnimationToShowFocusline,
+ queue: "scrollanimation"
+ }).dequeue("scrollanimation");
+}
+
+// scrollAmountWhenFocusLineIsOutOfViewport is set to 0 (default), scroll it the minimum distance
+// needed to be completely in view. If the value is greater than 0 and less than or equal to 1,
+// besides of scrolling the minimum needed to be visible, it scrolls additionally
+// (viewport height * scrollAmountWhenFocusLineIsOutOfViewport) pixels
+Scroll.prototype.scrollNodeVerticallyIntoView = function(rep, innerHeight)
+{
+ var viewport = this._getViewPortTopBottom();
+ var isPartOfRepLineOutOfViewport = this._partOfRepLineIsOutOfViewport(viewport, rep);
+
+ // when the selection changes outside of the viewport the browser automatically scrolls the line
+ // to inside of the viewport. Tested on IE, Firefox, Chrome in releases from 2015 until now
+ // So, when the line scrolled gets outside of the viewport we let the browser handle it.
+ var linePosition = caretPosition.getPosition();
+ if(linePosition){
+ var distanceOfTopOfViewport = linePosition.top - viewport.top;
+ var distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom;
+ var caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
+ var caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
+ if(caretIsAboveOfViewport){
+ var pixelsToScroll = distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
+ this._scrollYPage(pixelsToScroll);
+ }else if(caretIsBelowOfViewport){
+ var pixelsToScroll = -distanceOfBottomOfViewport + this._getPixelsRelativeToPercentageOfViewport(innerHeight);
+ this._scrollYPage(pixelsToScroll);
+ }else{
+ this.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, true, innerHeight);
+ }
+ }
+}
+
+Scroll.prototype._partOfRepLineIsOutOfViewport = function(viewportPosition, rep)
+{
+ var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
+ var line = rep.lines.atIndex(focusLine);
+ var linePosition = this._getLineEntryTopBottom(line);
+ var lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
+ var lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
+
+ return lineIsBelowOfViewport || lineIsAboveOfViewport;
+}
+
+Scroll.prototype._getLineEntryTopBottom = function(entry, destObj)
+{
+ var dom = entry.lineNode;
+ var top = dom.offsetTop;
+ var height = dom.offsetHeight;
+ var obj = (destObj || {});
+ obj.top = top;
+ obj.bottom = (top + height);
+ return obj;
+}
+
+Scroll.prototype._arrowUpWasPressedInTheFirstLineOfTheViewport = function(arrowUp, rep)
+{
+ var percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
+ return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
+}
+
+Scroll.prototype.getVisibleLineRange = function(rep)
+{
+ var viewport = this._getViewPortTopBottom();
+ //console.log("viewport top/bottom: %o", viewport);
+ var obj = {};
+ var self = this;
+ var start = rep.lines.search(function(e)
+ {
+ return self._getLineEntryTopBottom(e, obj).bottom > viewport.top;
+ });
+ var end = rep.lines.search(function(e)
+ {
+ // return the first line that the top position is greater or equal than
+ // the viewport. That is the first line that is below the viewport bottom.
+ // So the line that is in the bottom of the viewport is the very previous one.
+ return self._getLineEntryTopBottom(e, obj).top >= viewport.bottom;
+ });
+ if (end < start) end = start; // unlikely
+ // top.console.log(start+","+(end -1));
+ return [start, end - 1];
+}
+
+Scroll.prototype.getVisibleCharRange = function(rep)
+{
+ var lineRange = this.getVisibleLineRange(rep);
+ return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
+}
+
+exports.init = function(outerWin)
+{
+ return new Scroll(outerWin);
+}