From 865f2e565a830ae540080e6625cda096c799be9c Mon Sep 17 00:00:00 2001
From: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
Date: Sat, 13 Jul 2024 21:13:09 +0200
Subject: [PATCH] Moved first js files to ts
---
pnpm-lock.yaml | 15 +
src/package.json | 1 +
src/static/js/ace2_inner.js | 5 +-
.../js/{caretPosition.js => caretPosition.ts} | 12 +-
src/static/js/scroll.js | 351 ------------------
src/static/js/scroll.ts | 337 +++++++++++++++++
6 files changed, 361 insertions(+), 360 deletions(-)
rename src/static/js/{caretPosition.js => caretPosition.ts} (95%)
delete mode 100644 src/static/js/scroll.js
create mode 100644 src/static/js/scroll.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a20d3427d..09b4a8bc7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -294,6 +294,9 @@ importers:
'@types/http-errors':
specifier: ^2.0.4
version: 2.0.4
+ '@types/jquery':
+ specifier: ^3.5.30
+ version: 3.5.30
'@types/jsdom':
specifier: ^21.1.7
version: 21.1.7
@@ -1485,6 +1488,9 @@ packages:
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
+ '@types/jquery@3.5.30':
+ resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==}
+
'@types/jsdom@21.1.7':
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
@@ -1572,6 +1578,9 @@ packages:
'@types/sinonjs__fake-timers@8.1.5':
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
+ '@types/sizzle@2.3.8':
+ resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==}
+
'@types/superagent@8.1.7':
resolution: {integrity: sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==}
@@ -5448,6 +5457,10 @@ snapshots:
'@types/http-errors@2.0.4': {}
+ '@types/jquery@3.5.30':
+ dependencies:
+ '@types/sizzle': 2.3.8
+
'@types/jsdom@21.1.7':
dependencies:
'@types/node': 20.14.10
@@ -5550,6 +5563,8 @@ snapshots:
'@types/sinonjs__fake-timers@8.1.5': {}
+ '@types/sizzle@2.3.8': {}
+
'@types/superagent@8.1.7':
dependencies:
'@types/cookiejar': 2.1.5
diff --git a/src/package.json b/src/package.json
index 6b19c014c..ab49c1ae7 100644
--- a/src/package.json
+++ b/src/package.json
@@ -87,6 +87,7 @@
"@types/express": "^4.17.21",
"@types/formidable": "^3.4.5",
"@types/http-errors": "^2.0.4",
+ "@types/jquery": "^3.5.30",
"@types/jsdom": "^21.1.7",
"@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.7",
diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index f62615da2..f2966203c 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -30,6 +30,8 @@ const setAssoc = Ace2Common.setAssoc;
const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks');
+import Scroll from './scroll'
+
function Ace2Inner(editorInfo, cssManagers) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
@@ -42,7 +44,6 @@ function Ace2Inner(editorInfo, cssManagers) {
const SkipList = require('./skiplist');
const undoModule = require('./undomodule').undoModule;
const AttributeManager = require('./AttributeManager');
- const Scroll = require('./scroll');
const DEBUG = false;
const THE_TAB = ' '; // 4
@@ -77,7 +78,7 @@ function Ace2Inner(editorInfo, cssManagers) {
};
appendNewSideDivLine();
- const scroll = Scroll.init(outerWin);
+ const scroll = new Scroll(outerWin);
let outsideKeyDown = noop;
let outsideKeyPress = (e) => true;
diff --git a/src/static/js/caretPosition.js b/src/static/js/caretPosition.ts
similarity index 95%
rename from src/static/js/caretPosition.js
rename to src/static/js/caretPosition.ts
index 2814da74a..23e956d30 100644
--- a/src/static/js/caretPosition.js
+++ b/src/static/js/caretPosition.ts
@@ -3,7 +3,7 @@
// 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 = () => {
+export const getPosition = () => {
const range = getSelectionRange();
console.log("Getting range", range)
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
@@ -65,7 +65,7 @@ const getPositionOfElementOrSelection = (element) => {
// 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 = (caretLinePosition, rep) => {
+export const getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
@@ -126,7 +126,7 @@ const getLastRootChildNode = (node) => {
// 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 = (caretLinePosition, rep) => {
+export const getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
const isCaretLineLastBrowserLine =
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
@@ -154,7 +154,7 @@ const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
return lastRootChildNodePosition.top === caretLineTop;
};
-const getPreviousVisibleLine = (line, rep) => {
+export const getPreviousVisibleLine = (line, rep) => {
const firstLineOfPad = 0;
if (line <= firstLineOfPad) {
return firstLineOfPad;
@@ -166,9 +166,8 @@ const getPreviousVisibleLine = (line, rep) => {
};
-exports.getPreviousVisibleLine = getPreviousVisibleLine;
-const getNextVisibleLine = (line, rep) => {
+export const getNextVisibleLine = (line, rep) => {
const lastLineOfThePad = rep.lines.length() - 1;
if (line >= lastLineOfThePad) {
return lastLineOfThePad;
@@ -178,7 +177,6 @@ const getNextVisibleLine = (line, rep) => {
return getNextVisibleLine(line + 1, rep);
}
};
-exports.getNextVisibleLine = getNextVisibleLine;
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
diff --git a/src/static/js/scroll.js b/src/static/js/scroll.js
deleted file mode 100644
index 6614a1973..000000000
--- a/src/static/js/scroll.js
+++ /dev/null
@@ -1,351 +0,0 @@
-'use strict';
-
-/*
- 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
is a line)
- Browser Line = each vertical line. A
can be break into more than one
- browser line.
-*/
-const caretPosition = require('./caretPosition');
-
-function Scroll(outerWin) {
- // scroll settings
- this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport;
-
- // DOM reference
- this.outerWin = outerWin;
- this.doc = this.outerWin.contentDocument;
- 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?
- const 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
- const 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
- const 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)) {
- const 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
- const caretLine = rep.selStart[0];
- const lineAfterCaretLine = caretLine + 1;
- const firstLineVisibleAfterCaretLine = caretPosition.getNextVisibleLine(lineAfterCaretLine, rep);
- const caretLineIsPartiallyVisibleOnViewport =
- this._isLinePartiallyVisibleOnViewport(caretLine, rep);
- const lineAfterCaretLineIsPartiallyVisibleOnViewport =
- this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
- if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
- // check if the caret is in the bottom of the viewport
- const caretLinePosition = caretPosition.getPosition();
- const viewportBottom = this._getViewPortTopBottom().bottom;
- const nextLineBottom = caretPosition.getBottomOfNextBrowserLine(caretLinePosition, rep);
- const nextLineIsBelowViewportBottom = nextLineBottom > viewportBottom;
- return nextLineIsBelowViewportBottom;
- }
- return false;
-};
-
-Scroll.prototype._isLinePartiallyVisibleOnViewport = function (lineNumber, rep) {
- const lineNode = rep.lines.atIndex(lineNumber);
- const linePosition = this._getLineEntryTopBottom(lineNode);
- const lineTop = linePosition.top;
- const lineBottom = linePosition.bottom;
- const viewport = this._getViewPortTopBottom();
- const viewportBottom = viewport.bottom;
- const viewportTop = viewport.top;
-
- const topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
- const bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
- const topOfLineIsBelowViewportTop = lineTop >= viewportTop;
- const topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
- const bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
- const bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
-
- return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
- (topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
- (bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
-};
-
-Scroll.prototype._getViewPortTopBottom = function () {
- const theTop = this.getScrollY();
- const doc = this.doc;
- const 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)
- const viewportExtraSpacesAndPosition =
- this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
- return {
- top: theTop,
- bottom: (theTop + height - viewportExtraSpacesAndPosition),
- };
-};
-
-Scroll.prototype._getEditorPositionTop = function () {
- const editor = parent.document.getElementsByTagName('iframe');
- const editorPositionTop = editor[0].offsetTop;
- return editorPositionTop;
-};
-
-// ep_page_view adds padding-top, which makes the viewport smaller
-Scroll.prototype._getPaddingTopAddedWhenPageViewIsEnable = function () {
- const aceOuter = this.rootDocument.getElementsByName('ace_outer');
- const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top'));
- return aceOuterPaddingTop;
-};
-
-Scroll.prototype._getScrollXY = function () {
- const win = this.outerWin;
- const odoc = this.doc;
- if (typeof (win.pageYOffset) === 'number') {
- return {
- x: win.pageXOffset,
- y: win.pageYOffset,
- };
- }
- const 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) {
- const caretLine = rep.selStart[0];
- const linePrevCaretLine = caretLine - 1;
- const firstLineVisibleBeforeCaretLine =
- caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep);
- const caretLineIsPartiallyVisibleOnViewport =
- this._isLinePartiallyVisibleOnViewport(caretLine, rep);
- const lineBeforeCaretLineIsPartiallyVisibleOnViewport =
- this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
- if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
- const caretLinePosition = caretPosition.getPosition(); // get the position of the browser line
- const viewportPosition = this._getViewPortTopBottom();
- const viewportTop = viewportPosition.top;
- const viewportBottom = viewportPosition.bottom;
- const caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop;
- const caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom;
- const caretLineIsInsideOfViewport =
- caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
- if (caretLineIsInsideOfViewport) {
- const prevLineTop = caretPosition.getPositionTopOfPreviousBrowserLine(caretLinePosition, rep);
- const 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) {
- let pixels = 0;
- const 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) {
- let percentageToScroll = this.scrollSettings.percentage.editionBelowViewport;
- if (aboveOfViewport) {
- percentageToScroll = this.scrollSettings.percentage.editionAboveViewport;
- }
- return percentageToScroll;
-};
-
-Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeight) {
- let pixels = 0;
- const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
- if (percentageToScrollUp > 0 && percentageToScrollUp <= 1) {
- pixels = parseInt(innerHeight * percentageToScrollUp);
- }
- return pixels;
-};
-
-Scroll.prototype._scrollYPage = function (pixelsToScroll) {
- const 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) {
- const outerDocBody = this.doc.getElementById('outerdocbody');
-
- // it works on later versions of Chrome
- const $outerDocBody = $(outerDocBody);
- this._triggerScrollWithAnimation(
- $outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
-
- // it works on Firefox and earlier versions of Chrome
- const $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) {
- const viewport = this._getViewPortTopBottom();
-
- // 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.
- const linePosition = caretPosition.getPosition();
- if (linePosition) {
- const distanceOfTopOfViewport = linePosition.top - viewport.top;
- const distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom - linePosition.height;
- const caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
- const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
- if (caretIsAboveOfViewport) {
- const pixelsToScroll =
- distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
- this._scrollYPage(pixelsToScroll);
- } else if (caretIsBelowOfViewport) {
- // setTimeout is required here as line might not be fully rendered onto the pad
- setTimeout(() => {
- const outer = window.parent;
- // scroll to the very end of the pad outer
- outer.scrollTo(0, outer[0].innerHeight);
- }, 150);
- // if the above setTimeout and functionality is removed then hitting an enter
- // key while on the last line wont be an optimal user experience
- // Details at: https://github.com/ether/etherpad-lite/pull/4639/files
- }
- }
-};
-
-Scroll.prototype._partOfRepLineIsOutOfViewport = function (viewportPosition, rep) {
- const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
- const line = rep.lines.atIndex(focusLine);
- const linePosition = this._getLineEntryTopBottom(line);
- const lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
- const lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
-
- return lineIsBelowOfViewport || lineIsAboveOfViewport;
-};
-
-Scroll.prototype._getLineEntryTopBottom = function (entry, destObj) {
- const dom = entry.lineNode;
- const top = dom.offsetTop;
- const height = dom.offsetHeight;
- const obj = (destObj || {});
- obj.top = top;
- obj.bottom = (top + height);
- return obj;
-};
-
-Scroll.prototype._arrowUpWasPressedInTheFirstLineOfTheViewport = function (arrowUp, rep) {
- const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
- return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
-};
-
-Scroll.prototype.getVisibleLineRange = function (rep) {
- const viewport = this._getViewPortTopBottom();
- // console.log("viewport top/bottom: %o", viewport);
- const obj = {};
- const self = this;
- const start = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).bottom > viewport.top);
- // 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.
- let end = rep.lines.search((e) => 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) {
- const lineRange = this.getVisibleLineRange(rep);
- return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
-};
-
-exports.init = (outerWin) => new Scroll(outerWin);
diff --git a/src/static/js/scroll.ts b/src/static/js/scroll.ts
new file mode 100644
index 000000000..0d2dac44c
--- /dev/null
+++ b/src/static/js/scroll.ts
@@ -0,0 +1,337 @@
+import {getBottomOfNextBrowserLine, getNextVisibleLine, getPosition, getPositionTopOfPreviousBrowserLine, getPreviousVisibleLine} from './caretPosition';
+
+
+class Scroll {
+ private readonly outerWin: HTMLIFrameElement;
+ private readonly doc: Document;
+ private rootDocument: Document;
+ private scrollSettings: any;
+
+ constructor(outerWin: HTMLIFrameElement) {
+ this.scrollSettings = window.clientVars.scrollWhenFocusLineIsOutOfViewport;
+
+ // DOM reference
+ this.outerWin = outerWin;
+ this.doc = this.outerWin.contentDocument!;
+ this.rootDocument = parent.parent.document;
+ }
+
+ scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(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?
+ const 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
+ const 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
+ const pixelsToScroll = this._getPixelsRelativeToPercentageOfViewport(innerHeight);
+ this._scrollYPage(pixelsToScroll);
+ }
+ }
+ }
+
+ scrollWhenPressArrowKeys(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)) {
+ const 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);
+ }
+ }
+
+ _isCaretAtTheBottomOfViewport(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
+ const caretLine = rep.selStart[0];
+ const lineAfterCaretLine = caretLine + 1;
+ const firstLineVisibleAfterCaretLine = getNextVisibleLine(lineAfterCaretLine, rep);
+ const caretLineIsPartiallyVisibleOnViewport =
+ this._isLinePartiallyVisibleOnViewport(caretLine, rep);
+ const lineAfterCaretLineIsPartiallyVisibleOnViewport =
+ this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
+ if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
+ // check if the caret is in the bottom of the viewport
+ const caretLinePosition = getPosition();
+ const viewportBottom = this._getViewPortTopBottom().bottom;
+ const nextLineBottom = getBottomOfNextBrowserLine(caretLinePosition, rep);
+ const nextLineIsBelowViewportBottom = nextLineBottom > viewportBottom;
+ return nextLineIsBelowViewportBottom;
+ }
+ return false;
+ };
+
+ _isLinePartiallyVisibleOnViewport(lineNumber, rep){
+ const lineNode = rep.lines.atIndex(lineNumber);
+ const linePosition = this._getLineEntryTopBottom(lineNode);
+ const lineTop = linePosition.top;
+ const lineBottom = linePosition.bottom;
+ const viewport = this._getViewPortTopBottom();
+ const viewportBottom = viewport.bottom;
+ const viewportTop = viewport.top;
+
+ const topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
+ const bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
+ const topOfLineIsBelowViewportTop = lineTop >= viewportTop;
+ const topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
+ const bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
+ const bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
+
+ return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
+ (topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
+ (bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
+ };
+
+ _getViewPortTopBottom() {
+ const theTop = this.getScrollY();
+ const doc = this.doc;
+ const 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)
+ const viewportExtraSpacesAndPosition =
+ this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
+ return {
+ top: theTop,
+ bottom: (theTop + height - viewportExtraSpacesAndPosition),
+ };
+ };
+
+ _getEditorPositionTop() {
+ const editor = parent.document.getElementsByTagName('iframe');
+ const editorPositionTop = editor[0].offsetTop;
+ return editorPositionTop;
+ };
+
+ _getPaddingTopAddedWhenPageViewIsEnable() {
+ const aceOuter = this.rootDocument.getElementsByName('ace_outer');
+ const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top'));
+ return aceOuterPaddingTop;
+ };
+
+ _getScrollXY() {
+ const win = this.outerWin;
+ const odoc = this.doc;
+ if (typeof (win.pageYOffset) === 'number') {
+ return {
+ x: win.pageXOffset,
+ y: win.pageYOffset,
+ };
+ }
+ const docel = odoc.documentElement;
+ if (docel && typeof (docel.scrollTop) === 'number') {
+ return {
+ x: docel.scrollLeft,
+ y: docel.scrollTop,
+ };
+ }
+ };
+
+ getScrollX() {
+ return this._getScrollXY().x;
+ };
+
+ getScrollY () {
+ return this._getScrollXY().y;
+ };
+
+ setScrollX(x) {
+ this.outerWin.scrollTo(x, this.getScrollY());
+ };
+
+ setScrollY(y) {
+ this.outerWin.scrollTo(this.getScrollX(), y);
+ };
+
+ setScrollXY(x, y) {
+ this.outerWin.scrollTo(x, y);
+ };
+
+ _isCaretAtTheTopOfViewport(rep) {
+ const caretLine = rep.selStart[0];
+ const linePrevCaretLine = caretLine - 1;
+ const firstLineVisibleBeforeCaretLine =
+ getPreviousVisibleLine(linePrevCaretLine, rep);
+ const caretLineIsPartiallyVisibleOnViewport =
+ this._isLinePartiallyVisibleOnViewport(caretLine, rep);
+ const lineBeforeCaretLineIsPartiallyVisibleOnViewport =
+ this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
+ if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
+ const caretLinePosition = getPosition(); // get the position of the browser line
+ const viewportPosition = this._getViewPortTopBottom();
+ const viewportTop = viewportPosition.top;
+ const viewportBottom = viewportPosition.bottom;
+ const caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop;
+ const caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom;
+ const caretLineIsInsideOfViewport =
+ caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
+ if (caretLineIsInsideOfViewport) {
+ const prevLineTop = getPositionTopOfPreviousBrowserLine(caretLinePosition, rep);
+ const 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.
+ _getPixelsRelativeToPercentageOfViewport(innerHeight, aboveOfViewport) {
+ let pixels = 0;
+ const 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
+ _getPercentageToScroll(aboveOfViewport: boolean) {
+ let percentageToScroll = this.scrollSettings.percentage.editionBelowViewport;
+ if (aboveOfViewport) {
+ percentageToScroll = this.scrollSettings.percentage.editionAboveViewport;
+ }
+ return percentageToScroll;
+ };
+
+ _getPixelsToScrollWhenUserPressesArrowUp(innerHeight) {
+ let pixels = 0;
+ const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
+ if (percentageToScrollUp > 0 && percentageToScrollUp <= 1) {
+ pixels = parseInt(innerHeight * percentageToScrollUp);
+ }
+ return pixels;
+ };
+
+ _scrollYPage(pixelsToScroll) {
+ const durationOfAnimationToShowFocusline = this.scrollSettings.duration;
+ if (durationOfAnimationToShowFocusline) {
+ this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
+ } else {
+ this._scrollYPageWithoutAnimation(pixelsToScroll);
+ }
+ };
+
+ _scrollYPageWithoutAnimation(pixelsToScroll) {
+ this.outerWin.scrollBy(0, pixelsToScroll);
+ };
+
+ _scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline) {
+ const outerDocBody = this.doc.getElementById('outerdocbody');
+
+ // it works on later versions of Chrome
+ const $outerDocBody = $(outerDocBody);
+ this._triggerScrollWithAnimation(
+ $outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
+
+ // it works on Firefox and earlier versions of Chrome
+ const $outerDocBodyParent = $outerDocBody.parent();
+ this._triggerScrollWithAnimation(
+ $outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
+ };
+
+ _triggerScrollWithAnimation($elem, pixelsToScroll, durationOfAnimationToShowFocusline) {
+ // clear the queue of animation
+ $elem.stop('scrollanimation');
+ $elem.animate({
+ scrollTop: `+=${pixelsToScroll}`,
+ }, {
+ duration: durationOfAnimationToShowFocusline,
+ queue: 'scrollanimation',
+ }).dequeue('scrollanimation');
+ };
+
+
+
+ scrollNodeVerticallyIntoView(rep, innerHeight) {
+ const viewport = this._getViewPortTopBottom();
+
+ // 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.
+ const linePosition = getPosition();
+ if (linePosition) {
+ const distanceOfTopOfViewport = linePosition.top - viewport.top;
+ const distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom - linePosition.height;
+ const caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
+ const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
+ if (caretIsAboveOfViewport) {
+ const pixelsToScroll =
+ distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
+ this._scrollYPage(pixelsToScroll);
+ } else if (caretIsBelowOfViewport) {
+ // setTimeout is required here as line might not be fully rendered onto the pad
+ setTimeout(() => {
+ const outer = window.parent;
+ // scroll to the very end of the pad outer
+ outer.scrollTo(0, outer[0].innerHeight);
+ }, 150);
+ // if the above setTimeout and functionality is removed then hitting an enter
+ // key while on the last line wont be an optimal user experience
+ // Details at: https://github.com/ether/etherpad-lite/pull/4639/files
+ }
+ }
+ };
+
+ _partOfRepLineIsOutOfViewport(viewportPosition, rep) {
+ const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
+ const line = rep.lines.atIndex(focusLine);
+ const linePosition = this._getLineEntryTopBottom(line);
+ const lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
+ const lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
+
+ return lineIsBelowOfViewport || lineIsAboveOfViewport;
+ };
+
+ _getLineEntryTopBottom(entry, destObj) {
+ const dom = entry.lineNode;
+ const top = dom.offsetTop;
+ const height = dom.offsetHeight;
+ const obj = (destObj || {});
+ obj.top = top;
+ obj.bottom = (top + height);
+ return obj;
+ };
+
+ _arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep) {
+ const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
+ return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
+ };
+
+ getVisibleLineRange(rep) {
+ const viewport = this._getViewPortTopBottom();
+ // console.log("viewport top/bottom: %o", viewport);
+ const obj = {};
+ const self = this;
+ const start = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).bottom > viewport.top);
+ // 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.
+ let end = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).top >= viewport.bottom);
+ if (end < start) end = start; // unlikely
+ // top.console.log(start+","+(end -1));
+ return [start, end - 1];
+ };
+
+ getVisibleCharRange(rep) {
+ const lineRange = this.getVisibleLineRange(rep);
+ return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
+ };
+}
+
+export default Scroll