diff options
Diffstat (limited to 'src/static')
-rw-r--r-- | src/static/js/ace2_inner.js | 229 | ||||
-rw-r--r-- | src/static/js/caretPosition.js | 241 | ||||
-rw-r--r-- | src/static/js/scroll.js | 366 |
3 files changed, 680 insertions, 156 deletions
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); +} |