mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-04 22:27:10 -04:00
Moved first js files to ts
This commit is contained in:
parent
be2616f766
commit
865f2e565a
6 changed files with 361 additions and 360 deletions
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
|
@ -294,6 +294,9 @@ importers:
|
||||||
'@types/http-errors':
|
'@types/http-errors':
|
||||||
specifier: ^2.0.4
|
specifier: ^2.0.4
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
|
'@types/jquery':
|
||||||
|
specifier: ^3.5.30
|
||||||
|
version: 3.5.30
|
||||||
'@types/jsdom':
|
'@types/jsdom':
|
||||||
specifier: ^21.1.7
|
specifier: ^21.1.7
|
||||||
version: 21.1.7
|
version: 21.1.7
|
||||||
|
@ -1485,6 +1488,9 @@ packages:
|
||||||
'@types/http-errors@2.0.4':
|
'@types/http-errors@2.0.4':
|
||||||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||||
|
|
||||||
|
'@types/jquery@3.5.30':
|
||||||
|
resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==}
|
||||||
|
|
||||||
'@types/jsdom@21.1.7':
|
'@types/jsdom@21.1.7':
|
||||||
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
||||||
|
|
||||||
|
@ -1572,6 +1578,9 @@ packages:
|
||||||
'@types/sinonjs__fake-timers@8.1.5':
|
'@types/sinonjs__fake-timers@8.1.5':
|
||||||
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
|
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
|
||||||
|
|
||||||
|
'@types/sizzle@2.3.8':
|
||||||
|
resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==}
|
||||||
|
|
||||||
'@types/superagent@8.1.7':
|
'@types/superagent@8.1.7':
|
||||||
resolution: {integrity: sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==}
|
resolution: {integrity: sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==}
|
||||||
|
|
||||||
|
@ -5448,6 +5457,10 @@ snapshots:
|
||||||
|
|
||||||
'@types/http-errors@2.0.4': {}
|
'@types/http-errors@2.0.4': {}
|
||||||
|
|
||||||
|
'@types/jquery@3.5.30':
|
||||||
|
dependencies:
|
||||||
|
'@types/sizzle': 2.3.8
|
||||||
|
|
||||||
'@types/jsdom@21.1.7':
|
'@types/jsdom@21.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.14.10
|
'@types/node': 20.14.10
|
||||||
|
@ -5550,6 +5563,8 @@ snapshots:
|
||||||
|
|
||||||
'@types/sinonjs__fake-timers@8.1.5': {}
|
'@types/sinonjs__fake-timers@8.1.5': {}
|
||||||
|
|
||||||
|
'@types/sizzle@2.3.8': {}
|
||||||
|
|
||||||
'@types/superagent@8.1.7':
|
'@types/superagent@8.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cookiejar': 2.1.5
|
'@types/cookiejar': 2.1.5
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/http-errors": "^2.0.4",
|
"@types/http-errors": "^2.0.4",
|
||||||
|
"@types/jquery": "^3.5.30",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
|
|
|
@ -30,6 +30,8 @@ const setAssoc = Ace2Common.setAssoc;
|
||||||
const noop = Ace2Common.noop;
|
const noop = Ace2Common.noop;
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
|
||||||
|
import Scroll from './scroll'
|
||||||
|
|
||||||
function Ace2Inner(editorInfo, cssManagers) {
|
function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
|
@ -42,7 +44,6 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const SkipList = require('./skiplist');
|
const SkipList = require('./skiplist');
|
||||||
const undoModule = require('./undomodule').undoModule;
|
const undoModule = require('./undomodule').undoModule;
|
||||||
const AttributeManager = require('./AttributeManager');
|
const AttributeManager = require('./AttributeManager');
|
||||||
const Scroll = require('./scroll');
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
|
||||||
const THE_TAB = ' '; // 4
|
const THE_TAB = ' '; // 4
|
||||||
|
@ -77,7 +78,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
appendNewSideDivLine();
|
appendNewSideDivLine();
|
||||||
|
|
||||||
const scroll = Scroll.init(outerWin);
|
const scroll = new Scroll(outerWin);
|
||||||
|
|
||||||
let outsideKeyDown = noop;
|
let outsideKeyDown = noop;
|
||||||
let outsideKeyPress = (e) => true;
|
let outsideKeyPress = (e) => true;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// One rep.line(div) can be broken in more than one line in the browser.
|
// 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
|
// This function is useful to get the caret position of the line as
|
||||||
// is represented by the browser
|
// is represented by the browser
|
||||||
exports.getPosition = () => {
|
export const getPosition = () => {
|
||||||
const range = getSelectionRange();
|
const range = getSelectionRange();
|
||||||
console.log("Getting range", range)
|
console.log("Getting range", range)
|
||||||
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
|
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
|
// 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
|
// [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
|
// 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]
|
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
|
||||||
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
|
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.
|
// So, we can use the caret line to calculate the bottom of the line.
|
||||||
// [2] the next line is part of another rep 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
|
// 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]
|
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
|
||||||
const isCaretLineLastBrowserLine =
|
const isCaretLineLastBrowserLine =
|
||||||
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
|
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
|
||||||
|
@ -154,7 +154,7 @@ const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
|
||||||
return lastRootChildNodePosition.top === caretLineTop;
|
return lastRootChildNodePosition.top === caretLineTop;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPreviousVisibleLine = (line, rep) => {
|
export const getPreviousVisibleLine = (line, rep) => {
|
||||||
const firstLineOfPad = 0;
|
const firstLineOfPad = 0;
|
||||||
if (line <= firstLineOfPad) {
|
if (line <= firstLineOfPad) {
|
||||||
return 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;
|
const lastLineOfThePad = rep.lines.length() - 1;
|
||||||
if (line >= lastLineOfThePad) {
|
if (line >= lastLineOfThePad) {
|
||||||
return lastLineOfThePad;
|
return lastLineOfThePad;
|
||||||
|
@ -178,7 +177,6 @@ const getNextVisibleLine = (line, rep) => {
|
||||||
return getNextVisibleLine(line + 1, rep);
|
return getNextVisibleLine(line + 1, rep);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
exports.getNextVisibleLine = getNextVisibleLine;
|
|
||||||
|
|
||||||
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
|
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
|
||||||
|
|
|
@ -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 <div> is a line)
|
|
||||||
Browser Line = each vertical line. A <div> 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);
|
|
337
src/static/js/scroll.ts
Normal file
337
src/static/js/scroll.ts
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue