';
+ if (!targetBody.firstChild) {
+ targetBody.innerHTML = '
';
}
observeChangesAroundSelection();
@@ -1022,7 +1028,7 @@ function Ace2Inner(editorInfo, cssManagers) {
j++;
}
if (!dirtyRangesCheckOut) {
- for (const bodyNode of document.body.childNodes) {
+ for (const bodyNode of targetBody.childNodes) {
if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) {
observeChangesAroundNode(bodyNode);
}
@@ -1044,11 +1050,11 @@ function Ace2Inner(editorInfo, cssManagers) {
const range = dirtyRanges[i];
a = range[0];
b = range[1];
- let firstDirtyNode = (((a === 0) && document.body.firstChild) ||
+ let firstDirtyNode = (((a === 0) && targetBody.firstChild) ||
getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
- let lastDirtyNode = (((b === rep.lines.length()) && document.body.lastChild) ||
+ let lastDirtyNode = (((b === rep.lines.length()) && targetBody.lastChild) ||
getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
@@ -1135,7 +1141,7 @@ function Ace2Inner(editorInfo, cssManagers) {
callstack: currentCallStack,
editorInfo,
rep,
- root: document.body,
+ root: targetBody,
point: selection.startPoint,
documentAttributeManager,
});
@@ -1147,7 +1153,7 @@ function Ace2Inner(editorInfo, cssManagers) {
callstack: currentCallStack,
editorInfo,
rep,
- root: document.body,
+ root: targetBody,
point: selection.endPoint,
documentAttributeManager,
});
@@ -1227,9 +1233,9 @@ function Ace2Inner(editorInfo, cssManagers) {
info.prepareForAdd();
entry.lineMarker = info.lineMarker;
if (!nodeToAddAfter) {
- document.body.insertBefore(node, document.body.firstChild);
+ targetBody.insertBefore(node, targetBody.firstChild);
} else {
- document.body.insertBefore(node, nodeToAddAfter.nextSibling);
+ targetBody.insertBefore(node, nodeToAddAfter.nextSibling);
}
nodeToAddAfter = node;
info.notifyAdded();
@@ -1326,7 +1332,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// Turn DOM node selection into [line,char] selection.
// This method has to work when the DOM is not pristine,
// assuming the point is not in a dirty node.
- if (point.node === document.body) {
+ if (point.node === targetBody) {
if (point.index === 0) {
return [0, 0];
} else {
@@ -1345,7 +1351,7 @@ function Ace2Inner(editorInfo, cssManagers) {
col = nodeText(n).length;
}
let parNode, prevSib;
- while ((parNode = n.parentNode) !== document.body) {
+ while ((parNode = n.parentNode) !== targetBody) {
if ((prevSib = n.previousSibling)) {
n = prevSib;
col += nodeText(n).length;
@@ -1398,7 +1404,7 @@ function Ace2Inner(editorInfo, cssManagers) {
insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo));
for (const k of keysToDelete) {
- const n = document.getElementById(k);
+ const n = targetDoc.getElementById(k);
n.parentNode.removeChild(n);
}
@@ -2087,7 +2093,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const a = cleanNodeForIndex(i - 1);
const b = cleanNodeForIndex(i);
if ((!a) || (!b)) return false; // violates precondition
- if ((a === true) && (b === true)) return !document.body.firstChild;
+ if ((a === true) && (b === true)) return !targetBody.firstChild;
if ((a === true) && b.previousSibling) return false;
if ((b === true) && a.nextSibling) return false;
if ((a === true) || (b === true)) return true;
@@ -2232,7 +2238,7 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const isNodeDirty = (n) => {
- if (n.parentNode !== document.body) return true;
+ if (n.parentNode !== targetBody) return true;
const data = getAssoc(n, 'dirtiness');
if (!data) return true;
if (n.id !== data.nodeId) return true;
@@ -2856,7 +2862,7 @@ function Ace2Inner(editorInfo, cssManagers) {
updateBrowserSelectionFromRep();
// get the current caret selection, can't use rep. here because that only gives
// us the start position not the current
- const myselection = document.getSelection();
+ const myselection = targetDoc.getSelection();
// get the carets selection offset in px IE 214
let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
myselection.focusNode.offsetTop;
@@ -2970,13 +2976,13 @@ function Ace2Inner(editorInfo, cssManagers) {
// with background doesn't seem to show up...
if (isNodeText(p.node) && p.index === p.maxIndex) {
let n = p.node;
- while (!n.nextSibling && n !== document.body && n.parentNode !== document.body) {
+ while (!n.nextSibling && n !== targetBody && n.parentNode !== targetBody) {
n = n.parentNode;
}
if (n.nextSibling &&
!(typeof n.nextSibling.tagName === 'string' &&
n.nextSibling.tagName.toLowerCase() === 'br') &&
- n !== p.node && n !== document.body && n.parentNode !== document.body) {
+ n !== p.node && n !== targetBody && n.parentNode !== targetBody) {
// found a parent, go to next node and dive in
p.node = n.nextSibling;
p.maxIndex = nodeMaxIndex(p.node);
@@ -3078,7 +3084,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// each of which has node (a magicdom node), index, and maxIndex. If the node
// is a text node, maxIndex is the length of the text; else maxIndex is 1.
// index is between 0 and maxIndex, inclusive.
- const browserSelection = window.getSelection();
+ const browserSelection = targetDoc.getSelection();
if (!browserSelection || browserSelection.type === 'None' ||
browserSelection.rangeCount === 0) {
return null;
@@ -3096,7 +3102,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (!isInBody(container)) {
// command-click in Firefox selects whole document, HEAD and BODY!
return {
- node: document.body,
+ node: targetBody,
index: 0,
maxIndex: 1,
};
@@ -3191,7 +3197,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// If non-nullish, pasting on a link should be suppressed.
let suppressPasteOnLink = null;
- $(document.body).on('auxclick', (e) => {
+ $(targetBody).on('auxclick', (e) => {
if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) {
// The user middle-clicked on a link. Usually users do this to open a link in a new tab, but
// in X11 (Linux) this will instead paste the contents of the primary selection at the mouse
@@ -3213,7 +3219,7 @@ function Ace2Inner(editorInfo, cssManagers) {
}
});
- $(document.body).on('paste', (e) => {
+ $(targetBody).on('paste', (e) => {
if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) {
scheduler.clearTimeout(suppressPasteOnLink);
suppressPasteOnLink = null;
@@ -3233,7 +3239,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// We reference document here, this is because if we don't this will expose a bug
// in Google Chrome. This bug will cause the last character on the last line to
// not fire an event when dropped into..
- $(document).on('drop', (e) => {
+ $(targetBody).on('drop', (e) => {
if (e.target.a || e.target.localName === 'a') {
e.preventDefault();
}
@@ -3251,7 +3257,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const lineAfterSelection = lastLineSelected.nextSibling;
const neighbor = lineBeforeSelection || lineAfterSelection;
- neighbor.appendChild(document.createElement('style'));
+ neighbor.appendChild(targetDoc.createElement('style'));
}
// Call drop hook
@@ -3263,10 +3269,10 @@ function Ace2Inner(editorInfo, cssManagers) {
});
});
- $(document.documentElement).on('compositionstart', () => {
+ $(targetDoc.documentElement).on('compositionstart', () => {
if (inInternationalComposition) return;
inInternationalComposition = new Promise((resolve) => {
- $(document.documentElement).one('compositionend', () => {
+ $(targetDoc.documentElement).one('compositionend', () => {
inInternationalComposition = null;
resolve();
});
@@ -3275,8 +3281,8 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const topLevel = (n) => {
- if ((!n) || n === document.body) return null;
- while (n.parentNode !== document.body) {
+ if ((!n) || n === targetBody) return null;
+ while (n.parentNode !== targetBody) {
n = n.parentNode;
}
return n;
@@ -3436,10 +3442,10 @@ function Ace2Inner(editorInfo, cssManagers) {
// but as it's non-text type the line-height/margins might not be present and it
// could be that this breaks a theme that has a different default line height..
// So instead of using an integer here we get the value from the Editor CSS.
- const innerdocbodyStyles = getComputedStyle(document.body);
+ const innerdocbodyStyles = getComputedStyle(targetBody);
const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']);
- for (const docLine of document.body.children) {
+ for (const docLine of targetBody.children) {
let h;
const nextDocLine = docLine.nextElementSibling;
if (nextDocLine) {
@@ -3450,7 +3456,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// included on the first line. The default stylesheet doesn't add
// extra margins/padding, but plugins might.
h = nextDocLine.offsetTop - parseInt(
- window.getComputedStyle(document.body)
+ window.getComputedStyle(targetBody)
.getPropertyValue('padding-top').split('px')[0]);
} else {
h = nextDocLine.offsetTop - docLine.offsetTop;
@@ -3496,15 +3502,15 @@ function Ace2Inner(editorInfo, cssManagers) {
this.init = async () => {
await $.ready;
inCallStack('setup', () => {
- if (browser.firefox) $(document.body).addClass('mozilla');
- if (browser.safari) $(document.body).addClass('safari');
- document.body.classList.toggle('authorColors', true);
- document.body.classList.toggle('doesWrap', doesWrap);
+ if (browser.firefox) $(targetBody).addClass('mozilla');
+ if (browser.safari) $(targetBody).addClass('safari');
+ targetBody.classList.toggle('authorColors', true);
+ targetBody.classList.toggle('doesWrap', doesWrap);
enforceEditability();
// set up dom and rep
- while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
+ while (targetBody.firstChild) targetBody.removeChild(targetBody.firstChild);
const oneEntry = createDomLineEntry('');
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo]);
diff --git a/src/static/js/caretPosition.js b/src/static/js/caretPosition.js
index 03af77f33..2814da74a 100644
--- a/src/static/js/caretPosition.js
+++ b/src/static/js/caretPosition.js
@@ -5,6 +5,7 @@
// is represented by the browser
exports.getPosition = () => {
const range = getSelectionRange();
+ console.log("Getting range", range)
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
// When there's a or any element that has no height, we can't get the dimension of the
// element where the caret is. As we can't get the element height, we create a text node to get
diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js
index 585cccbb5..c02832835 100644
--- a/src/static/js/pad_editor.js
+++ b/src/static/js/pad_editor.js
@@ -24,9 +24,9 @@
const Cookies = require('./pad_utils').Cookies;
const padcookie = require('./pad_cookie').padcookie;
const padutils = require('./pad_utils').padutils;
+const Ace2Editor = require('./ace').Ace2Editor;
const padeditor = (() => {
- let Ace2Editor = undefined;
let pad = undefined;
let settings = undefined;
@@ -35,7 +35,6 @@ const padeditor = (() => {
// this is accessed directly from other files
viewZoom: 100,
init: async (initialViewOptions, _pad) => {
- Ace2Editor = require('./ace').Ace2Editor;
pad = _pad;
settings = pad.settings;
self.ace = new Ace2Editor();
diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js
index 58105d23c..6601cb2c3 100644
--- a/src/static/js/pad_utils.js
+++ b/src/static/js/pad_utils.js
@@ -443,7 +443,7 @@ const inThirdPartyIframe = () => {
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
// window object.
if (typeof window !== 'undefined') {
- exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({
+ exports.Cookies = require('js-cookie').withAttributes({
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
// because the cookies are third-party (not same-site). Many browsers/users block third-party
diff --git a/src/static/js/pluginfw/client_plugins.js b/src/static/js/pluginfw/client_plugins.js
index 221e786f8..3a0687733 100644
--- a/src/static/js/pluginfw/client_plugins.js
+++ b/src/static/js/pluginfw/client_plugins.js
@@ -7,24 +7,13 @@ exports.baseURL = '';
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
-exports.update = (cb) => {
- // It appears that this response (see #620) may interrupt the current thread
- // of execution on Firefox. This schedules the response in the run-loop,
- // which appears to fix the issue.
- const callback = () => setTimeout(cb, 0);
-
- jQuery.getJSON(
- `${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`
- ).done((data) => {
- defs.plugins = data.plugins;
- defs.parts = data.parts;
- defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');
- defs.loaded = true;
- callback();
- }).fail((err) => {
- console.error(`Failed to load plugin-definitions: ${err}`);
- callback();
- });
+exports.update = async (modules) => {
+ const data = await jQuery.getJSON(
+ `${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`);
+ defs.plugins = data.plugins;
+ defs.parts = data.parts;
+ defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks', null, modules);
+ defs.loaded = true;
};
const adoptPluginsFromAncestorsOf = (frame) => {
diff --git a/src/static/js/scroll.js b/src/static/js/scroll.js
index 86d6a3344..6614a1973 100644
--- a/src/static/js/scroll.js
+++ b/src/static/js/scroll.js
@@ -15,7 +15,7 @@ function Scroll(outerWin) {
// DOM reference
this.outerWin = outerWin;
- this.doc = this.outerWin.document;
+ this.doc = this.outerWin.contentDocument;
this.rootDocument = parent.parent.document;
}
diff --git a/src/static/js/vendors/farbtastic.js b/src/static/js/vendors/farbtastic.js
index ad832dc72..5a187ea47 100644
--- a/src/static/js/vendors/farbtastic.js
+++ b/src/static/js/vendors/farbtastic.js
@@ -172,7 +172,7 @@ $._farbtastic = function (container, options) {
angle2 = d2 * Math.PI * 2,
// Endpoints
x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
- x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
+ let x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
// Midpoint chosen so that the endpoints are tangent to the circle.
am = (angle1 + angle2) / 2,
tan = 1 / Math.cos((angle2 - angle1) / 2),
@@ -329,8 +329,8 @@ $._farbtastic = function (container, options) {
// Update the overlay canvas.
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
- for (i in circles) {
- var c = circles[i];
+ for (let i in circles) {
+ const c = circles[i];
fb.ctxOverlay.lineWidth = c.lw;
fb.ctxOverlay.strokeStyle = c.c;
fb.ctxOverlay.beginPath();
diff --git a/src/templates/padBootstrap.js b/src/templates/padBootstrap.js
index 5758234ae..77861758c 100644
--- a/src/templates/padBootstrap.js
+++ b/src/templates/padBootstrap.js
@@ -1,3 +1,4 @@
+
(async () => {
window.clientVars = {
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server
@@ -6,7 +7,7 @@
};
// Allow other frames to access this frame's modules.
- window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie');
+ //window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie');
const basePath = new URL('..', window.location.href).pathname;
window.$ = window.jQuery = require('../../src/static/js/rjquery').jQuery;
diff --git a/var/js/.gitignore b/var/js/.gitignore
index e69de29bb..086f4e283 100644
--- a/var/js/.gitignore
+++ b/var/js/.gitignore
@@ -0,0 +1,2 @@
+*.js
+*.map
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 04/13] 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
From b7e0e6b216bbbf3afe5086ceac60931805887172 Mon Sep 17 00:00:00 2001
From: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
Date: Sat, 13 Jul 2024 21:26:57 +0200
Subject: [PATCH 05/13] Fixed caret positioning
---
src/static/js/ace2_inner.js | 4 ++--
src/static/js/caretPosition.ts | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index f2966203c..dd83f36d3 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -3010,7 +3010,7 @@ function Ace2Inner(editorInfo, cssManagers) {
};
}
};
- const browserSelection = window.getSelection();
+ const browserSelection = targetDoc.getSelection();
if (browserSelection) {
browserSelection.removeAllRanges();
if (selection) {
@@ -3153,7 +3153,7 @@ function Ace2Inner(editorInfo, cssManagers) {
browserSelection.anchorOffset === range.endOffset,
};
- if (selection.startPoint.node.ownerDocument !== window.document) {
+ if (selection.startPoint.node.ownerDocument !== targetDoc) {
return null;
}
diff --git a/src/static/js/caretPosition.ts b/src/static/js/caretPosition.ts
index 23e956d30..236584a4c 100644
--- a/src/static/js/caretPosition.ts
+++ b/src/static/js/caretPosition.ts
@@ -5,7 +5,6 @@
// is represented by the browser
export const getPosition = () => {
const range = getSelectionRange();
- console.log("Getting range", range)
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
// When there's a or any element that has no height, we can't get the dimension of the
// element where the caret is. As we can't get the element height, we create a text node to get
@@ -190,6 +189,7 @@ const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => {
};
const getSelectionRange = () => {
+ console.log("Selection is",window.frameElement)
if (!window.getSelection) {
return;
}
From 85dc9c088f7c39b80868604fb5b359cc5976c53c Mon Sep 17 00:00:00 2001
From: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
Date: Sat, 13 Jul 2024 21:45:25 +0200
Subject: [PATCH 06/13] Added support for plugins
---
src/node/hooks/express/specialpages.ts | 1 +
src/static/js/pluginfw/shared.js | 8 ++++----
src/templates/padBootstrap.js | 2 +-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts
index 714d7b3dd..0905abf03 100644
--- a/src/node/hooks/express/specialpages.ts
+++ b/src/node/hooks/express/specialpages.ts
@@ -90,6 +90,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
const pluginModules = new Set();
for (const part of plugins.parts) {
for (const [, hookFnName] of Object.entries(part.client_hooks || {})) {
+ console.log(hookFnName.split(':')[0])
pluginModules.add(hookFnName.split(':')[0]);
}
}
diff --git a/src/static/js/pluginfw/shared.js b/src/static/js/pluginfw/shared.js
index 2c81ccd81..7d58412c0 100644
--- a/src/static/js/pluginfw/shared.js
+++ b/src/static/js/pluginfw/shared.js
@@ -9,7 +9,7 @@ const disabledHookReasons = {
},
};
-const loadFn = (path, hookName) => {
+const loadFn = (path, hookName, modules) => {
let functionName;
const parts = path.split(':');
@@ -24,7 +24,7 @@ const loadFn = (path, hookName) => {
functionName = parts[1];
}
- let fn = require(path);
+ let fn = modules ? modules.get(path) : require(/* webpackIgnore: true */ path);
functionName = functionName ? functionName : hookName;
for (const name of functionName.split('.')) {
@@ -33,7 +33,7 @@ const loadFn = (path, hookName) => {
return fn;
};
-const extractHooks = (parts, hookSetName, normalizer) => {
+const extractHooks = (parts, hookSetName, normalizer, modules) => {
const hooks = {};
for (const part of parts) {
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
@@ -53,7 +53,7 @@ const extractHooks = (parts, hookSetName, normalizer) => {
}
let hookFn;
try {
- hookFn = loadFn(hookFnName, hookName);
+ hookFn = loadFn(hookFnName, hookName, modules);
if (!hookFn) throw new Error('Not a function');
} catch (err) {
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
diff --git a/src/templates/padBootstrap.js b/src/templates/padBootstrap.js
index 77861758c..62dd44b58 100644
--- a/src/templates/padBootstrap.js
+++ b/src/templates/padBootstrap.js
@@ -27,7 +27,7 @@
window.plugins.baseURL = basePath;
await window.plugins.update(new Map([
<% for (const module of pluginModules) { %>
- [<%- JSON.stringify(module) %>, require(<%- JSON.stringify(module) %>)],
+ [<%- JSON.stringify(module) %>, require("../../src/plugin_packages/"+<%- JSON.stringify(module) %>)],
<% } %>
]));
// Mechanism for tests to register hook functions (install fake plugins).
From 1ccd27e2d342d28dac756e480718448d8fec5405 Mon Sep 17 00:00:00 2001
From: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
Date: Sat, 13 Jul 2024 21:59:16 +0200
Subject: [PATCH 07/13] Fixed get undefined.
---
src/static/js/pluginfw/shared.js | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/static/js/pluginfw/shared.js b/src/static/js/pluginfw/shared.js
index 7d58412c0..b2c2337f6 100644
--- a/src/static/js/pluginfw/shared.js
+++ b/src/static/js/pluginfw/shared.js
@@ -24,7 +24,13 @@ const loadFn = (path, hookName, modules) => {
functionName = parts[1];
}
- let fn = modules ? modules.get(path) : require(/* webpackIgnore: true */ path);
+ let fn
+ if (modules === undefined || !("get" in modules)) {
+ fn = require(/* webpackIgnore: true */ path);
+ } else {
+ fn = modules.get(path);
+ }
+
functionName = functionName ? functionName : hookName;
for (const name of functionName.split('.')) {
From 5d0da514f9c8ee437a27d0a9d854bd64bdc618de Mon Sep 17 00:00:00 2001
From: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
Date: Sat, 13 Jul 2024 22:14:57 +0200
Subject: [PATCH 08/13] Removed require of socketio, l10n, html10n and error
reporter
---
src/static/js/l10n.js | 3 ++-
src/static/js/socketio.js | 2 +-
src/static/js/vendors/html10n.js | 6 +++++-
src/templates/pad.html | 4 ----
src/templates/padBootstrap.js | 4 ++++
5 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/src/static/js/l10n.js b/src/static/js/l10n.js
index 7206f913b..ab80ef3ca 100644
--- a/src/static/js/l10n.js
+++ b/src/static/js/l10n.js
@@ -1,4 +1,5 @@
-'use strict';
+import html10n from '../js/vendors/html10n';
+
((document) => {
// Set language for l10n
diff --git a/src/static/js/socketio.js b/src/static/js/socketio.js
index 1d3739775..cdc1c9a23 100644
--- a/src/static/js/socketio.js
+++ b/src/static/js/socketio.js
@@ -1,4 +1,4 @@
-'use strict';
+import io from 'socket.io-client';
/**
* Creates a socket.io connection.
diff --git a/src/static/js/vendors/html10n.js b/src/static/js/vendors/html10n.js
index 50b6d2279..1f6a11728 100644
--- a/src/static/js/vendors/html10n.js
+++ b/src/static/js/vendors/html10n.js
@@ -22,7 +22,7 @@
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
-window.html10n = (function(window, document, undefined) {
+export let html10n = (function(window, document, undefined) {
// fix console
(function() {
@@ -1054,3 +1054,7 @@ window.html10n = (function(window, document, undefined) {
return html10n
})(window, document)
+
+export default html10n
+
+window.html10n = html10n
diff --git a/src/templates/pad.html b/src/templates/pad.html
index 0d7072f82..08437b628 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -34,7 +34,6 @@
for the JavaScript code in this page.|
*/
-
@@ -53,8 +52,6 @@
<% e.end_block(); %>
-
-