From 856908219d1b0598e1f12ced248c67b346ed4758 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jul 2024 10:03:42 +0200 Subject: [PATCH 01/13] Bump lucide-react from 0.407.0 to 0.408.0 in the dev-dependencies group (#6510) Bumps the dev-dependencies group with 1 update: [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react). Updates `lucide-react` from 0.407.0 to 0.408.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.408.0/packages/lucide-react) --- updated-dependencies: - dependency-name: lucide-react dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/admin/package.json b/admin/package.json index 2c3241b5b..63227ba40 100644 --- a/admin/package.json +++ b/admin/package.json @@ -26,7 +26,7 @@ "eslint-plugin-react-refresh": "^0.4.8", "i18next": "^23.11.5", "i18next-browser-languagedetector": "^8.0.0", - "lucide-react": "^0.407.0", + "lucide-react": "^0.408.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.52.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 997e5e757..a20d3427d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,8 +65,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 lucide-react: - specifier: ^0.407.0 - version: 0.407.0(react@18.3.1) + specifier: ^0.408.0 + version: 0.408.0(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -3213,8 +3213,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@0.407.0: - resolution: {integrity: sha512-+dRIu9Sry+E8wPF9+sY5eKld2omrU4X5IKXxrgqBt+o11IIHVU0QOfNoVWFuj0ZRDrxr4Wci26o2mKZqLGE0lA==} + lucide-react@0.408.0: + resolution: {integrity: sha512-8kETAAeWmOvtGIr7HPHm51DXoxlfkNncQ5FZWXR+abX8saQwMYXANWIkUstaYtcKSo/imOe/q+tVFA8ANzdSVA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -7557,7 +7557,7 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@0.407.0(react@18.3.1): + lucide-react@0.408.0(react@18.3.1): dependencies: react: 18.3.1 From 6d73fed7b6c88f45c28d755663f9f4eb25d21ee0 Mon Sep 17 00:00:00 2001 From: SamTv12345 Date: Fri, 12 Jul 2024 15:00:44 +0200 Subject: [PATCH 02/13] Added minify --- src/node/hooks/express/specialpages.ts | 43 +++++++++++++++---- src/templates/pad.html | 57 +------------------------- src/templates/padBootstrap.js | 40 ++++++++++++++++++ var/js/.gitignore | 0 4 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 src/templates/padBootstrap.js create mode 100644 var/js/.gitignore diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 85a23479f..16708f488 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -1,7 +1,7 @@ 'use strict'; const path = require('path'); -const eejs = require('../../eejs'); +const eejs = require('../../eejs') const fs = require('fs'); const fsp = fs.promises; const toolbar = require('../../utils/toolbar'); @@ -9,7 +9,8 @@ const hooks = require('../../../static/js/pluginfw/hooks'); const settings = require('../../utils/Settings'); const util = require('util'); const webaccess = require('./webaccess'); - +const plugins = require('../../../static/js/pluginfw/plugin_defs'); +import {buildSync} from 'esbuild' exports.expressPreSession = async (hookName:string, {app}:any) => { // This endpoint is intended to conform to: // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html @@ -73,14 +74,42 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { }); }; -exports.expressCreateServer = (hookName:string, args:any, cb:Function) => { +exports.expressCreateServer = async (hookName: string, args: any, cb: Function) => { // serve index.html under / - args.app.get('/', (req:any, res:any) => { + args.app.get('/', (req: any, res: any) => { res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); }); + await fsp.writeFile( + path.join(settings.root, 'var/js/padbootstrap.js'), + eejs.require('ep_etherpad-lite/templates/padBootstrap.js', { + pluginModules: (() => { + const pluginModules = new Set(); + for (const part of plugins.parts) { + for (const [, hookFnName] of Object.entries(part.client_hooks || {})) { + pluginModules.add(hookFnName.split(':')[0]); + } + } + return [...pluginModules]; + })(), + settings, + })); + + const result = buildSync({ + entryPoints: [settings.root + "/src/templates/padBootstrap.js"], // Entry file(s) + bundle: true, // Bundle the files together + minify: true, // Minify the output + sourcemap: true, // Generate source maps + sourceRoot: settings.root+"/src/static/js/", + target: ['es2020'], // Target ECMAScript version + write: false, // Do not write to file system, + }) + + const textResult = result.outputFiles[0].text + + // serve pad.html under /p - args.app.get('/p/:pad', (req:any, res:any, next:Function) => { + args.app.get('/p/:pad', (req: any, res: any, next: Function) => { // The below might break for pads being rewritten const isReadOnly = !webaccess.userCanModify(req.params.pad, req); @@ -99,7 +128,7 @@ exports.expressCreateServer = (hookName:string, args:any, cb:Function) => { }); // serve timeslider.html under /p/$padname/timeslider - args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => { + args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => { hooks.callAll('padInitToolbar', { toolbar, }); @@ -112,7 +141,7 @@ exports.expressCreateServer = (hookName:string, args:any, cb:Function) => { // The client occasionally polls this endpoint to get an updated expiration for the express_sid // cookie. This handler must be installed after the express-session middleware. - args.app.put('/_extendExpressSessionLifetime', (req:any, res:any) => { + args.app.put('/_extendExpressSessionLifetime', (req: any, res: any) => { // express-session automatically calls req.session.touch() so we don't need to do it here. res.json({status: 'ok'}); }); diff --git a/src/templates/pad.html b/src/templates/pad.html index c0c56bf24..0d7072f82 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -441,68 +441,13 @@ <% e.begin_block("scripts"); %> - - - - - + <% e.begin_block("customScripts"); %> <% e.end_block(); %> - - -
JavaScript license information
<% e.end_block(); %> diff --git a/src/templates/padBootstrap.js b/src/templates/padBootstrap.js new file mode 100644 index 000000000..5758234ae --- /dev/null +++ b/src/templates/padBootstrap.js @@ -0,0 +1,40 @@ +(async () => { + window.clientVars = { + // This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server + // sends the CLIENT_VARS message. + randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>, + }; + + // Allow other frames to access this frame's modules. + 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; + window.browser = require('../../src/static/js/vendors/browser'); + const pad = require('../../src/static/js/pad'); + pad.baseURL = basePath; + window.plugins = require('../../src/static/js/pluginfw/client_plugins'); + const hooks = require('../../src/static/js/pluginfw/hooks'); + + // TODO: These globals shouldn't exist. + window.pad = pad.pad; + window.chat = require('../../src/static/js/chat').chat; + window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar; + window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp; + require('../../src/static/js/skin_variants'); + + window.plugins.baseURL = basePath; + await window.plugins.update(new Map([ + <% for (const module of pluginModules) { %> + [<%- JSON.stringify(module) %>, require(<%- JSON.stringify(module) %>)], + <% } %> +])); + // Mechanism for tests to register hook functions (install fake plugins). + window._postPluginUpdateForTestingDone = false; + if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting(); + window._postPluginUpdateForTestingDone = true; + window.pluginDefs = require('../../src/static/js/pluginfw/plugin_defs'); + pad.init(); + await new Promise((resolve) => $(resolve)); + await hooks.aCallAll('documentReady'); +})(); diff --git a/var/js/.gitignore b/var/js/.gitignore new file mode 100644 index 000000000..e69de29bb From be2616f7662f7fb4ada9861c42eb066e36068201 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sat, 13 Jul 2024 20:40:56 +0200 Subject: [PATCH 03/13] Added POC for browser --- src/node/hooks/express/specialpages.ts | 28 ++++-- src/static/js/AttributeManager.js | 6 +- src/static/js/ace.js | 35 ++++---- src/static/js/ace2_inner.js | 108 ++++++++++++----------- src/static/js/caretPosition.js | 1 + src/static/js/pad_editor.js | 3 +- src/static/js/pad_utils.js | 2 +- src/static/js/pluginfw/client_plugins.js | 25 ++---- src/static/js/scroll.js | 2 +- src/static/js/vendors/farbtastic.js | 6 +- src/templates/padBootstrap.js | 3 +- var/js/.gitignore | 2 + 12 files changed, 117 insertions(+), 104 deletions(-) diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 16708f488..714d7b3dd 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -10,6 +10,9 @@ const settings = require('../../utils/Settings'); const util = require('util'); const webaccess = require('./webaccess'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); +import {hash, createHash} from 'node:crypto' + + import {buildSync} from 'esbuild' exports.expressPreSession = async (hookName:string, {app}:any) => { // This endpoint is intended to conform to: @@ -94,18 +97,30 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) })(), settings, })); + const hash = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/padbootstrap.js'))).digest('hex'); + const fileName = `padbootstrap-${hash.substring(0,16)}.min.js` const result = buildSync({ - entryPoints: [settings.root + "/src/templates/padBootstrap.js"], // Entry file(s) + entryPoints: [settings.root + "/var/js/padbootstrap.js"], // Entry file(s) bundle: true, // Bundle the files together - minify: true, // Minify the output + minify: false, // Minify the output sourcemap: true, // Generate source maps sourceRoot: settings.root+"/src/static/js/", target: ['es2020'], // Target ECMAScript version - write: false, // Do not write to file system, + metafile: true, + + write: true, // Do not write to file system, + outfile: settings.root + `/var/js/${fileName}`, // Output file }) - const textResult = result.outputFiles[0].text + + args.app.get(`/${fileName}`, (req: any, res: any) => { + res.sendFile(settings.root+`/var/js/${fileName}`) + }) + + args.app.get(`/${fileName}.map`, (req: any, res: any) => { + res.sendFile(settings.root+`/var/js/${fileName}.map`) + }) // serve pad.html under /p @@ -115,7 +130,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) hooks.callAll('padInitToolbar', { toolbar, - isReadOnly, + isReadOnly }); // can be removed when require-kernel is dropped @@ -124,6 +139,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) req, toolbar, isReadOnly, + entrypoint: "/"+fileName })); }); @@ -145,6 +161,4 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) // express-session automatically calls req.session.touch() so we don't need to do it here. res.json({status: 'ok'}); }); - - return cb(); }; diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index f508af641..63af431d9 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -4,7 +4,7 @@ const AttributeMap = require('./AttributeMap'); const Changeset = require('./Changeset'); const ChangesetUtils = require('./ChangesetUtils'); const attributes = require('./attributes'); -const _ = require('./underscore'); +const underscore = require("underscore") const lineMarkerAttribute = 'lmkr'; @@ -45,7 +45,7 @@ const AttributeManager = function (rep, applyChangesetCallback) { AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES; AttributeManager.lineAttributes = lineAttributes; -AttributeManager.prototype = _(AttributeManager.prototype).extend({ +AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({ applyChangeset(changeset) { if (!this.applyChangesetCallback) return changeset; @@ -335,7 +335,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); - const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1]) + const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1]) .map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); // if we have marker and any of attributes don't need to have marker. we need delete it diff --git a/src/static/js/ace.js b/src/static/js/ace.js index b0a042570..dece1c4c2 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -27,9 +27,10 @@ const hooks = require('./pluginfw/hooks'); const makeCSSManager = require('./cssmanager').makeCSSManager; const pluginUtils = require('./pluginfw/shared'); - +const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner') const debugLog = (...args) => {}; - +const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins') +const rJQuery = require('ep_etherpad-lite/static/js/rjquery') // The inner and outer iframe's locations are about:blank, so relative URLs are relative to that. // Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari // errors out unless given an absolute URL for a JavaScript-created element. @@ -99,6 +100,7 @@ const Ace2Editor = function () { }; const doActionsPendingInit = () => { + console.log('doActionsPendingInit', actionsPendingInit) for (const fn of actionsPendingInit) fn(); actionsPendingInit = []; }; @@ -257,19 +259,19 @@ const Ace2Editor = function () { // tag addStyleTagsFor(innerDocument, includedCSS); - const requireKernel = innerDocument.createElement('script'); - requireKernel.type = 'text/javascript'; - requireKernel.src = - absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`); - innerDocument.head.appendChild(requireKernel); + //const requireKernel = innerDocument.createElement('script'); + //requireKernel.type = 'text/javascript'; + //requireKernel.src = + // absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`); + //innerDocument.head.appendChild(requireKernel); // Pre-fetch modules to improve load performance. - for (const module of ['ace2_inner', 'ace2_common']) { + /*for (const module of ['ace2_inner', 'ace2_common']) { const script = innerDocument.createElement('script'); script.type = 'text/javascript'; script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` + `?callback=require.define&v=${clientVars.randomVersionString}`); innerDocument.head.appendChild(script); - } + }*/ const innerStyle = innerDocument.createElement('style'); innerStyle.type = 'text/css'; innerStyle.title = 'dynamicsyntax'; @@ -284,7 +286,7 @@ const Ace2Editor = function () { innerDocument.body.classList.add('innerdocbody'); innerDocument.body.setAttribute('spellcheck', 'false'); innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //   - +/* debugLog('Ace2Editor.init() waiting for require kernel load'); await eventFired(requireKernel, 'load'); debugLog('Ace2Editor.init() require kernel loaded'); @@ -292,17 +294,16 @@ const Ace2Editor = function () { require.setRootURI(absUrl('../javascripts/src')); require.setLibraryURI(absUrl('../javascripts/lib')); require.setGlobalKeyPath('require'); - +*/ // intentially moved before requiring client_plugins to save a 307 - innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner'); - innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); - innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow); + innerWindow.Ace2Inner = ace2_inner; + innerWindow.plugins = cl_plugins; - innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; + innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery; debugLog('Ace2Editor.init() waiting for plugins'); - await new Promise((resolve, reject) => innerWindow.plugins.ensure( - (err) => err != null ? reject(err) : resolve())); + /*await new Promise((resolve, reject) => innerWindow.plugins.ensure( + (err) => err != null ? reject(err) : resolve()));*/ debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); await innerWindow.Ace2Inner.init(info, { inner: makeCSSManager(innerStyle.sheet), diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 868906cfd..f62615da2 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -54,13 +54,16 @@ function Ace2Inner(editorInfo, cssManagers) { let thisAuthor = ''; let disposed = false; + const outerWin = document.getElementsByName("ace_outer")[0] + const targetDoc = outerWin.contentWindow.document.getElementsByName("ace_inner")[0].contentWindow.document + const targetBody = targetDoc.body const focus = () => { - window.focus(); + targetBody.focus(); }; - const outerWin = window.parent; - const outerDoc = outerWin.document; + const outerDoc = outerWin.contentWindow.document; + const sideDiv = outerDoc.getElementById('sidediv'); const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv'); const sideDivInner = outerDoc.getElementById('sidedivinner'); @@ -415,7 +418,7 @@ function Ace2Inner(editorInfo, cssManagers) { const setWraps = (newVal) => { doesWrap = newVal; - document.body.classList.toggle('doesWrap', doesWrap); + targetBody.classList.toggle('doesWrap', doesWrap); scheduler.setTimeout(() => { inCallStackIfNecessary('setWraps', () => { fastIncorp(7); @@ -445,7 +448,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const setTextFace = (face) => { - document.body.style.fontFamily = face; + targetBody.style.fontFamily = face; lineMetricsDiv.style.fontFamily = face; }; @@ -456,8 +459,8 @@ function Ace2Inner(editorInfo, cssManagers) { const setEditable = (newVal) => { isEditable = newVal; - document.body.contentEditable = isEditable ? 'true' : 'false'; - document.body.classList.toggle('static', !isEditable); + targetBody.contentEditable = isEditable ? 'true' : 'false'; + targetBody.classList.toggle('static', !isEditable); }; const enforceEditability = () => setEditable(isEditable); @@ -480,6 +483,8 @@ function Ace2Inner(editorInfo, cssManagers) { newText = `${lines.join('\n')}\n`; } + window.console.log('importText', {text, undoable, dontProcess, newText}) + inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { setDocText(newText); }); @@ -520,6 +525,7 @@ function Ace2Inner(editorInfo, cssManagers) { const oldLen = rep.lines.totalWidth(); const numLines = rep.lines.length(); + window.console.log(rep, numLines - 1); const upToLastLine = rep.lines.offsetOfIndex(numLines - 1); const lastLineLength = rep.lines.atIndex(numLines - 1).text.length; const assem = Changeset.smartOpAssembler(); @@ -640,8 +646,8 @@ function Ace2Inner(editorInfo, cssManagers) { // These properties are exposed const setters = { wraps: setWraps, - showsauthorcolors: (val) => document.body.classList.toggle('authorColors', !!val), - showsuserselections: (val) => document.body.classList.toggle('userSelections', !!val), + showsauthorcolors: (val) => targetBody.classList.toggle('authorColors', !!val), + showsuserselections: (val) => targetBody.classList.toggle('userSelections', !!val), showslinenumbers: (value) => { hasLineNumbers = !!value; sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers); @@ -654,8 +660,8 @@ function Ace2Inner(editorInfo, cssManagers) { styled: setStyled, textface: setTextFace, rtlistrue: (value) => { - document.body.classList.toggle('rtl', value); - document.body.classList.toggle('ltr', !value); + targetBody.classList.toggle('rtl', value); + targetBody.classList.toggle('ltr', !value); document.documentElement.dir = value ? 'rtl' : 'ltr'; }, }; @@ -894,11 +900,11 @@ function Ace2Inner(editorInfo, cssManagers) { clearObservedChanges(); const getCleanNodeByKey = (key) => { - let n = document.getElementById(key); + let n = targetDoc.getElementById(key); // copying and pasting can lead to duplicate ids while (n && isNodeDirty(n)) { n.id = ''; - n = document.getElementById(key); + n = targetDoc.getElementById(key); } return n; }; @@ -980,11 +986,11 @@ function Ace2Inner(editorInfo, cssManagers) { const observeSuspiciousNodes = () => { // inspired by Firefox bug #473255, where pasting formatted text // causes the cursor to jump away, making the new HTML never found. - if (document.body.getElementsByTagName) { - const elts = document.body.getElementsByTagName('style'); + if (targetBody.getElementsByTagName) { + const elts = targetBody.getElementsByTagName('style'); for (const elt of elts) { const n = topLevel(elt); - if (n && n.parentNode === document.body) { + if (n && n.parentNode === targetBody) { observeChangesAroundNode(n); } } @@ -999,8 +1005,8 @@ function Ace2Inner(editorInfo, cssManagers) { if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; // returns true if dom changes were made - if (!document.body.firstChild) { - document.body.innerHTML = '
'; + 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(); %> - - <% e.begin_block("body"); %> @@ -441,7 +438,6 @@ <% e.begin_block("scripts"); %> - diff --git a/src/templates/padBootstrap.js b/src/templates/padBootstrap.js index 62dd44b58..c86d170c1 100644 --- a/src/templates/padBootstrap.js +++ b/src/templates/padBootstrap.js @@ -1,5 +1,8 @@ (async () => { + + require('../../src/static/js/l10n') + window.clientVars = { // This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server // sends the CLIENT_VARS message. @@ -23,6 +26,7 @@ window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar; window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp; require('../../src/static/js/skin_variants'); + require('../../src/static/js/basic_error_handler') window.plugins.baseURL = basePath; await window.plugins.update(new Map([ From ea5073ce13a72e7579d8b46eff2d43f15ab0bb37 Mon Sep 17 00:00:00 2001 From: SamTv12345 Date: Tue, 16 Jul 2024 12:43:21 +0200 Subject: [PATCH 09/13] Fixed popup not showing --- src/node/utils/toolbar.ts | 29 +++++++++++++++-------------- src/static/js/ace2_inner.js | 8 ++++---- src/static/js/pad_savedrevs.js | 2 +- src/static/js/pad_utils.js | 1 - src/static/js/vendors/gritter.js | 6 +++--- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/node/utils/toolbar.ts b/src/node/utils/toolbar.ts index aac3fb3d3..f0ef45479 100644 --- a/src/node/utils/toolbar.ts +++ b/src/node/utils/toolbar.ts @@ -2,7 +2,7 @@ /** * The Toolbar Module creates and renders the toolbars and buttons */ -const _ = require('underscore'); +import {isString, reduce, each, isUndefined, map, first, last, extend, escape} from 'underscore'; const removeItem = (array: string[], what: string) => { let ax; @@ -21,7 +21,7 @@ const defaultButtonAttributes = (name: string, overrides?: boolean) => ({ const tag = (name: string, attributes: AttributeObj, contents?: string) => { const aStr = tagAttributes(attributes); - if (_.isString(contents) && contents!.length > 0) { + if (isString(contents) && contents!.length > 0) { return `<${name}${aStr}>${contents}`; } else { return `<${name}${aStr}>`; @@ -34,14 +34,14 @@ type AttributeObj = { } const tagAttributes = (attributes: AttributeObj) => { - attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => { - if (!_.isUndefined(val)) { + attributes = reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => { + if (!isUndefined(val)) { o[name] = val; } return o; }, {}); - return ` ${_.map(attributes, (val: string, name: string) => `${name}="${_.escape(val)}"`).join(' ')}`; + return ` ${map(attributes, (val: string, name: string) => `${name}="${escape(val)}"`).join(' ')}`; }; type ButtonGroupType = { @@ -58,7 +58,7 @@ class ButtonGroup { public static fromArray = function (array: string[]) { const btnGroup = new ButtonGroup(); - _.each(array, (btnName: string) => { + each(array, (btnName: string) => { const button = Button.load(btnName) as Button btnGroup.addButton(button); }); @@ -70,18 +70,19 @@ class ButtonGroup { return this; } - render() { + render(): string { if (this.buttons && this.buttons.length === 1) { this.buttons[0].grouping = ''; } else if (this.buttons && this.buttons.length > 1) { - _.first(this.buttons).grouping = 'grouped-left'; - _.last(this.buttons).grouping = 'grouped-right'; - _.each(this.buttons.slice(1, -1), (btn: Button) => { + first(this.buttons)!.grouping = 'grouped-left'; + last(this.buttons)!.grouping = 'grouped-right'; + each(this.buttons.slice(1, -1), (btn: Button) => { btn.grouping = 'grouped-middle'; }); } - return _.map(this.buttons, (btn: ButtonGroup) => { + // @ts-ignore + return map(this.buttons, (btn: ButtonGroup) => { if (btn) return btn.render(); }).join('\n'); } @@ -151,8 +152,8 @@ class SelectButton extends Button { select(attributes: AttributeObj) { const options: string[] = []; - _.each(this.options, (opt: AttributeSelect) => { - const a = _.extend({ + each(this.options, (opt: AttributeSelect) => { + const a = extend({ value: opt.value, }, opt.attributes); @@ -299,7 +300,7 @@ module.exports = { buttons[0].push('savedrevision'); } - const groups = _.map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render()); + const groups = map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render()); return groups.join(this.separator()); }, }; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index dd83f36d3..90abcb7b8 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3188,10 +3188,10 @@ function Ace2Inner(editorInfo, cssManagers) { editorInfo.ace_getInInternationalComposition = () => inInternationalComposition; const bindTheEventHandlers = () => { - $(document).on('keydown', handleKeyEvent); - $(document).on('keypress', handleKeyEvent); - $(document).on('keyup', handleKeyEvent); - $(document).on('click', handleClick); + $(targetDoc).on('keydown', handleKeyEvent); + $(targetDoc).on('keypress', handleKeyEvent); + $(targetDoc).on('keyup', handleKeyEvent); + $(targetDoc).on('click', handleClick); // dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer $(outerDoc).on('click', hideEditBarDropdowns); diff --git a/src/static/js/pad_savedrevs.js b/src/static/js/pad_savedrevs.js index b5868f699..4082e0380 100644 --- a/src/static/js/pad_savedrevs.js +++ b/src/static/js/pad_savedrevs.js @@ -20,7 +20,7 @@ let pad; exports.saveNow = () => { pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); - $.gritter.add({ + window.$.gritter.add({ // (string | mandatory) the heading of the notification title: html10n.get('pad.savedrevs.marked'), // (string | mandatory) the text inside the notification diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 6601cb2c3..467a8adc9 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -356,7 +356,6 @@ const padutils = { let globalExceptionHandler = null; padutils.setupGlobalExceptionHandler = () => { if (globalExceptionHandler == null) { - require('./vendors/gritter'); globalExceptionHandler = (e) => { let type; let err; diff --git a/src/static/js/vendors/gritter.js b/src/static/js/vendors/gritter.js index a20cb4de9..1b8b9a759 100644 --- a/src/static/js/vendors/gritter.js +++ b/src/static/js/vendors/gritter.js @@ -42,8 +42,8 @@ return Gritter.add(params || {}); } catch(e) { - var err = 'Gritter Error: ' + e; - (typeof(console) != 'undefined' && console.error) ? + const err = 'Gritter Error: ' + e; + (typeof(console) != 'undefined' && console.error) ? console.error(err, params) : alert(err); @@ -289,7 +289,7 @@ */ _runSetup: function(){ - for(opt in $.gritter.options){ + for(let opt in $.gritter.options){ this[opt] = $.gritter.options[opt]; } this._is_setup = 1; From a3e9f29558e7bf2470b421e42b2f7c14e96061d8 Mon Sep 17 00:00:00 2001 From: SamTv12345 Date: Tue, 16 Jul 2024 13:47:59 +0200 Subject: [PATCH 10/13] Fixed timeslider --- src/node/hooks/express/specialpages.ts | 47 ++++++++++++++++++++++++- src/static/js/vendors/html10n.js | 2 +- src/templates/timeSliderBootstrap.js | 37 ++++++++++++++++++++ src/templates/timeslider.html | 48 +------------------------- 4 files changed, 85 insertions(+), 49 deletions(-) create mode 100644 src/templates/timeSliderBootstrap.js diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 0905abf03..11d02c001 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -98,10 +98,30 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) })(), settings, })); + + await fsp.writeFile( + path.join(settings.root, 'var/js/timesliderBootstrap.js'), + eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', { + pluginModules: (() => { + 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]); + } + } + return [...pluginModules]; + })(), + settings, + })); + const hash = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/padbootstrap.js'))).digest('hex'); + const hashTimeSlider = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/timeSliderBootstrap.js'))).digest('hex'); const fileName = `padbootstrap-${hash.substring(0,16)}.min.js` - const result = buildSync({ + const fileNameTimeSlider = `timeSliderBootstrap-${hash.substring(0,16)}.min.js` + + buildSync({ entryPoints: [settings.root + "/var/js/padbootstrap.js"], // Entry file(s) bundle: true, // Bundle the files together minify: false, // Minify the output @@ -114,6 +134,19 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) outfile: settings.root + `/var/js/${fileName}`, // Output file }) + buildSync({ + entryPoints: [settings.root + "/var/js/timesliderBootstrap.js"], // Entry file(s) + bundle: true, // Bundle the files together + minify: false, // Minify the output + sourcemap: true, // Generate source maps + sourceRoot: settings.root+"/src/static/js/", + target: ['es2020'], // Target ECMAScript version + metafile: true, + + write: true, // Do not write to file system, + outfile: settings.root + `/var/js/${fileNameTimeSlider}`, // Output file + }) + args.app.get(`/${fileName}`, (req: any, res: any) => { res.sendFile(settings.root+`/var/js/${fileName}`) @@ -123,6 +156,17 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) res.sendFile(settings.root+`/var/js/${fileName}.map`) }) + args.app.get(`/${fileNameTimeSlider}`, (req: any, res: any) => { + res.sendFile(settings.root+`/var/js/${fileNameTimeSlider}`) + }) + + args.app.get(`/${fileNameTimeSlider}.map`, (req: any, res: any) => { + res.sendFile(settings.root+`/var/js/${fileNameTimeSlider}.map`) + }) + + + + // serve pad.html under /p args.app.get('/p/:pad', (req: any, res: any, next: Function) => { @@ -153,6 +197,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { req, toolbar, + entrypoint: "/"+fileNameTimeSlider })); }); diff --git a/src/static/js/vendors/html10n.js b/src/static/js/vendors/html10n.js index 1f6a11728..6fe5e307f 100644 --- a/src/static/js/vendors/html10n.js +++ b/src/static/js/vendors/html10n.js @@ -725,7 +725,7 @@ export let html10n = (function(window, document, undefined) { return; // initialize _pluralRules - if (!this._pluralRules) + if (!("_pluralRules" in this)) this._pluralRules = getPluralRules(html10n.language); var index = this._pluralRules(n); diff --git a/src/templates/timeSliderBootstrap.js b/src/templates/timeSliderBootstrap.js new file mode 100644 index 000000000..e3138cfbd --- /dev/null +++ b/src/templates/timeSliderBootstrap.js @@ -0,0 +1,37 @@ +// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt +window.clientVars = { + // This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the + // server sends the CLIENT_VARS message. + randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>, +}; +let BroadcastSlider; + + +(function () { + const timeSlider = require('ep_etherpad-lite/static/js/timeslider') + const pathComponents = location.pathname.split('/'); + + // Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL + const baseURL = pathComponents.slice(0,pathComponents.length-3).join('/') + '/'; + require('ep_etherpad-lite/static/js/l10n') + window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; // Expose jQuery #HACK + require('ep_etherpad-lite/static/js/vendors/gritter') + + window.browser = require('ep_etherpad-lite/static/js/vendors/browser'); + + window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); + const socket = timeSlider.socket; + BroadcastSlider = timeSlider.BroadcastSlider; + plugins.baseURL = baseURL; + plugins.update(function () { + + + /* TODO: These globals shouldn't exist. */ + + }); + const padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; + const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; + timeSlider.baseURL = baseURL; + timeSlider.init(); + padeditbar.init() +})(); diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 71346f21e..e2178e54e 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -47,8 +47,6 @@ <% e.begin_block("timesliderScripts"); %> - - <% e.end_block(); %> @@ -250,58 +248,14 @@ - - - - + <% e.end_block(); %> From 3ba0176bd8b36efafb9c30712ad943ccdd983265 Mon Sep 17 00:00:00 2001 From: SamTv12345 Date: Tue, 16 Jul 2024 14:45:49 +0200 Subject: [PATCH 11/13] Reworked paths --- src/node/hooks/express/specialpages.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 11d02c001..4053dbabd 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -90,7 +90,6 @@ 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]); } } @@ -106,7 +105,6 @@ 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]); } } @@ -116,13 +114,16 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) })); const hash = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/padbootstrap.js'))).digest('hex'); - const hashTimeSlider = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/timeSliderBootstrap.js'))).digest('hex'); + const hashTimeSlider = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/timesliderBootstrap.js'))).digest('hex'); const fileName = `padbootstrap-${hash.substring(0,16)}.min.js` - const fileNameTimeSlider = `timeSliderBootstrap-${hash.substring(0,16)}.min.js` + const fileNameTimeSlider = `timeSliderBootstrap-${hashTimeSlider.substring(0,16)}.min.js` + const outdir = path.join(settings.root, 'var','js') + + buildSync({ - entryPoints: [settings.root + "/var/js/padbootstrap.js"], // Entry file(s) + entryPoints: path.join(outdir, 'padbootstrap.js'), // Entry file(s) bundle: true, // Bundle the files together minify: false, // Minify the output sourcemap: true, // Generate source maps @@ -131,7 +132,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) metafile: true, write: true, // Do not write to file system, - outfile: settings.root + `/var/js/${fileName}`, // Output file + outfile: path.join(outdir,fileName), // Output file }) buildSync({ @@ -144,7 +145,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) metafile: true, write: true, // Do not write to file system, - outfile: settings.root + `/var/js/${fileNameTimeSlider}`, // Output file + outfile: path.join(outdir,fileNameTimeSlider), // Output file }) From 4ab21d90f74a484d9c75dd9f9ab0d2fc672b9e75 Mon Sep 17 00:00:00 2001 From: SamTv12345 Date: Tue, 16 Jul 2024 15:52:16 +0200 Subject: [PATCH 12/13] Fixed loading --- pnpm-lock.yaml | 12 +++ src/node/hooks/express/specialpages.ts | 113 +++++++++++++++---------- src/package.json | 7 +- 3 files changed, 82 insertions(+), 50 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09b4a8bc7..e6ad36935 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: cookie-parser: specifier: ^1.4.6 version: 1.4.6 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 cross-spawn: specifier: ^7.0.3 version: 7.0.3 @@ -2085,6 +2088,11 @@ packages: typescript: optional: true + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -6144,6 +6152,10 @@ snapshots: optionalDependencies: typescript: 5.5.3 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 4053dbabd..384da2cda 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -1,13 +1,13 @@ 'use strict'; -const path = require('path'); +import path from 'node:path'; const eejs = require('../../eejs') -const fs = require('fs'); +import fs from 'node:fs'; const fsp = fs.promises; const toolbar = require('../../utils/toolbar'); const hooks = require('../../../static/js/pluginfw/hooks'); const settings = require('../../utils/Settings'); -const util = require('util'); +import util from 'node:util'; const webaccess = require('./webaccess'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); import {hash, createHash} from 'node:crypto' @@ -83,91 +83,110 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); }); - await fsp.writeFile( - path.join(settings.root, 'var/js/padbootstrap.js'), - eejs.require('ep_etherpad-lite/templates/padBootstrap.js', { + + const padString = eejs.require('ep_etherpad-lite/templates/padBootstrap.js', { pluginModules: (() => { const pluginModules = new Set(); for (const part of plugins.parts) { for (const [, hookFnName] of Object.entries(part.client_hooks || {})) { + // @ts-ignore pluginModules.add(hookFnName.split(':')[0]); } } return [...pluginModules]; })(), settings, - })); + }) - await fsp.writeFile( - path.join(settings.root, 'var/js/timesliderBootstrap.js'), - eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', { + + const timeSliderString = eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', { pluginModules: (() => { const pluginModules = new Set(); for (const part of plugins.parts) { for (const [, hookFnName] of Object.entries(part.client_hooks || {})) { + // @ts-ignore pluginModules.add(hookFnName.split(':')[0]); } } return [...pluginModules]; })(), settings, - })); + }) + - const hash = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/padbootstrap.js'))).digest('hex'); - const hashTimeSlider = createHash('sha256').update(fs.readFileSync(path.join(settings.root, 'var/js/timesliderBootstrap.js'))).digest('hex'); - const fileName = `padbootstrap-${hash.substring(0,16)}.min.js` - const fileNameTimeSlider = `timeSliderBootstrap-${hashTimeSlider.substring(0,16)}.min.js` const outdir = path.join(settings.root, 'var','js') - - - buildSync({ - entryPoints: path.join(outdir, 'padbootstrap.js'), // Entry file(s) + const padWriteResult = buildSync({ + stdin: { + contents: padString, + resolveDir: path.join(settings.root, 'var','js'), + loader: 'js' + }, // Entry file(s) bundle: true, // Bundle the files together - minify: false, // Minify the output + minify: process.env.NODE_ENV === "production", // Minify the output + sourcemap: true, // Generate source maps + sourceRoot: settings.root+"/src/static/js/", + + target: ['es2020'], // Target ECMAScript version + metafile: true, + + write: false, // Do not write to file system, + }) + + const outputPadJS = padWriteResult.outputFiles[0].text + + const timeSliderWrite = buildSync({ + //entryPoints: [path.join(outdir, "timesliderBootstrap.js")], // Entry file(s), + stdin: { + contents: timeSliderString, + resolveDir: path.join(settings.root, 'var','js'), + loader: 'js' + }, + bundle: true, // Bundle the files together + minify: process.env.NODE_ENV === "production", // Minify the output sourcemap: true, // Generate source maps sourceRoot: settings.root+"/src/static/js/", target: ['es2020'], // Target ECMAScript version metafile: true, - write: true, // Do not write to file system, - outfile: path.join(outdir,fileName), // Output file + write: false, // Do not write to file system, }) - buildSync({ - entryPoints: [settings.root + "/var/js/timesliderBootstrap.js"], // Entry file(s) - bundle: true, // Bundle the files together - minify: false, // Minify the output - sourcemap: true, // Generate source maps - sourceRoot: settings.root+"/src/static/js/", - target: ['es2020'], // Target ECMAScript version - metafile: true, + const outputTimeslider = timeSliderWrite.outputFiles[0].text - write: true, // Do not write to file system, - outfile: path.join(outdir,fileNameTimeSlider), // Output file + const hash = createHash('sha256').update(outputPadJS).digest('hex').substring(0,8); + const hashTimeSlider = createHash('sha256').update(outputTimeslider).digest('hex').substring(0,8); + + const fileNamePad = `padbootstrap-${hash}.min.js` + const fileNameTimeSlider = `timeSliderBootstrap-${hashTimeSlider}.min.js` + const pathNamePad = path.join(outdir, fileNamePad) + const pathNameTimeSlider = path.join(outdir, fileNameTimeSlider) + + if (!fs.existsSync(pathNamePad)) { + fs.writeFileSync(pathNamePad, outputPadJS); + } + + if (!fs.existsSync(pathNameTimeSlider)) { + fs.writeFileSync(pathNameTimeSlider,outputTimeslider) + } + + args.app.get("/"+fileNamePad, (req: any, res: any) => { + res.sendFile(pathNamePad) }) - - args.app.get(`/${fileName}`, (req: any, res: any) => { - res.sendFile(settings.root+`/var/js/${fileName}`) + args.app.get("/"+fileNamePad+".map", (req: any, res: any) => { + res.sendFile(pathNamePad+".map") }) - args.app.get(`/${fileName}.map`, (req: any, res: any) => { - res.sendFile(settings.root+`/var/js/${fileName}.map`) + args.app.get("/"+fileNameTimeSlider, (req: any, res: any) => { + res.sendFile(pathNameTimeSlider) }) - args.app.get(`/${fileNameTimeSlider}`, (req: any, res: any) => { - res.sendFile(settings.root+`/var/js/${fileNameTimeSlider}`) + args.app.get("/"+fileNameTimeSlider+".map", (req: any, res: any) => { + res.sendFile(pathNameTimeSlider+".map") }) - args.app.get(`/${fileNameTimeSlider}.map`, (req: any, res: any) => { - res.sendFile(settings.root+`/var/js/${fileNameTimeSlider}.map`) - }) - - - - // serve pad.html under /p args.app.get('/p/:pad', (req: any, res: any, next: Function) => { @@ -185,7 +204,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) req, toolbar, isReadOnly, - entrypoint: "/"+fileName + entrypoint: "/"+fileNamePad })); }); diff --git a/src/package.json b/src/package.json index ab49c1ae7..ad5c95a69 100644 --- a/src/package.json +++ b/src/package.json @@ -75,7 +75,8 @@ "ueberdb2": "^4.2.82", "underscore": "1.13.6", "unorm": "1.6.0", - "wtfnode": "^0.9.3" + "wtfnode": "^0.9.3", + "cross-env": "^7.0.3" }, "bin": { "etherpad-healthcheck": "../bin/etherpad-healthcheck", @@ -124,8 +125,8 @@ "test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts ../node_modules/ep_*/static/tests/backend/specs/**", "test-utils": "mocha --import=tsx --timeout 5000 --recursive tests/backend/specs/*utils.ts", "test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api", - "dev": "node --require tsx/cjs node/server.ts", - "prod": "node --require tsx/cjs node/server.ts", + "dev": "cross-env NODE_ENV=development node --require tsx/cjs node/server.ts", + "prod": "cross-env NODE_ENV=production node --require tsx/cjs node/server.ts", "ts-check": "tsc --noEmit", "ts-check:watch": "tsc --noEmit --watch", "test-ui": "npx playwright test tests/frontend-new/specs", From 5cdf2277f0bad9ca963582cf9d3c6e9aa127932d Mon Sep 17 00:00:00 2001 From: SamTv12345 Date: Tue, 16 Jul 2024 16:23:19 +0200 Subject: [PATCH 13/13] Don't generate sources map in production mode --- src/node/hooks/express/specialpages.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 384da2cda..8ea64962a 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -125,19 +125,16 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) }, // Entry file(s) bundle: true, // Bundle the files together minify: process.env.NODE_ENV === "production", // Minify the output - sourcemap: true, // Generate source maps + sourcemap: !(process.env.NODE_ENV === "production"), // Generate source maps sourceRoot: settings.root+"/src/static/js/", - target: ['es2020'], // Target ECMAScript version metafile: true, - write: false, // Do not write to file system, }) const outputPadJS = padWriteResult.outputFiles[0].text const timeSliderWrite = buildSync({ - //entryPoints: [path.join(outdir, "timesliderBootstrap.js")], // Entry file(s), stdin: { contents: timeSliderString, resolveDir: path.join(settings.root, 'var','js'), @@ -145,18 +142,17 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) }, bundle: true, // Bundle the files together minify: process.env.NODE_ENV === "production", // Minify the output - sourcemap: true, // Generate source maps + sourcemap: !(process.env.NODE_ENV === "production"), // Generate source maps sourceRoot: settings.root+"/src/static/js/", target: ['es2020'], // Target ECMAScript version metafile: true, - write: false, // Do not write to file system, }) const outputTimeslider = timeSliderWrite.outputFiles[0].text - const hash = createHash('sha256').update(outputPadJS).digest('hex').substring(0,8); - const hashTimeSlider = createHash('sha256').update(outputTimeslider).digest('hex').substring(0,8); + const hash = padWriteResult.outputFiles[0].hash + const hashTimeSlider = timeSliderWrite.outputFiles[0].hash const fileNamePad = `padbootstrap-${hash}.min.js` const fileNameTimeSlider = `timeSliderBootstrap-${hashTimeSlider}.min.js` @@ -175,18 +171,10 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) res.sendFile(pathNamePad) }) - args.app.get("/"+fileNamePad+".map", (req: any, res: any) => { - res.sendFile(pathNamePad+".map") - }) - args.app.get("/"+fileNameTimeSlider, (req: any, res: any) => { res.sendFile(pathNameTimeSlider) }) - args.app.get("/"+fileNameTimeSlider+".map", (req: any, res: any) => { - res.sendFile(pathNameTimeSlider+".map") - }) - // serve pad.html under /p args.app.get('/p/:pad', (req: any, res: any, next: Function) => {