Feat/bundle js (#6511)

* Added minify

* Added POC for browser

* Moved first js files to ts

* Fixed caret positioning

* Added support for plugins

* Fixed get undefined.

* Removed require of socketio, l10n, html10n and error reporter

* Added minify

* Added POC for browser

* Moved first js files to ts

* Fixed caret positioning

* Added support for plugins

* Fixed get undefined.

* Removed require of socketio, l10n, html10n and error reporter

* Fixed popup not showing

* Fixed timeslider

* Reworked paths

* Fixed loading

* Don't generate sources map in production mode

* Non working hmr

* Added live reloading.

* Fixed timeslider when hot reloading

* Removed eval

* Fixed.

* Fixed env

* Fixed frontend tests.

* Added minifying via lightningcss

* Added minify via esbuild

* Fixed diagnostic url

* Removed lightningcss

* Fixed types

* Fixed alias

* Fixed loadtest

* Fixed

* Fixed loading ep_font_color3

* Restructure windows build

* Fixed windows build

* Fixed pnpm lock

---------

Co-authored-by: SamTv12345 <samtv12345@samtv12345.com>
This commit is contained in:
SamTV12345 2024-07-18 08:51:30 +02:00 committed by GitHub
parent 33b388b14c
commit d6d636955c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 2764 additions and 1763 deletions

View file

@ -68,7 +68,7 @@ jobs:
# rules. # rules.
- -
name: Install all dependencies and symlink for ep_etherpad-lite name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: pnpm i
#- #-
# name: Install etherpad plugins # name: Install etherpad plugins
# run: rm -Rf node_modules/ep_align/static/tests/* # run: rm -Rf node_modules/ep_align/static/tests/*
@ -92,7 +92,6 @@ jobs:
- name: Build admin frontend - name: Build admin frontend
working-directory: admin working-directory: admin
run: | run: |
pnpm install
pnpm run build pnpm run build
# name: Run the frontend admin tests # name: Run the frontend admin tests
# shell: bash # shell: bash
@ -124,7 +123,7 @@ jobs:
- name: Run the frontend admin tests - name: Run the frontend admin tests
shell: bash shell: bash
run: | run: |
pnpm run dev & pnpm run prod &
connected=false connected=false
can_connect() { can_connect() {
curl -sSfo /dev/null http://localhost:9001/ || return 1 curl -sSfo /dev/null http://localhost:9001/ || return 1

View file

@ -59,7 +59,7 @@ jobs:
- name: Run the frontend tests - name: Run the frontend tests
shell: bash shell: bash
run: | run: |
pnpm run dev & pnpm run prod &
connected=false connected=false
can_connect() { can_connect() {
curl -sSfo /dev/null http://localhost:9001/ || return 1 curl -sSfo /dev/null http://localhost:9001/ || return 1
@ -122,7 +122,7 @@ jobs:
- name: Run the frontend tests - name: Run the frontend tests
shell: bash shell: bash
run: | run: |
pnpm run dev & pnpm run prod &
connected=false connected=false
can_connect() { can_connect() {
curl -sSfo /dev/null http://localhost:9001/ || return 1 curl -sSfo /dev/null http://localhost:9001/ || return 1
@ -192,7 +192,7 @@ jobs:
- name: Run the frontend tests - name: Run the frontend tests
shell: bash shell: bash
run: | run: |
pnpm run dev & pnpm run prod &
connected=false connected=false
can_connect() { can_connect() {
curl -sSfo /dev/null http://localhost:9001/ || return 1 curl -sSfo /dev/null http://localhost:9001/ || return 1

View file

@ -154,11 +154,12 @@ jobs:
name: Run Etherpad name: Run Etherpad
working-directory: etherpad/src working-directory: etherpad/src
run: | run: |
pnpm install cypress pnpm i
.\node_modules\.bin\cypress.cmd install --force pnpm exec playwright install --with-deps
pnpm run prod & pnpm run prod &
curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test
pnpm exec cypress run --config-file ./tests/frontend/cypress/cypress.config.js pnpm exec playwright install chromium --with-deps
pnpm run test-ui --project=chromium
# On release, upload windows zip to GitHub release tab # On release, upload windows zip to GitHub release tab
- -
name: Rename to etherpad-lite-win.zip name: Rename to etherpad-lite-win.zip

View file

@ -50,21 +50,13 @@ rm -rf src/node_modules || true
#$(try cd ./bin/installDeps.sh) #$(try cd ./bin/installDeps.sh)
# Install admin frontend # Install admin frontend
cd admin
try pnpm install try pnpm install
try pnpm run build try pnpm run build:etherpad
cd ..
# Nuke the admin folder as it is not needed anymore :D # Nuke the admin folder as it is not needed anymore :D
rm -rf admin rm -rf admin
rm -rf oidc
export NODE_ENV=production rm -rf src/node_modules
try pnpm install --production
log "copy the windows settings template..." log "copy the windows settings template..."
try cp settings.json.template settings.json try cp settings.json.template settings.json

View file

@ -78,7 +78,7 @@ jobs:
- name: Run the frontend tests - name: Run the frontend tests
shell: bash shell: bash
run: | run: |
pnpm run dev & pnpm run prod &
connected=false connected=false
can_connect() { can_connect() {
curl -sSfo /dev/null http://localhost:9001/ || return 1 curl -sSfo /dev/null http://localhost:9001/ || return 1

View file

@ -45,4 +45,4 @@ fi
log "Starting Etherpad..." log "Starting Etherpad..."
# cd src # cd src
exec pnpm run dev "$@" exec pnpm run prod "$@"

View file

@ -28,7 +28,8 @@
"plugins": "pnpm --filter bin run plugins", "plugins": "pnpm --filter bin run plugins",
"install-plugins": "pnpm --filter bin run plugins i", "install-plugins": "pnpm --filter bin run plugins i",
"remove-plugins": "pnpm --filter bin run remove-plugins", "remove-plugins": "pnpm --filter bin run remove-plugins",
"list-plugins": "pnpm --filter bin run list-plugins" "list-plugins": "pnpm --filter bin run list-plugins",
"build:etherpad": "pnpm --filter admin run build-copy && pnpm --filter ui run build-copy"
}, },
"dependencies": { "dependencies": {
"ep_etherpad-lite": "workspace:./src" "ep_etherpad-lite": "workspace:./src"

View file

@ -152,6 +152,9 @@ importers:
cookie-parser: cookie-parser:
specifier: ^1.4.6 specifier: ^1.4.6
version: 1.4.6 version: 1.4.6
cross-env:
specifier: ^7.0.3
version: 7.0.3
cross-spawn: cross-spawn:
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3 version: 7.0.3
@ -294,6 +297,9 @@ importers:
'@types/http-errors': '@types/http-errors':
specifier: ^2.0.4 specifier: ^2.0.4
version: 2.0.4 version: 2.0.4
'@types/jquery':
specifier: ^3.5.30
version: 3.5.30
'@types/jsdom': '@types/jsdom':
specifier: ^21.1.7 specifier: ^21.1.7
version: 21.1.7 version: 21.1.7
@ -321,6 +327,9 @@ importers:
'@types/underscore': '@types/underscore':
specifier: ^1.11.15 specifier: ^1.11.15
version: 1.11.15 version: 1.11.15
chokidar:
specifier: ^3.6.0
version: 3.6.0
eslint: eslint:
specifier: ^9.7.0 specifier: ^9.7.0
version: 9.7.0 version: 9.7.0
@ -360,6 +369,9 @@ importers:
ui: ui:
devDependencies: devDependencies:
ep_etherpad-lite:
specifier: workspace:../src
version: link:../src
typescript: typescript:
specifier: ^5.5.3 specifier: ^5.5.3
version: 5.5.3 version: 5.5.3
@ -1489,6 +1501,9 @@ packages:
'@types/http-errors@2.0.4': '@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
'@types/jquery@3.5.30':
resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==}
'@types/jsdom@21.1.7': '@types/jsdom@21.1.7':
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
@ -1576,6 +1591,9 @@ packages:
'@types/sinonjs__fake-timers@8.1.5': '@types/sinonjs__fake-timers@8.1.5':
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
'@types/sizzle@2.3.8':
resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==}
'@types/superagent@8.1.7': '@types/superagent@8.1.7':
resolution: {integrity: sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==} resolution: {integrity: sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==}
@ -2080,6 +2098,11 @@ packages:
typescript: typescript:
optional: true 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: cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -5456,6 +5479,10 @@ snapshots:
'@types/http-errors@2.0.4': {} '@types/http-errors@2.0.4': {}
'@types/jquery@3.5.30':
dependencies:
'@types/sizzle': 2.3.8
'@types/jsdom@21.1.7': '@types/jsdom@21.1.7':
dependencies: dependencies:
'@types/node': 20.14.11 '@types/node': 20.14.11
@ -5558,6 +5585,8 @@ snapshots:
'@types/sinonjs__fake-timers@8.1.5': {} '@types/sinonjs__fake-timers@8.1.5': {}
'@types/sizzle@2.3.8': {}
'@types/superagent@8.1.7': '@types/superagent@8.1.7':
dependencies: dependencies:
'@types/cookiejar': 2.1.5 '@types/cookiejar': 2.1.5
@ -6137,6 +6166,10 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.5.3 typescript: 5.5.3
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.3
cross-spawn@7.0.3: cross-spawn@7.0.3:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1

View file

@ -42,7 +42,8 @@
"name": "specialpages", "name": "specialpages",
"hooks": { "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages", "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
"expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages" "expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages",
"socketio": "ep_etherpad-lite/node/hooks/express/specialpages"
} }
}, },
{ {

View file

@ -996,7 +996,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
percentageToScrollWhenUserPressesArrowUp: percentageToScrollWhenUserPressesArrowUp:
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
}, },
initialChangesets: [], // FIXME: REMOVE THIS SHIT initialChangesets: [], // FIXME: REMOVE THIS SHIT,
mode: process.env.NODE_ENV
}; };
// Add a username to the clientVars if one avaiable // Add a username to the clientVars if one avaiable

View file

@ -1,14 +1,23 @@
'use strict'; 'use strict';
const path = require('path'); import path from 'node:path';
const eejs = require('../../eejs'); const eejs = require('../../eejs')
const fs = require('fs'); import fs from 'node:fs';
const fsp = fs.promises; const fsp = fs.promises;
const toolbar = require('../../utils/toolbar'); const toolbar = require('../../utils/toolbar');
const hooks = require('../../../static/js/pluginfw/hooks'); const hooks = require('../../../static/js/pluginfw/hooks');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const util = require('util'); import util from 'node:util';
const webaccess = require('./webaccess'); const webaccess = require('./webaccess');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
import {build, buildSync} from 'esbuild'
let ioI: { sockets: { sockets: any[]; }; } | null = null
exports.socketio = (hookName: string, {io}: any) => {
ioI = io
}
exports.expressPreSession = async (hookName:string, {app}:any) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// This endpoint is intended to conform to: // This endpoint is intended to conform to:
@ -73,33 +82,245 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
}); });
}; };
exports.expressCreateServer = (hookName:string, args:any, cb:Function) => {
// serve index.html under /
args.app.get('/', (req:any, res:any) => { const convertTypescript = (content: string) => {
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); const outputRaw = buildSync({
stdin: {
contents: content,
resolveDir: path.join(settings.root, 'var','js'),
loader: 'js'
},
alias:{
"ep_etherpad-lite/static/js/browser": 'ep_etherpad-lite/static/js/vendors/browser',
"ep_etherpad-lite/static/js/nice-select": 'ep_etherpad-lite/static/js/vendors/nice-select'
},
bundle: true, // Bundle the files together
minify: process.env.NODE_ENV === "production", // Minify the output
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 output = outputRaw.outputFiles[0].text
return {
output,
hash: outputRaw.outputFiles[0].hash.replaceAll('/','2')
}
}
const handleLiveReload = async (args: any, padString: string, timeSliderString: string ) => {
const chokidar = await import('chokidar')
const watcher = chokidar.watch(path.join(settings.root, 'src', 'static', 'js'));
let routeHandlers: { [key: string]: Function } = {};
const setRouteHandler = (path: string, newHandler: Function) => {
routeHandlers[path] = newHandler;
};
args.app.use((req: any, res: any, next: Function) => {
if (req.path.startsWith('/p/') && req.path.split('/').length == 3) {
req.params = {
pad: req.path.split('/')[2]
}
routeHandlers['/p/:pad'](req, res);
} else if (req.path.startsWith('/p/') && req.path.split('/').length == 4) {
req.params = {
pad: req.path.split('/')[2]
}
routeHandlers['/p/:pad/timeslider'](req, res);
} else if (routeHandlers[req.path]) {
routeHandlers[req.path](req, res);
} else {
next();
}
}); });
// serve pad.html under /p function handleUpdate() {
args.app.get('/p/:pad', (req:any, res:any, next:Function) => { convertTypescriptWatched(padString, (output, hash) => {
console.log("New pad hash is", hash)
setRouteHandler('/watch/pad', (req: any, res: any) => {
res.header('Content-Type', 'application/javascript');
res.send(output)
})
setRouteHandler("/p/:pad", (req: any, res: any, next: Function) => {
// The below might break for pads being rewritten // The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req); const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', { hooks.callAll('padInitToolbar', {
toolbar, toolbar,
isReadOnly, isReadOnly
}); });
// can be removed when require-kernel is dropped // can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\''); res.header('Feature-Policy', 'sync-xhr \'self\'');
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', { const content = eejs.require('ep_etherpad-lite/templates/pad.html', {
req, req,
toolbar, toolbar,
isReadOnly, isReadOnly,
})); entrypoint: '/watch/pad?hash=' + hash
})
res.send(content);
})
ioI!.sockets.sockets.forEach(socket => socket.emit('liveupdate'))
})
convertTypescriptWatched(timeSliderString, (output, hash) => {
// serve timeslider.html under /p/$padname/timeslider
console.log("New timeslider hash is", hash)
setRouteHandler('/watch/timeslider', (req: any, res: any) => {
res.header('Content-Type', 'application/javascript');
res.send(output)
})
setRouteHandler("/p/:pad/timeslider", (req: any, res: any, next: Function) => {
console.log("Reloading pad")
// The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', {
toolbar,
isReadOnly
});
// can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\'');
const content = eejs.require('ep_etherpad-lite/templates/timeslider.html', {
req,
toolbar,
isReadOnly,
entrypoint: '/watch/timeslider?hash=' + hash
})
res.send(content);
})
})
}
watcher.on('change', path => {
console.log(`File ${path} has been changed`);
handleUpdate();
});
handleUpdate()
}
const convertTypescriptWatched = (content: string, cb: (output:string, hash: string)=>void) => {
build({
stdin: {
contents: content,
resolveDir: path.join(settings.root, 'var','js'),
loader: 'js'
},
alias:{
"ep_etherpad-lite/static/js/browser": 'ep_etherpad-lite/static/js/vendors/browser',
"ep_etherpad-lite/static/js/nice-select": 'ep_etherpad-lite/static/js/vendors/nice-select'
},
bundle: true, // Bundle the files together
minify: process.env.NODE_ENV === "production", // Minify the output
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,
}).then((outputRaw) => {
cb(
outputRaw.outputFiles[0].text,
outputRaw.outputFiles[0].hash.replaceAll('/','2')
)
})
}
exports.expressCreateServer = async (hookName: string, args: any, cb: Function) => {
// serve index.html under /
args.app.get('/', (req: any, res: any) => {
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
});
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,
})
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 outdir = path.join(settings.root, 'var','js')
let fileNamePad: string
let fileNameTimeSlider: string
if(process.env.NODE_ENV === "production"){
const padSliderWrite = convertTypescript(padString)
const timeSliderWrite = convertTypescript(timeSliderString)
fileNamePad = `padbootstrap-${padSliderWrite.hash}.min.js`
fileNameTimeSlider = `timeSliderBootstrap-${timeSliderWrite.hash}.min.js`
const pathNamePad = path.join(outdir, fileNamePad)
const pathNameTimeSlider = path.join(outdir, fileNameTimeSlider)
if (!fs.existsSync(pathNamePad)) {
fs.writeFileSync(pathNamePad, padSliderWrite.output);
}
if (!fs.existsSync(pathNameTimeSlider)) {
fs.writeFileSync(pathNameTimeSlider,timeSliderWrite.output)
}
args.app.get("/"+fileNamePad, (req: any, res: any) => {
res.sendFile(pathNamePad)
})
args.app.get("/"+fileNameTimeSlider, (req: any, res: any) => {
res.sendFile(pathNameTimeSlider)
})
// serve pad.html under /p
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);
hooks.callAll('padInitToolbar', {
toolbar,
isReadOnly
});
// can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\'');
const content = eejs.require('ep_etherpad-lite/templates/pad.html', {
req,
toolbar,
isReadOnly,
entrypoint: "/"+fileNamePad
})
res.send(content);
}); });
// serve timeslider.html under /p/$padname/timeslider // 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', { hooks.callAll('padInitToolbar', {
toolbar, toolbar,
}); });
@ -107,15 +328,17 @@ exports.expressCreateServer = (hookName:string, args:any, cb:Function) => {
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
req, req,
toolbar, toolbar,
entrypoint: "/"+fileNameTimeSlider
})); }));
}); });
} else {
await handleLiveReload(args, padString, timeSliderString)
}
// The client occasionally polls this endpoint to get an updated expiration for the express_sid // 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. // 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. // express-session automatically calls req.session.touch() so we don't need to do it here.
res.json({status: 'ok'}); res.json({status: 'ok'});
}); });
return cb();
}; };

View file

@ -300,12 +300,12 @@ const getFileCompressed = async (filename, contentType) => {
try { try {
logger.info('Compress CSS file %s.', filename); logger.info('Compress CSS file %s.', filename);
const compressResult = await compressCSS(content); const compressResult = await compressCSS(path.resolve(ROOT_DIR,filename));
if (compressResult.error) { if (compressResult.error) {
console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error); console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error);
} else { } else {
content = compressResult.code.toString(); // Convert content obj code to string content = compressResult
} }
} catch (error) { } catch (error) {
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`); console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);

View file

@ -3,9 +3,8 @@
* Worker thread to minify JS & CSS files out of the main NodeJS thread * Worker thread to minify JS & CSS files out of the main NodeJS thread
*/ */
const fsp = require('fs').promises;
import {expose} from 'threads' import {expose} from 'threads'
import {transform} from 'esbuild'; import {build, transform} from 'esbuild';
/* /*
* Minify JS content * Minify JS content
@ -21,8 +20,23 @@ const compressJS = async (content) => {
* @param {string} ROOT_DIR - the root dir of Etherpad * @param {string} ROOT_DIR - the root dir of Etherpad
*/ */
const compressCSS = async (content) => { const compressCSS = async (content) => {
return await transform(content, {loader: 'css', minify: true}); const transformedCSS = await build(
{
entryPoints: [content],
minify: true,
bundle: true,
loader:{
'.ttf': 'dataurl',
'.otf': 'dataurl',
'.woff': 'dataurl',
'.woff2': 'dataurl',
'.eot': 'dataurl',
'.svg': 'dataurl'
},
write: false
}
)
return transformedCSS.outputFiles[0].text
}; };
expose({ expose({

View file

@ -2,7 +2,7 @@
/** /**
* The Toolbar Module creates and renders the toolbars and buttons * 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) => { const removeItem = (array: string[], what: string) => {
let ax; let ax;
@ -21,7 +21,7 @@ const defaultButtonAttributes = (name: string, overrides?: boolean) => ({
const tag = (name: string, attributes: AttributeObj, contents?: string) => { const tag = (name: string, attributes: AttributeObj, contents?: string) => {
const aStr = tagAttributes(attributes); const aStr = tagAttributes(attributes);
if (_.isString(contents) && contents!.length > 0) { if (isString(contents) && contents!.length > 0) {
return `<${name}${aStr}>${contents}</${name}>`; return `<${name}${aStr}>${contents}</${name}>`;
} else { } else {
return `<${name}${aStr}></${name}>`; return `<${name}${aStr}></${name}>`;
@ -34,14 +34,14 @@ type AttributeObj = {
} }
const tagAttributes = (attributes: AttributeObj) => { const tagAttributes = (attributes: AttributeObj) => {
attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => { attributes = reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
if (!_.isUndefined(val)) { if (!isUndefined(val)) {
o[name] = val; o[name] = val;
} }
return o; 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 = { type ButtonGroupType = {
@ -58,7 +58,7 @@ class ButtonGroup {
public static fromArray = function (array: string[]) { public static fromArray = function (array: string[]) {
const btnGroup = new ButtonGroup(); const btnGroup = new ButtonGroup();
_.each(array, (btnName: string) => { each(array, (btnName: string) => {
const button = Button.load(btnName) as Button const button = Button.load(btnName) as Button
btnGroup.addButton(button); btnGroup.addButton(button);
}); });
@ -70,18 +70,19 @@ class ButtonGroup {
return this; return this;
} }
render() { render(): string {
if (this.buttons && this.buttons.length === 1) { if (this.buttons && this.buttons.length === 1) {
this.buttons[0].grouping = ''; this.buttons[0].grouping = '';
} else if (this.buttons && this.buttons.length > 1) { } else if (this.buttons && this.buttons.length > 1) {
_.first(this.buttons).grouping = 'grouped-left'; first(this.buttons)!.grouping = 'grouped-left';
_.last(this.buttons).grouping = 'grouped-right'; last(this.buttons)!.grouping = 'grouped-right';
_.each(this.buttons.slice(1, -1), (btn: Button) => { each(this.buttons.slice(1, -1), (btn: Button) => {
btn.grouping = 'grouped-middle'; btn.grouping = 'grouped-middle';
}); });
} }
return _.map(this.buttons, (btn: ButtonGroup) => { // @ts-ignore
return map(this.buttons, (btn: ButtonGroup) => {
if (btn) return btn.render(); if (btn) return btn.render();
}).join('\n'); }).join('\n');
} }
@ -151,8 +152,8 @@ class SelectButton extends Button {
select(attributes: AttributeObj) { select(attributes: AttributeObj) {
const options: string[] = []; const options: string[] = [];
_.each(this.options, (opt: AttributeSelect) => { each(this.options, (opt: AttributeSelect) => {
const a = _.extend({ const a = extend({
value: opt.value, value: opt.value,
}, opt.attributes); }, opt.attributes);
@ -299,7 +300,7 @@ module.exports = {
buttons[0].push('savedrevision'); 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()); return groups.join(this.separator());
}, },
}; };

View file

@ -34,6 +34,7 @@
"async": "^3.2.5", "async": "^3.2.5",
"axios": "^1.7.2", "axios": "^1.7.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"esbuild": "^0.23.0", "esbuild": "^0.23.0",
@ -87,6 +88,7 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.5",
"@types/http-errors": "^2.0.4", "@types/http-errors": "^2.0.4",
"@types/jquery": "^3.5.30",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.7", "@types/mocha": "^10.0.7",
@ -96,6 +98,7 @@
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/underscore": "^1.11.15", "@types/underscore": "^1.11.15",
"chokidar": "^3.6.0",
"eslint": "^9.7.0", "eslint": "^9.7.0",
"eslint-config-etherpad": "^4.0.4", "eslint-config-etherpad": "^4.0.4",
"etherpad-cli-client": "^3.0.2", "etherpad-cli-client": "^3.0.2",
@ -120,17 +123,17 @@
}, },
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts ../node_modules/ep_*/static/tests/backend/specs/**", "test": "cross-env NODE_ENV=production 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-utils": "cross-env NODE_ENV=production mocha --import=tsx --timeout 5000 --recursive tests/backend/specs/*utils.ts",
"test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api", "test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api",
"dev": "node --require tsx/cjs node/server.ts", "dev": "cross-env NODE_ENV=development node --require tsx/cjs node/server.ts",
"prod": "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": "tsc --noEmit",
"ts-check:watch": "tsc --noEmit --watch", "ts-check:watch": "tsc --noEmit --watch",
"test-ui": "npx playwright test tests/frontend-new/specs", "test-ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs",
"test-ui:ui": "npx playwright test tests/frontend-new/specs --ui", "test-ui:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs --ui",
"test-admin": "npx playwright test tests/frontend-new/admin-spec --workers 1", "test-admin": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --workers 1",
"test-admin:ui": "npx playwright test tests/frontend-new/admin-spec --ui --workers 1", "test-admin:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --ui --workers 1",
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts" "debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts"
}, },
"version": "2.1.1", "version": "2.1.1",

View file

@ -4,7 +4,7 @@ const AttributeMap = require('./AttributeMap');
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils'); const ChangesetUtils = require('./ChangesetUtils');
const attributes = require('./attributes'); const attributes = require('./attributes');
const _ = require('./underscore'); const underscore = require("underscore")
const lineMarkerAttribute = 'lmkr'; const lineMarkerAttribute = 'lmkr';
@ -45,7 +45,7 @@ const AttributeManager = function (rep, applyChangesetCallback) {
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES; AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
AttributeManager.lineAttributes = lineAttributes; AttributeManager.lineAttributes = lineAttributes;
AttributeManager.prototype = _(AttributeManager.prototype).extend({ AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({
applyChangeset(changeset) { applyChangeset(changeset) {
if (!this.applyChangesetCallback) return changeset; if (!this.applyChangesetCallback) return changeset;
@ -335,7 +335,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); 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(); .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 // if we have marker and any of attributes don't need to have marker. we need delete it

View file

@ -27,9 +27,10 @@
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const makeCSSManager = require('./cssmanager').makeCSSManager; const makeCSSManager = require('./cssmanager').makeCSSManager;
const pluginUtils = require('./pluginfw/shared'); const pluginUtils = require('./pluginfw/shared');
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
const debugLog = (...args) => {}; 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. // 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 // 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. // errors out unless given an absolute URL for a JavaScript-created element.
@ -257,19 +258,19 @@ const Ace2Editor = function () {
// <head> tag // <head> tag
addStyleTagsFor(innerDocument, includedCSS); addStyleTagsFor(innerDocument, includedCSS);
const requireKernel = innerDocument.createElement('script'); //const requireKernel = innerDocument.createElement('script');
requireKernel.type = 'text/javascript'; //requireKernel.type = 'text/javascript';
requireKernel.src = //requireKernel.src =
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`); // absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
innerDocument.head.appendChild(requireKernel); //innerDocument.head.appendChild(requireKernel);
// Pre-fetch modules to improve load performance. // 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'); const script = innerDocument.createElement('script');
script.type = 'text/javascript'; script.type = 'text/javascript';
script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` + script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
`?callback=require.define&v=${clientVars.randomVersionString}`); `?callback=require.define&v=${clientVars.randomVersionString}`);
innerDocument.head.appendChild(script); innerDocument.head.appendChild(script);
} }*/
const innerStyle = innerDocument.createElement('style'); const innerStyle = innerDocument.createElement('style');
innerStyle.type = 'text/css'; innerStyle.type = 'text/css';
innerStyle.title = 'dynamicsyntax'; innerStyle.title = 'dynamicsyntax';
@ -284,7 +285,7 @@ const Ace2Editor = function () {
innerDocument.body.classList.add('innerdocbody'); innerDocument.body.classList.add('innerdocbody');
innerDocument.body.setAttribute('spellcheck', 'false'); innerDocument.body.setAttribute('spellcheck', 'false');
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); // &nbsp; innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); // &nbsp;
/*
debugLog('Ace2Editor.init() waiting for require kernel load'); debugLog('Ace2Editor.init() waiting for require kernel load');
await eventFired(requireKernel, 'load'); await eventFired(requireKernel, 'load');
debugLog('Ace2Editor.init() require kernel loaded'); debugLog('Ace2Editor.init() require kernel loaded');
@ -292,17 +293,16 @@ const Ace2Editor = function () {
require.setRootURI(absUrl('../javascripts/src')); require.setRootURI(absUrl('../javascripts/src'));
require.setLibraryURI(absUrl('../javascripts/lib')); require.setLibraryURI(absUrl('../javascripts/lib'));
require.setGlobalKeyPath('require'); require.setGlobalKeyPath('require');
*/
// intentially moved before requiring client_plugins to save a 307 // intentially moved before requiring client_plugins to save a 307
innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner'); innerWindow.Ace2Inner = ace2_inner;
innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); innerWindow.plugins = cl_plugins;
innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow);
innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery;
debugLog('Ace2Editor.init() waiting for plugins'); debugLog('Ace2Editor.init() waiting for plugins');
await new Promise((resolve, reject) => innerWindow.plugins.ensure( /*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
(err) => err != null ? reject(err) : resolve())); (err) => err != null ? reject(err) : resolve()));*/
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
await innerWindow.Ace2Inner.init(info, { await innerWindow.Ace2Inner.init(info, {
inner: makeCSSManager(innerStyle.sheet), inner: makeCSSManager(innerStyle.sheet),

View file

@ -30,6 +30,8 @@ const setAssoc = Ace2Common.setAssoc;
const noop = Ace2Common.noop; const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
import Scroll from './scroll'
function Ace2Inner(editorInfo, cssManagers) { function Ace2Inner(editorInfo, cssManagers) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker; const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;
@ -42,7 +44,6 @@ function Ace2Inner(editorInfo, cssManagers) {
const SkipList = require('./skiplist'); const SkipList = require('./skiplist');
const undoModule = require('./undomodule').undoModule; const undoModule = require('./undomodule').undoModule;
const AttributeManager = require('./AttributeManager'); const AttributeManager = require('./AttributeManager');
const Scroll = require('./scroll');
const DEBUG = false; const DEBUG = false;
const THE_TAB = ' '; // 4 const THE_TAB = ' '; // 4
@ -54,13 +55,16 @@ function Ace2Inner(editorInfo, cssManagers) {
let thisAuthor = ''; let thisAuthor = '';
let disposed = false; 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 = () => { const focus = () => {
window.focus(); targetBody.focus();
}; };
const outerWin = window.parent; const outerDoc = outerWin.contentWindow.document;
const outerDoc = outerWin.document;
const sideDiv = outerDoc.getElementById('sidediv'); const sideDiv = outerDoc.getElementById('sidediv');
const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv'); const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv');
const sideDivInner = outerDoc.getElementById('sidedivinner'); const sideDivInner = outerDoc.getElementById('sidedivinner');
@ -74,7 +78,7 @@ function Ace2Inner(editorInfo, cssManagers) {
}; };
appendNewSideDivLine(); appendNewSideDivLine();
const scroll = Scroll.init(outerWin); const scroll = new Scroll(outerWin);
let outsideKeyDown = noop; let outsideKeyDown = noop;
let outsideKeyPress = (e) => true; let outsideKeyPress = (e) => true;
@ -415,7 +419,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const setWraps = (newVal) => { const setWraps = (newVal) => {
doesWrap = newVal; doesWrap = newVal;
document.body.classList.toggle('doesWrap', doesWrap); targetBody.classList.toggle('doesWrap', doesWrap);
scheduler.setTimeout(() => { scheduler.setTimeout(() => {
inCallStackIfNecessary('setWraps', () => { inCallStackIfNecessary('setWraps', () => {
fastIncorp(7); fastIncorp(7);
@ -445,7 +449,7 @@ function Ace2Inner(editorInfo, cssManagers) {
}; };
const setTextFace = (face) => { const setTextFace = (face) => {
document.body.style.fontFamily = face; targetBody.style.fontFamily = face;
lineMetricsDiv.style.fontFamily = face; lineMetricsDiv.style.fontFamily = face;
}; };
@ -456,8 +460,8 @@ function Ace2Inner(editorInfo, cssManagers) {
const setEditable = (newVal) => { const setEditable = (newVal) => {
isEditable = newVal; isEditable = newVal;
document.body.contentEditable = isEditable ? 'true' : 'false'; targetBody.contentEditable = isEditable ? 'true' : 'false';
document.body.classList.toggle('static', !isEditable); targetBody.classList.toggle('static', !isEditable);
}; };
const enforceEditability = () => setEditable(isEditable); const enforceEditability = () => setEditable(isEditable);
@ -480,6 +484,7 @@ function Ace2Inner(editorInfo, cssManagers) {
newText = `${lines.join('\n')}\n`; newText = `${lines.join('\n')}\n`;
} }
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
setDocText(newText); setDocText(newText);
}); });
@ -640,8 +645,8 @@ function Ace2Inner(editorInfo, cssManagers) {
// These properties are exposed // These properties are exposed
const setters = { const setters = {
wraps: setWraps, wraps: setWraps,
showsauthorcolors: (val) => document.body.classList.toggle('authorColors', !!val), showsauthorcolors: (val) => targetBody.classList.toggle('authorColors', !!val),
showsuserselections: (val) => document.body.classList.toggle('userSelections', !!val), showsuserselections: (val) => targetBody.classList.toggle('userSelections', !!val),
showslinenumbers: (value) => { showslinenumbers: (value) => {
hasLineNumbers = !!value; hasLineNumbers = !!value;
sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers); sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers);
@ -654,8 +659,8 @@ function Ace2Inner(editorInfo, cssManagers) {
styled: setStyled, styled: setStyled,
textface: setTextFace, textface: setTextFace,
rtlistrue: (value) => { rtlistrue: (value) => {
document.body.classList.toggle('rtl', value); targetBody.classList.toggle('rtl', value);
document.body.classList.toggle('ltr', !value); targetBody.classList.toggle('ltr', !value);
document.documentElement.dir = value ? 'rtl' : 'ltr'; document.documentElement.dir = value ? 'rtl' : 'ltr';
}, },
}; };
@ -894,11 +899,11 @@ function Ace2Inner(editorInfo, cssManagers) {
clearObservedChanges(); clearObservedChanges();
const getCleanNodeByKey = (key) => { const getCleanNodeByKey = (key) => {
let n = document.getElementById(key); let n = targetDoc.getElementById(key);
// copying and pasting can lead to duplicate ids // copying and pasting can lead to duplicate ids
while (n && isNodeDirty(n)) { while (n && isNodeDirty(n)) {
n.id = ''; n.id = '';
n = document.getElementById(key); n = targetDoc.getElementById(key);
} }
return n; return n;
}; };
@ -980,11 +985,11 @@ function Ace2Inner(editorInfo, cssManagers) {
const observeSuspiciousNodes = () => { const observeSuspiciousNodes = () => {
// inspired by Firefox bug #473255, where pasting formatted text // inspired by Firefox bug #473255, where pasting formatted text
// causes the cursor to jump away, making the new HTML never found. // causes the cursor to jump away, making the new HTML never found.
if (document.body.getElementsByTagName) { if (targetBody.getElementsByTagName) {
const elts = document.body.getElementsByTagName('style'); const elts = targetBody.getElementsByTagName('style');
for (const elt of elts) { for (const elt of elts) {
const n = topLevel(elt); const n = topLevel(elt);
if (n && n.parentNode === document.body) { if (n && n.parentNode === targetBody) {
observeChangesAroundNode(n); observeChangesAroundNode(n);
} }
} }
@ -999,8 +1004,8 @@ function Ace2Inner(editorInfo, cssManagers) {
if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
// returns true if dom changes were made // returns true if dom changes were made
if (!document.body.firstChild) { if (!targetBody.firstChild) {
document.body.innerHTML = '<div><!-- --></div>'; targetBody.innerHTML = '<div><!-- --></div>';
} }
observeChangesAroundSelection(); observeChangesAroundSelection();
@ -1022,7 +1027,7 @@ function Ace2Inner(editorInfo, cssManagers) {
j++; j++;
} }
if (!dirtyRangesCheckOut) { 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)))) { if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) {
observeChangesAroundNode(bodyNode); observeChangesAroundNode(bodyNode);
} }
@ -1044,11 +1049,11 @@ function Ace2Inner(editorInfo, cssManagers) {
const range = dirtyRanges[i]; const range = dirtyRanges[i];
a = range[0]; a = range[0];
b = range[1]; b = range[1];
let firstDirtyNode = (((a === 0) && document.body.firstChild) || let firstDirtyNode = (((a === 0) && targetBody.firstChild) ||
getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); 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); getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
@ -1135,7 +1140,7 @@ function Ace2Inner(editorInfo, cssManagers) {
callstack: currentCallStack, callstack: currentCallStack,
editorInfo, editorInfo,
rep, rep,
root: document.body, root: targetBody,
point: selection.startPoint, point: selection.startPoint,
documentAttributeManager, documentAttributeManager,
}); });
@ -1147,7 +1152,7 @@ function Ace2Inner(editorInfo, cssManagers) {
callstack: currentCallStack, callstack: currentCallStack,
editorInfo, editorInfo,
rep, rep,
root: document.body, root: targetBody,
point: selection.endPoint, point: selection.endPoint,
documentAttributeManager, documentAttributeManager,
}); });
@ -1227,9 +1232,9 @@ function Ace2Inner(editorInfo, cssManagers) {
info.prepareForAdd(); info.prepareForAdd();
entry.lineMarker = info.lineMarker; entry.lineMarker = info.lineMarker;
if (!nodeToAddAfter) { if (!nodeToAddAfter) {
document.body.insertBefore(node, document.body.firstChild); targetBody.insertBefore(node, targetBody.firstChild);
} else { } else {
document.body.insertBefore(node, nodeToAddAfter.nextSibling); targetBody.insertBefore(node, nodeToAddAfter.nextSibling);
} }
nodeToAddAfter = node; nodeToAddAfter = node;
info.notifyAdded(); info.notifyAdded();
@ -1326,7 +1331,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// Turn DOM node selection into [line,char] selection. // Turn DOM node selection into [line,char] selection.
// This method has to work when the DOM is not pristine, // This method has to work when the DOM is not pristine,
// assuming the point is not in a dirty node. // assuming the point is not in a dirty node.
if (point.node === document.body) { if (point.node === targetBody) {
if (point.index === 0) { if (point.index === 0) {
return [0, 0]; return [0, 0];
} else { } else {
@ -1345,7 +1350,7 @@ function Ace2Inner(editorInfo, cssManagers) {
col = nodeText(n).length; col = nodeText(n).length;
} }
let parNode, prevSib; let parNode, prevSib;
while ((parNode = n.parentNode) !== document.body) { while ((parNode = n.parentNode) !== targetBody) {
if ((prevSib = n.previousSibling)) { if ((prevSib = n.previousSibling)) {
n = prevSib; n = prevSib;
col += nodeText(n).length; col += nodeText(n).length;
@ -1398,7 +1403,7 @@ function Ace2Inner(editorInfo, cssManagers) {
insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo)); insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo));
for (const k of keysToDelete) { for (const k of keysToDelete) {
const n = document.getElementById(k); const n = targetDoc.getElementById(k);
n.parentNode.removeChild(n); n.parentNode.removeChild(n);
} }
@ -2087,7 +2092,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const a = cleanNodeForIndex(i - 1); const a = cleanNodeForIndex(i - 1);
const b = cleanNodeForIndex(i); const b = cleanNodeForIndex(i);
if ((!a) || (!b)) return false; // violates precondition 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 ((a === true) && b.previousSibling) return false;
if ((b === true) && a.nextSibling) return false; if ((b === true) && a.nextSibling) return false;
if ((a === true) || (b === true)) return true; if ((a === true) || (b === true)) return true;
@ -2232,7 +2237,7 @@ function Ace2Inner(editorInfo, cssManagers) {
}; };
const isNodeDirty = (n) => { const isNodeDirty = (n) => {
if (n.parentNode !== document.body) return true; if (n.parentNode !== targetBody) return true;
const data = getAssoc(n, 'dirtiness'); const data = getAssoc(n, 'dirtiness');
if (!data) return true; if (!data) return true;
if (n.id !== data.nodeId) return true; if (n.id !== data.nodeId) return true;
@ -2856,7 +2861,7 @@ function Ace2Inner(editorInfo, cssManagers) {
updateBrowserSelectionFromRep(); updateBrowserSelectionFromRep();
// get the current caret selection, can't use rep. here because that only gives // get the current caret selection, can't use rep. here because that only gives
// us the start position not the current // us the start position not the current
const myselection = document.getSelection(); const myselection = targetDoc.getSelection();
// get the carets selection offset in px IE 214 // get the carets selection offset in px IE 214
let caretOffsetTop = myselection.focusNode.parentNode.offsetTop || let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
myselection.focusNode.offsetTop; myselection.focusNode.offsetTop;
@ -2970,13 +2975,13 @@ function Ace2Inner(editorInfo, cssManagers) {
// with background doesn't seem to show up... // with background doesn't seem to show up...
if (isNodeText(p.node) && p.index === p.maxIndex) { if (isNodeText(p.node) && p.index === p.maxIndex) {
let n = p.node; 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; n = n.parentNode;
} }
if (n.nextSibling && if (n.nextSibling &&
!(typeof n.nextSibling.tagName === 'string' && !(typeof n.nextSibling.tagName === 'string' &&
n.nextSibling.tagName.toLowerCase() === 'br') && 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 // found a parent, go to next node and dive in
p.node = n.nextSibling; p.node = n.nextSibling;
p.maxIndex = nodeMaxIndex(p.node); p.maxIndex = nodeMaxIndex(p.node);
@ -3003,7 +3008,7 @@ function Ace2Inner(editorInfo, cssManagers) {
}; };
} }
}; };
const browserSelection = window.getSelection(); const browserSelection = targetDoc.getSelection();
if (browserSelection) { if (browserSelection) {
browserSelection.removeAllRanges(); browserSelection.removeAllRanges();
if (selection) { if (selection) {
@ -3078,7 +3083,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// each of which has node (a magicdom node), index, and maxIndex. If the node // 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. // is a text node, maxIndex is the length of the text; else maxIndex is 1.
// index is between 0 and maxIndex, inclusive. // index is between 0 and maxIndex, inclusive.
const browserSelection = window.getSelection(); const browserSelection = targetDoc.getSelection();
if (!browserSelection || browserSelection.type === 'None' || if (!browserSelection || browserSelection.type === 'None' ||
browserSelection.rangeCount === 0) { browserSelection.rangeCount === 0) {
return null; return null;
@ -3096,7 +3101,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (!isInBody(container)) { if (!isInBody(container)) {
// command-click in Firefox selects whole document, HEAD and BODY! // command-click in Firefox selects whole document, HEAD and BODY!
return { return {
node: document.body, node: targetBody,
index: 0, index: 0,
maxIndex: 1, maxIndex: 1,
}; };
@ -3146,7 +3151,7 @@ function Ace2Inner(editorInfo, cssManagers) {
browserSelection.anchorOffset === range.endOffset, browserSelection.anchorOffset === range.endOffset,
}; };
if (selection.startPoint.node.ownerDocument !== window.document) { if (selection.startPoint.node.ownerDocument !== targetDoc) {
return null; return null;
} }
@ -3181,17 +3186,17 @@ function Ace2Inner(editorInfo, cssManagers) {
editorInfo.ace_getInInternationalComposition = () => inInternationalComposition; editorInfo.ace_getInInternationalComposition = () => inInternationalComposition;
const bindTheEventHandlers = () => { const bindTheEventHandlers = () => {
$(document).on('keydown', handleKeyEvent); $(targetDoc).on('keydown', handleKeyEvent);
$(document).on('keypress', handleKeyEvent); $(targetDoc).on('keypress', handleKeyEvent);
$(document).on('keyup', handleKeyEvent); $(targetDoc).on('keyup', handleKeyEvent);
$(document).on('click', handleClick); $(targetDoc).on('click', handleClick);
// dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer // dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer
$(outerDoc).on('click', hideEditBarDropdowns); $(outerDoc).on('click', hideEditBarDropdowns);
// If non-nullish, pasting on a link should be suppressed. // If non-nullish, pasting on a link should be suppressed.
let suppressPasteOnLink = null; let suppressPasteOnLink = null;
$(document.body).on('auxclick', (e) => { $(targetBody).on('auxclick', (e) => {
if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) { 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 // 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 // in X11 (Linux) this will instead paste the contents of the primary selection at the mouse
@ -3213,7 +3218,7 @@ function Ace2Inner(editorInfo, cssManagers) {
} }
}); });
$(document.body).on('paste', (e) => { $(targetBody).on('paste', (e) => {
if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) { if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) {
scheduler.clearTimeout(suppressPasteOnLink); scheduler.clearTimeout(suppressPasteOnLink);
suppressPasteOnLink = null; suppressPasteOnLink = null;
@ -3233,7 +3238,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// We reference document here, this is because if we don't this will expose a bug // 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 // in Google Chrome. This bug will cause the last character on the last line to
// not fire an event when dropped into.. // not fire an event when dropped into..
$(document).on('drop', (e) => { $(targetBody).on('drop', (e) => {
if (e.target.a || e.target.localName === 'a') { if (e.target.a || e.target.localName === 'a') {
e.preventDefault(); e.preventDefault();
} }
@ -3251,7 +3256,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const lineAfterSelection = lastLineSelected.nextSibling; const lineAfterSelection = lastLineSelected.nextSibling;
const neighbor = lineBeforeSelection || lineAfterSelection; const neighbor = lineBeforeSelection || lineAfterSelection;
neighbor.appendChild(document.createElement('style')); neighbor.appendChild(targetDoc.createElement('style'));
} }
// Call drop hook // Call drop hook
@ -3263,10 +3268,10 @@ function Ace2Inner(editorInfo, cssManagers) {
}); });
}); });
$(document.documentElement).on('compositionstart', () => { $(targetDoc.documentElement).on('compositionstart', () => {
if (inInternationalComposition) return; if (inInternationalComposition) return;
inInternationalComposition = new Promise((resolve) => { inInternationalComposition = new Promise((resolve) => {
$(document.documentElement).one('compositionend', () => { $(targetDoc.documentElement).one('compositionend', () => {
inInternationalComposition = null; inInternationalComposition = null;
resolve(); resolve();
}); });
@ -3275,8 +3280,8 @@ function Ace2Inner(editorInfo, cssManagers) {
}; };
const topLevel = (n) => { const topLevel = (n) => {
if ((!n) || n === document.body) return null; if ((!n) || n === targetBody) return null;
while (n.parentNode !== document.body) { while (n.parentNode !== targetBody) {
n = n.parentNode; n = n.parentNode;
} }
return n; return n;
@ -3436,10 +3441,10 @@ function Ace2Inner(editorInfo, cssManagers) {
// but as it's non-text type the line-height/margins might not be present and it // 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.. // 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. // 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']); const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']);
for (const docLine of document.body.children) { for (const docLine of targetBody.children) {
let h; let h;
const nextDocLine = docLine.nextElementSibling; const nextDocLine = docLine.nextElementSibling;
if (nextDocLine) { if (nextDocLine) {
@ -3450,7 +3455,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// included on the first line. The default stylesheet doesn't add // included on the first line. The default stylesheet doesn't add
// extra margins/padding, but plugins might. // extra margins/padding, but plugins might.
h = nextDocLine.offsetTop - parseInt( h = nextDocLine.offsetTop - parseInt(
window.getComputedStyle(document.body) window.getComputedStyle(targetBody)
.getPropertyValue('padding-top').split('px')[0]); .getPropertyValue('padding-top').split('px')[0]);
} else { } else {
h = nextDocLine.offsetTop - docLine.offsetTop; h = nextDocLine.offsetTop - docLine.offsetTop;
@ -3496,15 +3501,15 @@ function Ace2Inner(editorInfo, cssManagers) {
this.init = async () => { this.init = async () => {
await $.ready; await $.ready;
inCallStack('setup', () => { inCallStack('setup', () => {
if (browser.firefox) $(document.body).addClass('mozilla'); if (browser.firefox) $(targetBody).addClass('mozilla');
if (browser.safari) $(document.body).addClass('safari'); if (browser.safari) $(targetBody).addClass('safari');
document.body.classList.toggle('authorColors', true); targetBody.classList.toggle('authorColors', true);
document.body.classList.toggle('doesWrap', doesWrap); targetBody.classList.toggle('doesWrap', doesWrap);
enforceEditability(); enforceEditability();
// set up dom and rep // 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(''); const oneEntry = createDomLineEntry('');
doRepLineSplice(0, rep.lines.length(), [oneEntry]); doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo]); insertDomLines(null, [oneEntry.domInfo]);

View file

@ -32,6 +32,9 @@ const colorutils = require('./colorutils').colorutils;
const _ = require('./underscore'); const _ = require('./underscore');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
// These parameters were global, now they are injected. A reference to the // These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate. // Timeslider controller would probably be more appropriate.
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => { const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {

View file

@ -26,6 +26,7 @@
const _ = require('./underscore'); const _ = require('./underscore');
const padmodals = require('./pad_modals').padmodals; const padmodals = require('./pad_modals').padmodals;
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;
import html10n from './vendors/html10n';
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let BroadcastSlider; let BroadcastSlider;

View file

@ -3,8 +3,11 @@
// One rep.line(div) can be broken in more than one line in the browser. // One rep.line(div) can be broken in more than one line in the browser.
// This function is useful to get the caret position of the line as // This function is useful to get the caret position of the line as
// is represented by the browser // is represented by the browser
exports.getPosition = () => { import {Position, RepModel, RepNode} from "./types/RepModel";
export const getPosition = () => {
const range = getSelectionRange(); const range = getSelectionRange();
// @ts-ignore
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null; if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
// When there's a <br> or any element that has no height, we can't get the dimension of the // When there's a <br> or any element that has no height, we can't get the dimension of the
// element where the caret is. As we can't get the element height, we create a text node to get // element where the caret is. As we can't get the element height, we create a text node to get
@ -18,7 +21,7 @@ exports.getPosition = () => {
return line; return line;
}; };
const createSelectionRange = (range) => { const createSelectionRange = (range: Range) => {
const clonedRange = range.cloneRange(); const clonedRange = range.cloneRange();
// we set the selection start and end to avoid error when user selects a text bigger than // we set the selection start and end to avoid error when user selects a text bigger than
@ -30,14 +33,14 @@ const createSelectionRange = (range) => {
return clonedRange; return clonedRange;
}; };
const getPositionOfRepLineAtOffset = (node, offset) => { const getPositionOfRepLineAtOffset = (node: any, offset: number) => {
// it is not a text node, so we cannot make a selection // it is not a text node, so we cannot make a selection
if (node.tagName === 'BR' || node.tagName === 'EMPTY') { if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
return getPositionOfElementOrSelection(node); return getPositionOfElementOrSelection(node);
} }
while (node.length === 0 && node.nextSibling) { while (node.length === 0 && node.nextSibling) {
node = node.nextSibling; node = node.nextSibling as any;
} }
const newRange = new Range(); const newRange = new Range();
@ -48,14 +51,13 @@ const getPositionOfRepLineAtOffset = (node, offset) => {
return linePosition; return linePosition;
}; };
const getPositionOfElementOrSelection = (element) => { const getPositionOfElementOrSelection = (element: Range):Position => {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
const linePosition = { return {
bottom: rect.bottom, bottom: rect.bottom,
height: rect.height, height: rect.height,
top: rect.top, top: rect.top,
}; } satisfies Position;
return linePosition;
}; };
// here we have two possibilities: // here we have two possibilities:
@ -64,7 +66,7 @@ const getPositionOfElementOrSelection = (element) => {
// where is the top of the previous line // where is the top of the previous line
// [2] the line before is part of another rep line. It's possible this line has different margins // [2] the line before is part of another rep line. It's possible this line has different margins
// height. So we have to get the exactly position of the line // height. So we have to get the exactly position of the line
exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => { export const getPositionTopOfPreviousBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1] let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep); const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
@ -80,7 +82,7 @@ exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
return previousLineTop; return previousLineTop;
}; };
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => { const caretLineIsFirstBrowserLine = (caretLineTop: number, rep: RepModel) => {
const caretRepLine = rep.selStart[0]; const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode; const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const firstRootNode = getFirstRootChildNode(lineNode); const firstRootNode = getFirstRootChildNode(lineNode);
@ -91,7 +93,7 @@ const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
}; };
// find the first root node, usually it is a text node // find the first root node, usually it is a text node
const getFirstRootChildNode = (node) => { const getFirstRootChildNode = (node: RepNode) => {
if (!node.firstChild) { if (!node.firstChild) {
return node; return node;
} else { } else {
@ -99,7 +101,7 @@ const getFirstRootChildNode = (node) => {
} }
}; };
const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => { const getDimensionOfLastBrowserLineOfRepLine = (line: number, rep: RepModel) => {
const lineNode = rep.lines.atIndex(line).lineNode; const lineNode = rep.lines.atIndex(line).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode); const lastRootChildNode = getLastRootChildNode(lineNode);
@ -109,7 +111,7 @@ const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
return lastRootChildNodePosition; return lastRootChildNodePosition;
}; };
const getLastRootChildNode = (node) => { const getLastRootChildNode = (node: RepNode) => {
if (!node.lastChild) { if (!node.lastChild) {
return { return {
node, node,
@ -125,7 +127,7 @@ const getLastRootChildNode = (node) => {
// So, we can use the caret line to calculate the bottom of the line. // So, we can use the caret line to calculate the bottom of the line.
// [2] the next line is part of another rep line. // [2] the next line is part of another rep line.
// It's possible this line has different dimensions, so we have to get the exactly dimension of it // It's possible this line has different dimensions, so we have to get the exactly dimension of it
exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => { export const getBottomOfNextBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1] let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
const isCaretLineLastBrowserLine = const isCaretLineLastBrowserLine =
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep); caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
@ -142,7 +144,7 @@ exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
return nextLineBottom; return nextLineBottom;
}; };
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => { const caretLineIsLastBrowserLineOfRepLine = (caretLineTop: number, rep: RepModel) => {
const caretRepLine = rep.selStart[0]; const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode; const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode); const lastRootChildNode = getLastRootChildNode(lineNode);
@ -153,7 +155,7 @@ const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
return lastRootChildNodePosition.top === caretLineTop; return lastRootChildNodePosition.top === caretLineTop;
}; };
const getPreviousVisibleLine = (line, rep) => { export const getPreviousVisibleLine = (line: number, rep: RepModel): number => {
const firstLineOfPad = 0; const firstLineOfPad = 0;
if (line <= firstLineOfPad) { if (line <= firstLineOfPad) {
return firstLineOfPad; return firstLineOfPad;
@ -165,9 +167,8 @@ const getPreviousVisibleLine = (line, rep) => {
}; };
exports.getPreviousVisibleLine = getPreviousVisibleLine;
const getNextVisibleLine = (line, rep) => { export const getNextVisibleLine = (line: number, rep: RepModel): number => {
const lastLineOfThePad = rep.lines.length() - 1; const lastLineOfThePad = rep.lines.length() - 1;
if (line >= lastLineOfThePad) { if (line >= lastLineOfThePad) {
return lastLineOfThePad; return lastLineOfThePad;
@ -177,11 +178,10 @@ const getNextVisibleLine = (line, rep) => {
return getNextVisibleLine(line + 1, rep); return getNextVisibleLine(line + 1, rep);
} }
}; };
exports.getNextVisibleLine = getNextVisibleLine;
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0; const isLineVisible = (line: number, rep: RepModel) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => { const getDimensionOfFirstBrowserLineOfRepLine = (line: number, rep: RepModel) => {
const lineNode = rep.lines.atIndex(line).lineNode; const lineNode = rep.lines.atIndex(line).lineNode;
const firstRootChildNode = getFirstRootChildNode(lineNode); const firstRootChildNode = getFirstRootChildNode(lineNode);

View file

@ -21,10 +21,12 @@ const padcookie = require('./pad_cookie').padcookie;
const Tinycon = require('tinycon/tinycon'); const Tinycon = require('tinycon/tinycon');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const padeditor = require('./pad_editor').padeditor; const padeditor = require('./pad_editor').padeditor;
import html10n from './vendors/html10n';
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463 // Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
exports.chat = (() => { exports.chat = (() => {
let isStuck = false; let isStuck = false;
let userAndChat = false; let userAndChat = false;

View file

@ -1,16 +0,0 @@
'use strict';
((document) => {
// Set language for l10n
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) language = language[1];
html10n.bind('indexed', () => {
html10n.localize([language, navigator.language, navigator.userLanguage, 'en']);
});
html10n.bind('localized', () => {
document.documentElement.lang = html10n.getLanguage();
document.documentElement.dir = html10n.getDirection();
});
})(document);

18
src/static/js/l10n.ts Normal file
View file

@ -0,0 +1,18 @@
import html10n from '../js/vendors/html10n';
// Set language for l10n
let regexpLang: string | undefined;
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) regexpLang = language[1];
html10n.mt.bind('indexed', () => {
console.log('Navigator language', navigator.language)
console.log('Localizing things', [regexpLang, navigator.language, 'en'])
html10n.localize([regexpLang, navigator.language, 'en']);
});
html10n.mt.bind('localized', () => {
document.documentElement.lang = html10n.getLanguage()!;
document.documentElement.dir = html10n.getDirection()!;
});

View file

@ -24,12 +24,15 @@
let socket; let socket;
// These jQuery things should create local references, but for now `require()` // These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins. // assigns to the global `$` and augments it with plugins.
require('./vendors/jquery'); require('./vendors/jquery');
require('./vendors/farbtastic'); require('./vendors/farbtastic');
require('./vendors/gritter'); require('./vendors/gritter');
import html10n from './vendors/html10n'
const Cookies = require('./pad_utils').Cookies; const Cookies = require('./pad_utils').Cookies;
const chat = require('./chat').chat; const chat = require('./chat').chat;
const getCollabClient = require('./collab_client').getCollabClient; const getCollabClient = require('./collab_client').getCollabClient;
@ -136,7 +139,8 @@ const getParameters = [
name: 'lang', name: 'lang',
checkVal: null, checkVal: null,
callback: (val) => { callback: (val) => {
window.html10n.localize([val, 'en']); console.log('Val is', val)
html10n.localize([val, 'en']);
Cookies.set('language', val); Cookies.set('language', val);
}, },
}, },
@ -281,6 +285,7 @@ const handshake = async () => {
} }
}); });
socket.on('error', (error) => { socket.on('error', (error) => {
// pad.collabClient might be null if the error occurred before the hanshake completed. // pad.collabClient might be null if the error occurred before the hanshake completed.
if (pad.collabClient != null) { if (pad.collabClient != null) {
@ -313,6 +318,15 @@ const handshake = async () => {
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {}); () => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
setInterval(ping, window.clientVars.sessionRefreshInterval); setInterval(ping, window.clientVars.sessionRefreshInterval);
} }
if(window.clientVars.mode === "development") {
console.warn('Enabling development mode with live update')
socket.on('liveupdate', ()=>{
console.log('Live reload update received')
location.reload()
})
}
} else if (obj.disconnect) { } else if (obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect); padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect(); socket.disconnect();
@ -713,7 +727,7 @@ const pad = {
$.ajax( $.ajax(
{ {
type: 'post', type: 'post',
url: 'ep/pad/connection-diagnostic-info', url: '../ep/pad/connection-diagnostic-info',
data: { data: {
diagnosticInfo: JSON.stringify(pad.diagnosticInfo), diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
}, },

View file

@ -1,4 +1,5 @@
'use strict'; 'use strict';
import html10n from './vendors/html10n';
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => { exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) { if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {

View file

@ -24,9 +24,10 @@
const Cookies = require('./pad_utils').Cookies; const Cookies = require('./pad_utils').Cookies;
const padcookie = require('./pad_cookie').padcookie; const padcookie = require('./pad_cookie').padcookie;
const padutils = require('./pad_utils').padutils; const padutils = require('./pad_utils').padutils;
const Ace2Editor = require('./ace').Ace2Editor;
import html10n from '../js/vendors/html10n'
const padeditor = (() => { const padeditor = (() => {
let Ace2Editor = undefined;
let pad = undefined; let pad = undefined;
let settings = undefined; let settings = undefined;
@ -35,7 +36,6 @@ const padeditor = (() => {
// this is accessed directly from other files // this is accessed directly from other files
viewZoom: 100, viewZoom: 100,
init: async (initialViewOptions, _pad) => { init: async (initialViewOptions, _pad) => {
Ace2Editor = require('./ace').Ace2Editor;
pad = _pad; pad = _pad;
settings = pad.settings; settings = pad.settings;
self.ace = new Ace2Editor(); self.ace = new Ace2Editor();
@ -99,7 +99,7 @@ const padeditor = (() => {
$('#languagemenu').val(html10n.getLanguage()); $('#languagemenu').val(html10n.getLanguage());
$('#languagemenu').on('change', () => { $('#languagemenu').on('change', () => {
Cookies.set('language', $('#languagemenu').val()); Cookies.set('language', $('#languagemenu').val());
window.html10n.localize([$('#languagemenu').val(), 'en']); html10n.localize([$('#languagemenu').val(), 'en']);
if ($('select').niceSelect) { if ($('select').niceSelect) {
$('select').niceSelect('update'); $('select').niceSelect('update');
} }

View file

@ -22,6 +22,9 @@
* limitations under the License. * limitations under the License.
*/ */
import html10n from './vendors/html10n';
const padimpexp = (() => { const padimpexp = (() => {
let pad; let pad;

View file

@ -20,7 +20,7 @@ let pad;
exports.saveNow = () => { exports.saveNow = () => {
pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
$.gritter.add({ window.$.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
title: html10n.get('pad.savedrevs.marked'), title: html10n.get('pad.savedrevs.marked'),
// (string | mandatory) the text inside the notification // (string | mandatory) the text inside the notification

View file

@ -18,7 +18,7 @@
const padutils = require('./pad_utils').padutils; const padutils = require('./pad_utils').padutils;
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
let myUserInfo = {}; let myUserInfo = {};
let colorPickerOpen = false; let colorPickerOpen = false;

View file

@ -356,7 +356,6 @@ const padutils = {
let globalExceptionHandler = null; let globalExceptionHandler = null;
padutils.setupGlobalExceptionHandler = () => { padutils.setupGlobalExceptionHandler = () => {
if (globalExceptionHandler == null) { if (globalExceptionHandler == null) {
require('./vendors/gritter');
globalExceptionHandler = (e) => { globalExceptionHandler = (e) => {
let type; let type;
let err; let err;
@ -443,7 +442,7 @@ const inThirdPartyIframe = () => {
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global // This file is included from Node so that it can reuse randomString, but Node doesn't have a global
// window object. // window object.
if (typeof window !== 'undefined') { 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=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 // 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 // because the cookies are third-party (not same-site). Many browsers/users block third-party

View file

@ -7,24 +7,13 @@ exports.baseURL = '';
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb(); exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
exports.update = (cb) => { exports.update = async (modules) => {
// It appears that this response (see #620) may interrupt the current thread const data = await jQuery.getJSON(
// of execution on Firefox. This schedules the response in the run-loop, `${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`);
// 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.plugins = data.plugins;
defs.parts = data.parts; defs.parts = data.parts;
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks'); defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks', null, modules);
defs.loaded = true; defs.loaded = true;
callback();
}).fail((err) => {
console.error(`Failed to load plugin-definitions: ${err}`);
callback();
});
}; };
const adoptPluginsFromAncestorsOf = (frame) => { const adoptPluginsFromAncestorsOf = (frame) => {

View file

@ -9,7 +9,7 @@ const disabledHookReasons = {
}, },
}; };
const loadFn = (path, hookName) => { const loadFn = (path, hookName, modules) => {
let functionName; let functionName;
const parts = path.split(':'); const parts = path.split(':');
@ -24,7 +24,13 @@ const loadFn = (path, hookName) => {
functionName = parts[1]; functionName = parts[1];
} }
let fn = require(path); let fn
if (modules === undefined || !("get" in modules)) {
fn = require(/* webpackIgnore: true */ path);
} else {
fn = modules.get(path);
}
functionName = functionName ? functionName : hookName; functionName = functionName ? functionName : hookName;
for (const name of functionName.split('.')) { for (const name of functionName.split('.')) {
@ -33,7 +39,7 @@ const loadFn = (path, hookName) => {
return fn; return fn;
}; };
const extractHooks = (parts, hookSetName, normalizer) => { const extractHooks = (parts, hookSetName, normalizer, modules) => {
const hooks = {}; const hooks = {};
for (const part of parts) { for (const part of parts) {
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) { for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
@ -53,7 +59,7 @@ const extractHooks = (parts, hookSetName, normalizer) => {
} }
let hookFn; let hookFn;
try { try {
hookFn = loadFn(hookFnName, hookName); hookFn = loadFn(hookFnName, hookName, modules);
if (!hookFn) throw new Error('Not a function'); if (!hookFn) throw new Error('Not a function');
} catch (err) { } catch (err) {
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` + console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +

View file

@ -1,351 +0,0 @@
'use strict';
/*
This file handles scroll on edition or when user presses arrow keys.
In this file we have two representations of line (browser and rep line).
Rep Line = a line in the way is represented by Etherpad(rep) (each <div> is a line)
Browser Line = each vertical line. A <div> can be break into more than one
browser line.
*/
const caretPosition = require('./caretPosition');
function Scroll(outerWin) {
// scroll settings
this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport;
// DOM reference
this.outerWin = outerWin;
this.doc = this.outerWin.document;
this.rootDocument = parent.parent.document;
}
Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary =
function (rep, isScrollableEvent, innerHeight) {
// are we placing the caret on the line at the bottom of viewport?
// And if so, do we need to scroll the editor, as defined on the settings.json?
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);

338
src/static/js/scroll.ts Normal file
View file

@ -0,0 +1,338 @@
import {getBottomOfNextBrowserLine, getNextVisibleLine, getPosition, getPositionTopOfPreviousBrowserLine, getPreviousVisibleLine} from './caretPosition';
import {Position, RepModel, RepNode, WindowElementWithScrolling} from "./types/RepModel";
class Scroll {
private readonly outerWin: HTMLIFrameElement;
private readonly doc: Document;
private rootDocument: Document;
private scrollSettings: any;
constructor(outerWin: HTMLIFrameElement) {
// @ts-ignore
this.scrollSettings = window.clientVars.scrollWhenFocusLineIsOutOfViewport;
// DOM reference
this.outerWin = outerWin;
this.doc = this.outerWin.contentDocument!;
this.rootDocument = parent.parent.document;
}
scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep: RepModel, isScrollableEvent: boolean, innerHeight: number) {
// 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: boolean, rep: RepModel, innerHeight: number) {
// 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: RepModel) {
// 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);
return nextLineBottom > viewportBottom;
}
return false;
};
_isLinePartiallyVisibleOnViewport(lineNumber: number, rep: RepModel){
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 as WindowElementWithScrolling;
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: number) {
this.outerWin.scrollTo(x, this.getScrollY());
};
setScrollY(y: number) {
this.outerWin.scrollTo(this.getScrollX(), y);
};
setScrollXY(x: number, y: number) {
this.outerWin.scrollTo(x, y);
};
_isCaretAtTheTopOfViewport(rep: RepModel) {
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: number, aboveOfViewport?: boolean) {
let pixels = 0;
const scrollPercentageRelativeToViewport = this._getPercentageToScroll(aboveOfViewport);
if (scrollPercentageRelativeToViewport > 0 && scrollPercentageRelativeToViewport <= 1) {
pixels = parseInt(String(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|undefined) {
let percentageToScroll = this.scrollSettings.percentage.editionBelowViewport;
if (aboveOfViewport) {
percentageToScroll = this.scrollSettings.percentage.editionAboveViewport;
}
return percentageToScroll;
};
_getPixelsToScrollWhenUserPressesArrowUp(innerHeight: number) {
let pixels = 0;
const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
if (percentageToScrollUp > 0 && percentageToScrollUp <= 1) {
pixels = parseInt(String(innerHeight * percentageToScrollUp));
}
return pixels;
};
_scrollYPage(pixelsToScroll: number) {
const durationOfAnimationToShowFocusline = this.scrollSettings.duration;
if (durationOfAnimationToShowFocusline) {
this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
} else {
this._scrollYPageWithoutAnimation(pixelsToScroll);
}
};
_scrollYPageWithoutAnimation(pixelsToScroll: number) {
this.outerWin.scrollBy(0, pixelsToScroll);
};
_scrollYPageWithAnimation(pixelsToScroll: number, durationOfAnimationToShowFocusline: number) {
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:any, pixelsToScroll: number, durationOfAnimationToShowFocusline: number) {
// clear the queue of animation
$elem.stop('scrollanimation');
$elem.animate({
scrollTop: `+=${pixelsToScroll}`,
}, {
duration: durationOfAnimationToShowFocusline,
queue: 'scrollanimation',
}).dequeue('scrollanimation');
};
scrollNodeVerticallyIntoView(rep: RepModel, innerHeight: number) {
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: Position, rep: RepModel) {
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: RepNode, destObj?: Position) {
const dom = entry.lineNode;
const top = dom.offsetTop;
const height = dom.offsetHeight;
const obj = (destObj || {}) as Position;
obj.top = top;
obj.bottom = (top + height);
return obj;
};
_arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp: boolean, rep: RepModel) {
const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
};
getVisibleLineRange(rep: RepModel) {
const viewport = this._getViewPortTopBottom();
// console.log("viewport top/bottom: %o", viewport);
const obj = {} as Position;
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: RepModel) {
const lineRange = this.getVisibleLineRange(rep);
return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
};
}
export default Scroll

View file

@ -1,4 +1,4 @@
'use strict'; import io from 'socket.io-client';
/** /**
* Creates a socket.io connection. * Creates a socket.io connection.

View file

@ -31,7 +31,7 @@ const randomString = require('./pad_utils').randomString;
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils; const padutils = require('./pad_utils').padutils;
const socketio = require('./socketio'); const socketio = require('./socketio');
import html10n from '../js/vendors/html10n'
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider; let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
const init = () => { const init = () => {
@ -117,6 +117,14 @@ const handleClientVars = (message) => {
setInterval(ping, window.clientVars.sessionRefreshInterval); setInterval(ping, window.clientVars.sessionRefreshInterval);
} }
if(window.clientVars.mode === "development") {
console.warn('Enabling development mode with live update')
socket.on('liveupdate', ()=>{
console.log('Doing live reload')
location.reload()
})
}
// load all script that doesn't work without the clientVars // load all script that doesn't work without the clientVars
BroadcastSlider = require('./broadcast_slider') BroadcastSlider = require('./broadcast_slider')
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); .loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);

View file

@ -0,0 +1,31 @@
export type RepModel = {
lines: {
atIndex: (num: number)=>RepNode,
offsetOfIndex: (range: number)=>number,
search: (filter: (e: RepNode)=>boolean)=>number,
length: ()=>number
}
selStart: number[],
selEnd: number[],
selFocusAtStart: boolean
}
export type Position = {
bottom: number,
height: number,
top: number
}
export type RepNode = {
firstChild: RepNode,
lineNode: RepNode
length: number,
lastChild: RepNode,
offsetHeight: number,
offsetTop: number
}
export type WindowElementWithScrolling = HTMLIFrameElement & {
pageYOffset: number|string,
pageXOffset: number
}

View file

@ -7,6 +7,7 @@
// Licensed under the terms of the GNU General Public License v2.0: // Licensed under the terms of the GNU General Public License v2.0:
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt // https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06 // edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
(function ($) { (function ($) {
var __debug = false; var __debug = false;
@ -172,7 +173,7 @@ $._farbtastic = function (container, options) {
angle2 = d2 * Math.PI * 2, angle2 = d2 * Math.PI * 2,
// Endpoints // Endpoints
x1 = Math.sin(angle1), y1 = -Math.cos(angle1); 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. // Midpoint chosen so that the endpoints are tangent to the circle.
am = (angle1 + angle2) / 2, am = (angle1 + angle2) / 2,
tan = 1 / Math.cos((angle2 - angle1) / 2), tan = 1 / Math.cos((angle2 - angle1) / 2),
@ -329,8 +330,8 @@ $._farbtastic = function (container, options) {
// Update the overlay canvas. // Update the overlay canvas.
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz); fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
for (i in circles) { for (let i in circles) {
var c = circles[i]; const c = circles[i];
fb.ctxOverlay.lineWidth = c.lw; fb.ctxOverlay.lineWidth = c.lw;
fb.ctxOverlay.strokeStyle = c.c; fb.ctxOverlay.strokeStyle = c.c;
fb.ctxOverlay.beginPath(); fb.ctxOverlay.beginPath();

View file

@ -42,7 +42,7 @@
return Gritter.add(params || {}); return Gritter.add(params || {});
} catch(e) { } catch(e) {
var err = 'Gritter Error: ' + e; const err = 'Gritter Error: ' + e;
(typeof(console) != 'undefined' && console.error) ? (typeof(console) != 'undefined' && console.error) ?
console.error(err, params) : console.error(err, params) :
alert(err); alert(err);
@ -289,7 +289,7 @@
*/ */
_runSetup: function(){ _runSetup: function(){
for(opt in $.gritter.options){ for(let opt in $.gritter.options){
this[opt] = $.gritter.options[opt]; this[opt] = $.gritter.options[opt];
} }
this._is_setup = 1; this._is_setup = 1;

File diff suppressed because it is too large Load diff

993
src/static/js/vendors/html10n.ts vendored Normal file
View file

@ -0,0 +1,993 @@
import {Func} from "mocha";
type PluralFunc = (n: number) => string
export class Html10n {
public language?: string
private rtl: string[]
private _pluralRules?: PluralFunc
public mt: MicroEvent
private loader: Loader | undefined
public translations: Map<string, any>
private macros: Map<string, Function>
constructor() {
this.language = undefined
this.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"]
this.mt = new MicroEvent()
this.translations = new Map()
this.macros = new Map()
this.macros.set('plural', (_key: string, param:string, opts: any)=>{
let str
, n = parseFloat(param);
if (isNaN(n))
return;
// initialize _pluralRules
if (this._pluralRules === undefined) {
this._pluralRules = this.getPluralRules(this.language!);
}
let index = this._pluralRules!(n);
// try to find a [zero|one|two] key if it's defined
if (n === 0 && ('zero') in opts) {
str = opts['zero'];
} else if (n == 1 && ('one') in opts) {
str = opts['one'];
} else if (n == 2 && ('two') in opts) {
str = opts['two'];
} else if (index in opts) {
str = opts[index];
}
return str;
})
document.addEventListener('DOMContentLoaded', ()=> {
this.index()
}, false)
}
bind(event: string, fct: Func) {
this.mt.bind(event, fct)
}
/**
* Get rules for plural forms (shared with JetPack), see:
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
* https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
*
* @param {string} lang
* locale (language) used.
*
* @return {PluralFunc}
* returns a function that gives the plural form name for a given integer:
* var fun = getPluralRules('en');
* fun(1) -> 'one'
* fun(0) -> 'other'
* fun(1000) -> 'other'.
*/
getPluralRules(lang: string): PluralFunc {
const locales2rules = new Map([
['af', 3],
['ak', 4],
['am', 4],
['ar', 1],
['asa', 3],
['az', 0],
['be', 11],
['bem', 3],
['bez', 3],
['bg', 3],
['bh', 4],
['bm', 0],
['bn', 3],
['bo', 0],
['br', 20],
['brx', 3],
['bs', 11],
['ca', 3],
['cgg', 3],
['chr', 3],
['cs', 12],
['cy', 17],
['da', 3],
['de', 3],
['dv', 3],
['dz', 0],
['ee', 3],
['el', 3],
['en', 3],
['eo', 3],
['es', 3],
['et', 3],
['eu', 3],
['fa', 0],
['ff', 5],
['fi', 3],
['fil', 4],
['fo', 3],
['fr', 5],
['fur', 3],
['fy', 3],
['ga', 8],
['gd', 24],
['gl', 3],
['gsw', 3],
['gu', 3],
['guw', 4],
['gv', 23],
['ha', 3],
['haw', 3],
['he', 2],
['hi', 4],
['hr', 11],
['hu', 0],
['id', 0],
['ig', 0],
['ii', 0],
['is', 3],
['it', 3],
['iu', 7],
['ja', 0],
['jmc', 3],
['jv', 0],
['ka', 0],
['kab', 5],
['kaj', 3],
['kcg', 3],
['kde', 0],
['kea', 0],
['kk', 3],
['kl', 3],
['km', 0],
['kn', 0],
['ko', 0],
['ksb', 3],
['ksh', 21],
['ku', 3],
['kw', 7],
['lag', 18],
['lb', 3],
['lg', 3],
['ln', 4],
['lo', 0],
['lt', 10],
['lv', 6],
['mas', 3],
['mg', 4],
['mk', 16],
['ml', 3],
['mn', 3],
['mo', 9],
['mr', 3],
['ms', 0],
['mt', 15],
['my', 0],
['nah', 3],
['naq', 7],
['nb', 3],
['nd', 3],
['ne', 3],
['nl', 3],
['nn', 3],
['no', 3],
['nr', 3],
['nso', 4],
['ny', 3],
['nyn', 3],
['om', 3],
['or', 3],
['pa', 3],
['pap', 3],
['pl', 13],
['ps', 3],
['pt', 3],
['rm', 3],
['ro', 9],
['rof', 3],
['ru', 11],
['rwk', 3],
['sah', 0],
['saq', 3],
['se', 7],
['seh', 3],
['ses', 0],
['sg', 0],
['sh', 11],
['shi', 19],
['sk', 12],
['sl', 14],
['sma', 7],
['smi', 7],
['smj', 7],
['smn', 7],
['sms', 7],
['sn', 3],
['so', 3],
['sq', 3],
['sr', 11],
['ss', 3],
['ssy', 3],
['st', 3],
['sv', 3],
['sw', 3],
['syr', 3],
['ta', 3],
['te', 3],
['teo', 3],
['th', 0],
['ti', 4],
['tig', 3],
['tk', 3],
['tl', 4],
['tn', 3],
['to', 0],
['tr', 0],
['ts', 3],
['tzm', 22],
['uk', 11],
['ur', 3],
['ve', 3],
['vi', 0],
['vun', 3],
['wa', 4],
['wae', 3],
['wo', 0],
['xh', 3],
['xog', 3],
['yo', 0],
['zh', 0],
['zu', 3]
])
function isIn(n: number, list: number[]) {
return list.indexOf(n) !== -1;
}
function isBetween(n: number, start: number, end: number) {
return start <= n && n <= end;
}
type PluralFunc = (n: number) => string
const pluralRules: {
[key: string]: PluralFunc
} = {
'0': function() {
return 'other';
},
'1': function(n: number) {
if ((isBetween((n % 100), 3, 10)))
return 'few';
if (n === 0)
return 'zero';
if ((isBetween((n % 100), 11, 99)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'2': function(n: number) {
if (n !== 0 && (n % 10) === 0)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'3': function(n: number) {
if (n == 1)
return 'one';
return 'other';
},
'4': function(n: number) {
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'5': function(n: number) {
if ((isBetween(n, 0, 2)) && n != 2)
return 'one';
return 'other';
},
'6': function(n: number) {
if (n === 0)
return 'zero';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'7': function(n: number) {
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'8': function(n: number) {
if ((isBetween(n, 3, 6)))
return 'few';
if ((isBetween(n, 7, 10)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'9': function(n: number) {
if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'10': function(n: number) {
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
return 'few';
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
return 'one';
return 'other';
},
'11': function(n: number) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if ((n % 10) === 0 ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 11, 14)))
return 'many';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'12': function(n: number) {
if ((isBetween(n, 2, 4)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'13': function(n: number) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if (n != 1 && (isBetween((n % 10), 0, 1)) ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 12, 14)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'14': function(n: number) {
if ((isBetween((n % 100), 3, 4)))
return 'few';
if ((n % 100) == 2)
return 'two';
if ((n % 100) == 1)
return 'one';
return 'other';
},
'15': function(n: number) {
if (n === 0 || (isBetween((n % 100), 2, 10)))
return 'few';
if ((isBetween((n % 100), 11, 19)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'16': function(n: number) {
if ((n % 10) == 1 && n != 11)
return 'one';
return 'other';
},
'17': function(n: number) {
if (n == 3)
return 'few';
if (n === 0)
return 'zero';
if (n == 6)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'18': function(n: number) {
if (n === 0)
return 'zero';
if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
return 'one';
return 'other';
},
'19': function(n: number) {
if ((isBetween(n, 2, 10)))
return 'few';
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'20': function(n: number) {
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
isBetween((n % 100), 10, 19) ||
isBetween((n % 100), 70, 79) ||
isBetween((n % 100), 90, 99)
))
return 'few';
if ((n % 1000000) === 0 && n !== 0)
return 'many';
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
return 'two';
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
return 'one';
return 'other';
},
'21': function(n: number) {
if (n === 0)
return 'zero';
if (n == 1)
return 'one';
return 'other';
},
'22': function(n: number) {
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
return 'one';
return 'other';
},
'23': function(n: number) {
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
return 'one';
return 'other';
},
'24': function(n: number) {
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
return 'few';
if (isIn(n, [2, 12]))
return 'two';
if (isIn(n, [1, 11]))
return 'one';
return 'other';
}
};
const index = locales2rules.get(lang.replace(/-.*$/, ''));
// @ts-ignore
if (!(index in pluralRules)) {
console.warn('plural form unknown for [' + lang + ']');
return function() { return 'other'; };
}
// @ts-ignore
return pluralRules[index];
}
getTranslatableChildren(element: HTMLElement) {
return element.querySelectorAll('*[data-l10n-id]')
}
localize(langs: (string|undefined)[]|string) {
console.log('Available langs ', langs)
if ('string' === typeof langs) {
langs = [langs];
}
let i = 0
langs.forEach((lang) => {
if(!lang) return;
langs[i++] = lang;
if(~lang.indexOf('-')) langs[i++] = lang.substring(0, lang.indexOf('-'));
})
this.build(langs, (er: null, translations: Map<string, any>) =>{
this.translations = translations
this.translateElement(translations)
this.mt.trigger('localized')
})
}
/**
* Triggers the translation process
* for an element
* @param translations A hash of all translation strings
* @param element A DOM element, if omitted, the document element will be used
*/
translateElement(translations: Map<string, any>, element?: HTMLElement) {
element = element || document.documentElement
const children = element ? this.getTranslatableChildren(element): document.childNodes
for (let child of children) {
this.translateNode(translations, child as HTMLElement)
}
// translate element itself if necessary
this.translateNode(translations, element)
}
asyncForEach(list: (string|undefined)[], iterator: any, cb: Function) {
let i = 0
, n = list.length
iterator(list[i], i, function each(err?: string) {
if(err) console.error(err)
i++
if (i < n) return iterator(list[i],i, each);
cb()
})
}
/**
* Builds a translation object from a list of langs (loads the necessary translations)
* @param langs Array - a list of langs sorted by priority (default langs should go last)
* @param cb Function - a callback that will be called once all langs have been loaded
*/
build(langs: (string|undefined)[], cb: Function) {
const build = new Map<string, any>()
this.asyncForEach(langs, (lang: string, _i: number, next:LoaderFunc)=> {
if(!lang) return next();
this.loader!.load(lang, next)
}, () =>{
let lang;
langs.reverse()
// loop through the priority array...
for (let i=0, n=langs.length; i < n; i++) {
lang = langs[i]
if(!lang) continue;
if(!(lang in langs)) {// uh, we don't have this lang availbable..
// then check for related langs
if(~lang.indexOf('-') != -1) {
lang = lang.split('-')[0];
}
let l: string|undefined = ''
for(l of langs) {
if(l && lang != l && l.indexOf(lang) === 0) {
lang = l
break;
}
}
// @ts-ignore
if(lang != l) continue;
}
// ... and apply all strings of the current lang in the list
// to our build object
//lang = "de"
if (this.loader!.langs.has(lang)) {
for (let string in this.loader!.langs.get(lang)) {
build.set(string,this.loader!.langs.get(lang)[string])
}
this.language = lang
} else {
const loaderLang = lang.split('-')[0]
for (let string in this.loader!.langs.get(loaderLang)) {
build.set(string,this.loader!.langs.get(loaderLang)[string])
}
this.language = loaderLang
}
// the last applied lang will be exposed as the
// lang the page was translated to
}
cb(null, build)
})
}
/**
* Returns the language that was last applied to the translations hash
* thus overriding most of the formerly applied langs
*/
getLanguage() {
return this.language
}
/**
* Returns the direction of the language returned be html10n#getLanguage
*/
getDirection() {
if(!this.language) return
const langCode = this.language.indexOf('-') == -1? this.language : this.language.substring(0, this.language.indexOf('-'))
return this.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl'
}
/**
* Index all <link>s
*/
index() {
// Find all <link>s
const links = document.getElementsByTagName('link')
, resources = []
for (let i=0, n=links.length; i < n; i++) {
if (links[i].type != 'application/l10n+json')
continue;
resources.push(links[i].href)
}
this.loader = new Loader(resources)
this.mt.trigger('indexed')
}
translateNode(translations: Map<string, any>, node: HTMLElement) {
const str: {
id?: string,
args?: any,
str?: string
} = {}
// get id
str.id = node.getAttribute('data-l10n-id') as string
if (!str.id) return
if(!translations.get(str.id)) return console.warn('Couldn\'t find translation key '+str.id)
// get args
if(window.JSON) {
str.args = JSON.parse(node.getAttribute('data-l10n-args') as string)
}else{
try{
//str.args = eval(node.getAttribute('data-l10n-args') as string)
console.error("Old eval method invoked!!")
}catch(e) {
console.warn('Couldn\'t parse args for '+str.id)
}
}
str.str = this.get(str.id, str.args)
// get attribute name to apply str to
let prop
, index = str.id.lastIndexOf('.')
, attrList = // allowed attributes
{ "title": 1
, "innerHTML": 1
, "alt": 1
, "textContent": 1
, "value": 1
, "placeholder": 1
}
if (index > 0 && str.id.substring(index + 1) in attrList) {
// an attribute has been specified (example: "my_translation_key.placeholder")
prop = str.id.substring(index + 1)
} else { // no attribute: assuming text content by default
prop = document.body.textContent ? 'textContent' : 'innerText'
}
// Apply translation
if (node.children.length === 0 || prop != 'textContent') {
// @ts-ignore
node[prop] = str.str!
node.setAttribute("aria-label", str.str!); // Sets the aria-label
// The idea of the above is that we always have an aria value
// This might be a bit of an abrupt solution but let's see how it goes
} else {
let children = node.childNodes,
found = false
let i = 0, n = children.length;
for (; i < n; i++) {
if (children[i].nodeType === 3 && /\S/.test(children[i].textContent!)) {
if (!found) {
children[i].nodeValue = str.str!
found = true
} else {
children[i].nodeValue = ''
}
}
}
if (!found) {
console.warn('Unexpected error: could not translate element content for key '+str.id, node)
}
}
}
get(id: string, args?:any) {
let translations = this.translations
if(!translations) return console.warn('No translations available (yet)')
if(!translations.get(id)) return console.warn('Could not find string '+id)
// apply macros
let str = translations.get(id)
str = this.substMacros(id, str, args)
// apply args
str = this.substArguments(str, args)
return str
}
substMacros(key: string, str:string, args:any) {
let regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}')
, match
while(match = regex.exec(str)) {
// a macro has been found
// Note: at the moment, only one parameter is supported
let macroName = match[1]
, paramName = match[2]
, optv = match[3]
, opts: {[key:string]:any} = {}
if (!(this.macros.has(macroName))) continue
if(optv) {
optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g)!.forEach(function(arg) {
const parts = arg.split(':')
, name = parts[0];
opts[name] = parts[1].trim()
})
}
let param
if (args && paramName in args) {
param = args[paramName]
} else if (paramName in this.translations) {
param = this.translations.get(paramName)
}
// there's no macro parser: it has to be defined in html10n.macros
let macro = this.macros.get(macroName)!
str = str.substring(0, match.index) + macro(key, param, opts) + str.substring(match.index+match[0].length)
}
return str
}
substArguments(str: string, args:any) {
let reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/
, match
let translations = this.translations;
while (match = reArgs.exec(str)) {
if (!match || match.length < 2)
return str // argument key not found
let arg = match[1]
, sub = ''
if (args && arg in args) {
sub = args[arg]
} else if (translations && arg in translations) {
sub = translations.get(arg)
} else {
console.warn('Could not find argument {{' + arg + '}}')
return str
}
str = str.substring(0, match.index) + sub + str.substring(match.index + match[0].length)
}
return str
}
}
class MicroEvent {
private events: Map<string, Function[]>
constructor() {
this.events = new Map();
}
bind(event: string, fct: Func) {
if (this.events.get(event) === undefined) {
this.events.set(event, []);
}
this.events.get(event)!.push(fct);
}
unbind(event: string, fct: Func) {
if (this.events.get(event) === undefined) {
return;
}
const index = this.events.get(event)!.indexOf(fct);
if (index !== -1) {
this.events.get(event)!.splice(index, 1);
}
}
trigger(event: string, ...args: any[]) {
if (this.events.get(event) === undefined) {
return;
}
for (const fct of this.events.get(event)!) {
fct(...args);
}
}
mixin(destObject: any) {
const props = ['bind', 'unbind', 'trigger'];
if (destObject !== undefined) {
for (const prop of props) {
// @ts-ignore
destObject[prop] = this[prop];
}
}
}
}
type LoaderFunc = () => void
type ErrorFunc = (data?:any)=>void
class Loader {
private resources: any
private cache: Map<string, any>
langs: Map<string, any>
constructor(resources: any) {
this.resources = resources;
this.cache = new Map();
this.langs = new Map();
}
load(lang: string, callback: LoaderFunc) {
if (this.langs.get(lang) !== undefined) {
callback();
return;
}
if (this.resources.length > 0) {
let reqs = 0
for (const resource of this.resources) {
this.fetch(resource, lang, (e)=> {
reqs++;
if (e) console.warn(e)
if (reqs < this.resources.length) return;// Call back once all reqs are completed
callback && callback()
})
}
}
}
fetch(href: string, lang: string, callback: ErrorFunc) {
if (this.cache.get(href)) {
this.parse(lang, href, this.cache.get(href), callback)
return;
}
const xhr = new XMLHttpRequest();
xhr.open('GET', href, /*async: */true)
if (xhr.overrideMimeType) {
xhr.overrideMimeType('application/json; charset=utf-8');
}
xhr.onreadystatechange = ()=> {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) {
const data = JSON.parse(xhr.responseText);
this.cache.set(href, data)
// Pass on the contents for parsing
this.parse(lang, href, data, callback)
} else {
callback(new Error('Failed to load '+href))
}
}
};
xhr.send(null);
}
parse(lang: string, href: string, data: {
[key: string]: string
}, callback: ErrorFunc) {
if ('object' !== typeof data) {
callback(new Error('A file couldn\'t be parsed as json.'))
return
}
function getBcp47LangCode(browserLang: string) {
const bcp47Lang = browserLang.toLowerCase();
// Browser => BCP 47
const langCodeMap = new Map([
['zh-cn', 'zh-hans-cn'],
['zh-hk', 'zh-hant-hk'],
['zh-mo', 'zh-hant-mo'],
['zh-my', 'zh-hans-my'],
['zh-sg', 'zh-hans-sg'],
['zh-tw', 'zh-hant-tw'],
])
return langCodeMap.get(bcp47Lang) ?? bcp47Lang;
}
// Issue #6129: Fix exceptions
// NOTE: translatewiki.net use all lowercase form by default ('en-gb' insted of 'en-GB')
function getJsonLangCode(bcp47Lang: string) {
const jsonLang = bcp47Lang.toLowerCase();
// BCP 47 => JSON
const langCodeMap = new Map([
['sr-ec', 'sr-cyrl'],
['sr-el', 'sr-latn'],
['zh-hk', 'zh-hant-hk'],
])
return langCodeMap.get(jsonLang) ?? jsonLang;
}
let bcp47LangCode = getBcp47LangCode(lang);
let jsonLangCode = getJsonLangCode(bcp47LangCode);
if (!data[jsonLangCode]) {
// lang not found
// This may be due to formatting (expected 'ru' but browser sent 'ru-RU')
// Set err msg before mutating lang (we may need this later)
const msg = 'Couldn\'t find translations for ' + lang +
'(lowercase BCP 47 lang tag ' + bcp47LangCode +
', JSON lang code ' + jsonLangCode + ')';
// Check for '-' (BCP 47 'ROOT-SCRIPT-REGION-VARIANT') and fallback until found data or ROOT
// - 'ROOT-SCRIPT-REGION': 'zh-Hans-CN'
// - 'ROOT-SCRIPT': 'zh-Hans'
// - 'ROOT-REGION': 'en-GB'
// - 'ROOT-VARIANT': 'be-tarask'
while (!data[jsonLangCode] && bcp47LangCode.lastIndexOf('-') > -1) {
// ROOT-SCRIPT-REGION-VARIANT formatting detected
bcp47LangCode = bcp47LangCode.substring(0, bcp47LangCode.lastIndexOf('-')); // set lang to ROOT lang
jsonLangCode = getJsonLangCode(bcp47LangCode);
}
if (!data[jsonLangCode]) {
// ROOT lang not found. (e.g 'zh')
// Loop through langs data. Maybe we have a variant? e.g (zh-hans)
let l; // langs item. Declare outside of loop
for (l in data) {
// Is not ROOT?
// And is variant of ROOT?
// (NOTE: index of ROOT equals 0 would cause unexpected ISO 639-1 vs. 639-3 issues,
// so append dash into query string)
// And is known lang?
if (bcp47LangCode != l && l.indexOf(lang + '-') === 0 && data[l]) {
bcp47LangCode = l; // set lang to ROOT-SCRIPT (e.g 'zh-hans')
jsonLangCode = getJsonLangCode(bcp47LangCode);
break;
}
}
// Did we find a variant? If not, return err.
if (bcp47LangCode != l) {
return callback(new Error(msg));
}
}
}
lang = jsonLangCode
if('string' === typeof data[lang]) {
// Import rule
// absolute path
let importUrl = data[lang];
// relative path
if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) {
importUrl = href+"/../"+data[lang]
}
this.fetch(importUrl, lang, callback)
return
}
if ('object' != typeof data[lang]) {
callback(new Error('Translations should be specified as JSON objects!'))
return
}
this.langs.set(lang,data[lang])
// TODO: Also store accompanying langs
callback()
}
}
export default new Html10n()

View file

@ -110,10 +110,10 @@
$dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px'); $dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px');
} }
$listHeight = $dropdown.find('.list').outerHeight(); let $listHeight = $dropdown.find('.list').outerHeight();
$top = $dropdown.parent().offset().top; let $top = $dropdown.parent().offset().top;
$bottom = $('body').height() - $top; let $bottom = $('body').height() - $top;
$maxListHeight = $bottom - $dropdown.outerHeight() - 20; let $maxListHeight = $bottom - $dropdown.outerHeight() - 20;
if ($maxListHeight < 200) { if ($maxListHeight < 200) {
$dropdown.addClass('reverse'); $dropdown.addClass('reverse');
$maxListHeight = 250; $maxListHeight = 250;

View file

@ -34,7 +34,6 @@
for the JavaScript code in this page.| for the JavaScript code in this page.|
*/ */
</script> </script>
<script src="../static/js/basic_error_handler.js?v=<%=settings.randomVersionString%>"></script>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
@ -53,8 +52,6 @@
<% e.end_block(); %> <% e.end_block(); %>
<link rel="localizations" type="application/l10n+json" href="../locales.json" /> <link rel="localizations" type="application/l10n+json" href="../locales.json" />
<script type="text/javascript" src="../static/js/vendors/html10n.js?v=<%=settings.randomVersionString%>"></script>
<script type="text/javascript" src="../static/js/l10n.js?v=<%=settings.randomVersionString%>"></script>
</head> </head>
<body> <body>
<% e.begin_block("body"); %> <% e.begin_block("body"); %>
@ -442,67 +439,11 @@
<% e.begin_block("scripts"); %> <% e.begin_block("scripts"); %>
<script type="text/javascript" src="../static/js/require-kernel.js?v=<%=settings.randomVersionString%>"></script> <script src="<%=entrypoint%>"></script>
<script type="text/javascript" src="../socket.io/socket.io.js"></script>
<!-- Include base packages manually (this help with debugging) -->
<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
<% e.begin_block("customScripts"); %> <% e.begin_block("customScripts"); %>
<script type="text/javascript" src="../static/skins/<%=encodeURI(settings.skinName)%>/pad.js?v=<%=settings.randomVersionString%>"></script> <script type="text/javascript" src="../static/skins/<%=encodeURI(settings.skinName)%>/pad.js?v=<%=settings.randomVersionString%>"></script>
<% e.end_block(); %> <% e.end_block(); %>
<!-- Bootstrap page -->
<script type="text/javascript">
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt
var 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)%>,
};
(function () {
var pathComponents = location.pathname.split('/');
// Strip 'p' and the padname from the pathname and set as baseURL
var baseURL = pathComponents.slice(0,pathComponents.length-2).join('/') + '/';
require.setRootURI(baseURL + "javascripts/src");
require.setLibraryURI(baseURL + "javascripts/lib");
require.setGlobalKeyPath("require");
$ = jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; // Expose jQuery #HACK
browser = require('ep_etherpad-lite/static/js/vendors/browser');
var plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
plugins.baseURL = baseURL;
plugins.update(function () {
// Mechanism for tests to register hook functions (install fake plugins).
window._postPluginUpdateForTestingDone = false;
if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting();
window._postPluginUpdateForTestingDone = true;
// Call documentReady hook
$(function() {
hooks.aCallAll('documentReady');
});
var pad = require('ep_etherpad-lite/static/js/pad');
pad.baseURL = baseURL;
pad.init();
});
/* TODO: These globals shouldn't exist. */
pad = require('ep_etherpad-lite/static/js/pad').pad;
chat = require('ep_etherpad-lite/static/js/chat').chat;
padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
require('ep_etherpad-lite/static/js/skin_variants');
}());
// @license-end
</script>
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div> <div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
<% e.end_block(); %> <% e.end_block(); %>
</body> </body>

View file

@ -0,0 +1,45 @@
(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.
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');
require('../../src/static/js/basic_error_handler')
window.plugins.baseURL = basePath;
await window.plugins.update(new Map([
<% for (const module of pluginModules) { %>
[<%- JSON.stringify(module) %>, require("../../src/plugin_packages/"+<%- 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');
})();

View file

@ -0,0 +1,41 @@
window.$ = window.jQuery = await import('../../src/static/js/rjquery').jQuery;
await import('../../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.
randomVersionString: "7a7bdbad",
};
(async () => {
// 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.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');
require('../../src/static/js/basic_error_handler')
window.plugins.baseURL = basePath;
await window.plugins.update(new Map([
]));
// 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');
})();

View file

@ -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()
})();

View file

@ -47,8 +47,6 @@
<link rel="localizations" type="application/l10n+json" href="../../locales.json" /> <link rel="localizations" type="application/l10n+json" href="../../locales.json" />
<% e.begin_block("timesliderScripts"); %> <% e.begin_block("timesliderScripts"); %>
<script type="text/javascript" src="../../static/js/vendors/html10n.js?v=<%=settings.randomVersionString%>"></script>
<script type="text/javascript" src="../../static/js/l10n.js?v=<%=settings.randomVersionString%>"></script>
<% e.end_block(); %> <% e.end_block(); %>
</head> </head>
@ -250,58 +248,14 @@
<!-------- JAVASCRIPT ---------> <!-------- JAVASCRIPT --------->
<!-----------------------------> <!----------------------------->
<script type="text/javascript" src="../../static/js/require-kernel.js?v=<%=settings.randomVersionString%>"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script> <script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<!-- Include base packages manually (this help with debugging) --> <!-- Include base packages manually (this help with debugging) -->
<script type="text/javascript" src="../../javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
<script type="text/javascript" src="../../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
<script type="text/javascript" src="../../static/skins/<%=encodeURI(settings.skinName)%>/timeslider.js?v=<%=settings.randomVersionString%>"></script> <script type="text/javascript" src="../../static/skins/<%=encodeURI(settings.skinName)%>/timeslider.js?v=<%=settings.randomVersionString%>"></script>
<!-- Bootstrap --> <!-- Bootstrap -->
<script type="text/javascript" > <script src="<%=entrypoint%>"></script>
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt
var 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 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.setRootURI(baseURL + "javascripts/src");
require.setLibraryURI(baseURL + "javascripts/lib");
require.setGlobalKeyPath("require");
$ = jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; // Expose jQuery #HACK
browser = require('ep_etherpad-lite/static/js/vendors/browser');
const plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
const socket = require('ep_etherpad-lite/static/js/timeslider').socket;
BroadcastSlider = require('ep_etherpad-lite/static/js/timeslider').BroadcastSlider;
plugins.baseURL = baseURL;
plugins.update(function () {
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
const timeslider = require('ep_etherpad-lite/static/js/timeslider')
timeslider.baseURL = baseURL;
timeslider.init();
/* 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;
padeditbar.init()
});
})();
// @license-end
</script>
<% e.end_block(); %> <% e.end_block(); %>
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div> <div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
</html> </html>

View file

@ -24,7 +24,7 @@ s!"points":[^,]*!"points": 1000!
' settings.json.template >settings.json ' settings.json.template >settings.json
log "Assuming src/bin/installDeps.sh has already been run" log "Assuming src/bin/installDeps.sh has already been run"
(cd src && npm run dev & (cd src && pnpm run prod &
ep_pid=$!) ep_pid=$!)
log "Waiting for Etherpad to accept connections (http://localhost:9001)..." log "Waiting for Etherpad to accept connections (http://localhost:9001)..."

View file

@ -6,9 +6,11 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview" "preview": "vite preview",
"build-copy": "tsc && vite build --outDir ../src/static/oidc --emptyOutDir"
}, },
"devDependencies": { "devDependencies": {
"ep_etherpad-lite": "workspace:../src",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"vite": "^5.3.4" "vite": "^5.3.4"
} }

686
ui/pad.html Normal file
View file

@ -0,0 +1,686 @@
<!doctype html>
<html translate="no" class="pad super-light-toolbar super-light-editor light-background">
<head>
<title>Etherpad</title>
<link rel="manifest" href="/manifest.json" />
<script>
/*
|@licstart The following is the entire license notice for the
JavaScript code in this page.|
Copyright 2011 Peter Martischka, Primary Technology.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
|@licend The above is the entire license notice
for the JavaScript code in this page.|
*/
</script>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="shortcut icon" href="../favicon.ico">
<link href="../static/css/pad.css?v=5ba315cd" rel="stylesheet">
<link href="../static/skins/colibris/pad.css?v=5ba315cd" rel="stylesheet">
<style title="dynamicsyntax"></style>
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
</head>
<body>
<!----------------------------->
<!--------- TOOLBAR ----------->
<!----------------------------->
<div id="editbar" class="toolbar">
<div id="toolbar-overlay"></div>
<ul class="menu_left" role="toolbar">
<li data-type="button" data-key="bold"><a class="grouped-left" data-l10n-id="pad.toolbar.bold.title"><button class=" buttonicon buttonicon-bold" data-l10n-id="pad.toolbar.bold.title"></button></a></li>
<li data-type="button" data-key="italic"><a class="grouped-middle" data-l10n-id="pad.toolbar.italic.title"><button class=" buttonicon buttonicon-italic" data-l10n-id="pad.toolbar.italic.title"></button></a></li>
<li data-type="button" data-key="underline"><a class="grouped-middle" data-l10n-id="pad.toolbar.underline.title"><button class=" buttonicon buttonicon-underline" data-l10n-id="pad.toolbar.underline.title"></button></a></li>
<li data-type="button" data-key="strikethrough"><a class="grouped-right" data-l10n-id="pad.toolbar.strikethrough.title"><button class=" buttonicon buttonicon-strikethrough" data-l10n-id="pad.toolbar.strikethrough.title"></button
></a></li><li class="separator"></li><li data-type="button" data-key="insertorderedlist"><a class="grouped-left" data-l10n-id="pad.toolbar.ol.title"><button class=" buttonicon buttonicon-insertorderedlist" data-l10n-id="pad.toolbar.ol.title"></button></a></li>
<li data-type="button" data-key="insertunorderedlist"><a class="grouped-middle" data-l10n-id="pad.toolbar.ul.title"><button class=" buttonicon buttonicon-insertunorderedlist" data-l10n-id="pad.toolbar.ul.title"></button></a></li>
<li data-type="button" data-key="indent"><a class="grouped-middle" data-l10n-id="pad.toolbar.indent.title"><button class=" buttonicon buttonicon-indent" data-l10n-id="pad.toolbar.indent.title"></button></a></li>
<li data-type="button" data-key="outdent"><a class="grouped-right" data-l10n-id="pad.toolbar.unindent.title"><button class=" buttonicon buttonicon-outdent" data-l10n-id="pad.toolbar.unindent.title"></button></a></li><li class="separator"></li><li data-type="button" data-key="undo"><a class="grouped-left" data-l10n-id="pad.toolbar.undo.title"><button class=" buttonicon buttonicon-undo" data-l10n-id="pad.toolbar.undo.title"></button></a></li>
<li data-type="button" data-key="redo"><a class="grouped-right" data-l10n-id="pad.toolbar.redo.title"><button class=" buttonicon buttonicon-redo" data-l10n-id="pad.toolbar.redo.title"></button></a></li><li class="separator"></li
><li data-type="button" data-key="clearauthorship"><a class="" data-l10n-id="pad.toolbar.clearAuthorship.title"><button class=" buttonicon buttonicon-clearauthorship" data-l10n-id="pad.toolbar.clearAuthorship.title"></button></a></li>
</ul>
<ul class="menu_right" role="toolbar">
<li data-type="button" data-key="import_export"><a class="grouped-left" data-l10n-id="pad.toolbar.import_export.title"><button class=" buttonicon buttonicon-import_export" data-l10n-id="pad.toolbar.import_export.title"></button></a></li>
<li data-type="button" data-key="showTimeSlider"><a class="grouped-middle" data-l10n-id="pad.toolbar.timeslider.title"><button class=" buttonicon buttonicon-history" data-l10n-id="pad.toolbar.timeslider.title"></button></a></li>
<li data-type="button" data-key="savedRevision"><a class="grouped-right" data-l10n-id="pad.toolbar.savedRevision.title"><button class=" buttonicon buttonicon-savedRevision" data-l10n-id="pad.toolbar.savedRevision.title"></button
></a></li><li class="separator"></li><li data-type="button" data-key="settings"><a class="grouped-left" data-l10n-id="pad.toolbar.settings.title"><button class=" buttonicon buttonicon-settings" data-l10n-id="pad.toolbar.settings.title"></button></a></li>
<li data-type="button" data-key="embed"><a class="grouped-right" data-l10n-id="pad.toolbar.embed.title"><button class=" buttonicon buttonicon-embed" data-l10n-id="pad.toolbar.embed.title"></button></a></li><li class="separator"></li><li data-type="button" data-key="showusers"><a class="" data-l10n-id="pad.toolbar.showusers.title"><button class=" buttonicon buttonicon-showusers" data-l10n-id="pad.toolbar.showusers.title"></button></a></li>
</ul>
<span class="show-more-icon-btn"></span> <!-- use on small screen to display hidden toolbar buttons -->
</div>
<div id="editorcontainerbox" class="flex-layout">
<!----------------------------->
<!--- PAD EDITOR (in iframe) -->
<!----------------------------->
<div id="editorcontainer" class="editorcontainer"></div>
<div id="editorloadingbox">
<div id="permissionDenied">
<p data-l10n-id="pad.permissionDenied" class="editorloadingbox-message">
You do not have permission to access this pad
</p>
</div>
<p data-l10n-id="pad.loading" id="loading" class="editorloadingbox-message">
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
Loading...
</p>
<noscript>
<p class="editorloadingbox-message">
<strong>
Sorry, you have to enable Javascript in order to use this.
</strong>
</p>
</noscript>
</div>
<!------------------------------------------------------------->
<!-- SETTINGS POPUP (change font, language, chat parameters) -->
<!------------------------------------------------------------->
<div id="settings" class="popup"><div class="popup-content">
<h1 data-l10n-id="pad.settings.padSettings"></h1>
<h2 data-l10n-id="pad.settings.myView"></h2>
<p class="hide-for-mobile">
<input type="checkbox" id="options-stickychat">
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
</p>
<p class="hide-for-mobile">
<input type="checkbox" id="options-chatandusers" onClick="chat.chatAndUsers();">
<label for="options-chatandusers" data-l10n-id="pad.settings.chatandusers"></label>
</p>
<p>
<input type="checkbox" id="options-colorscheck">
<label for="options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
</p>
<p>
<input type="checkbox" id="options-linenoscheck" checked>
<label for="options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
</p>
<p>
<input type="checkbox" id="options-rtlcheck">
<label for="options-rtlcheck" data-l10n-id="pad.settings.rtlcheck"></label>
</p>
<div class="dropdowns-container">
<p class="dropdown-line">
<label for="viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
<select id="viewfontmenu">
<option value="" data-l10n-id="pad.settings.fontType.normal">Normal</option>
Quicksand,Roboto,Alegreya,PlayfairDisplay,Montserrat,OpenDyslexic,RobotoMono
<option value="Quicksand">Quicksand</option>
<option value="Roboto">Roboto</option>
<option value="Alegreya">Alegreya</option>
<option value="PlayfairDisplay">PlayfairDisplay</option>
<option value="Montserrat">Montserrat</option>
<option value="OpenDyslexic">OpenDyslexic</option>
<option value="RobotoMono">RobotoMono</option>
</select>
</p>
<p class="dropdown-line">
<label for="languagemenu" data-l10n-id="pad.settings.language">Language:</label>
<select id="languagemenu">
<option value="af">Afrikaans</option>
<option value="ar">العربية</option>
<option value="ast">asturianu</option>
<option value="az">azərbaycanca</option>
<option value="azb">تورکجه</option>
<option value="bcc">بلوچی مکرانی</option>
<option value="be-tarask">беларуская (тарашкевіца)</option>
<option value="bg">български</option>
<option value="bn">বাংলা</option>
<option value="br">brezhoneg</option>
<option value="bs">bosanski</option>
<option value="ca">català</option>
<option value="cs">česky</option>
<option value="da">dansk</option>
<option value="de">Deutsch</option>
<option value="diq">Zazaki</option>
<option value="dsb">dolnoserbski</option>
<option value="el">Ελληνικά</option>
<option value="en-gb">British English</option>
<option value="en">English</option>
<option value="eo">Esperanto</option>
<option value="es">español</option>
<option value="et">eesti</option>
<option value="eu">euskara</option>
<option value="fa">فارسی</option>
<option value="ff">Fulfulde</option>
<option value="fi">suomi</option>
<option value="fo">føroyskt</option>
<option value="fr">français</option>
<option value="fy">Frysk</option>
<option value="gl">galego</option>
<option value="gu">ગુજરાતી</option>
<option value="he">עברית</option>
<option value="hi">हिन्दी</option>
<option value="hr">hrvatski</option>
<option value="hsb">hornjoserbsce</option>
<option value="hu">magyar</option>
<option value="hy">Հայերեն</option>
<option value="ia">interlingua</option>
<option value="id">Bahasa Indonesia</option>
<option value="is">íslenska</option>
<option value="it">italiano</option>
<option value="ja">日本語</option>
<option value="kab">Taqbaylit</option>
<option value="km">ភាសាខ្មែរ</option>
<option value="kn">ಕನ್ನಡ</option>
<option value="ko">한국어</option>
<option value="krc">къарачай-малкъар</option>
<option value="ksh">Ripoarisch</option>
<option value="ku-latn">Kurdî (latînî)</option>
<option value="lb">Lëtzebuergesch</option>
<option value="lt">lietuvių</option>
<option value="lv">latviešu</option>
<option value="map-bms">Basa Banyumasan</option>
<option value="mg">Malagasy</option>
<option value="mk">македонски</option>
<option value="ml">മലയാളം</option>
<option value="mn">монгол</option>
<option value="mnw">ဘာသာ မန်</option>
<option value="mr">मराठी</option>
<option value="ms">Bahasa Melayu</option>
<option value="my">မြန်မာဘာသာ</option>
<option value="nah">Nāhuatl</option>
<option value="nap">Nnapulitano</option>
<option value="nb">norsk (bokmål)</option>
<option value="nds">Plattdüütsch</option>
<option value="ne">नेपाली</option>
<option value="nl">Nederlands</option>
<option value="nn">norsk (nynorsk)</option>
<option value="oc">occitan</option>
<option value="os">Ирон</option>
<option value="pa">ਪੰਜਾਬੀ</option>
<option value="pl">polski</option>
<option value="pms">Piemontèis</option>
<option value="ps">پښتو</option>
<option value="pt-br">português do Brasil</option>
<option value="pt">português</option>
<option value="qqq">Message documentation</option>
<option value="ro">română</option>
<option value="ru">русский</option>
<option value="sc">sardu</option>
<option value="sco">Scots</option>
<option value="sd">سنڌي</option>
<option value="sh">srpskohrvatski / српскохрватски</option>
<option value="shn">လိၵ်ႈတႆး</option>
<option value="sk">slovenčina</option>
<option value="sl">slovenščina</option>
<option value="sq">shqip</option>
<option value="sr-ec">српски (ћирилица)</option>
<option value="sr-el">srpski (latinica)</option>
<option value="sv">svenska</option>
<option value="sw">Kiswahili</option>
<option value="ta">தமிழ்</option>
<option value="tcy">ತುಳು</option>
<option value="te">తెలుగు</option>
<option value="th">ไทย</option>
<option value="tr">Türkçe</option>
<option value="uk">українська</option>
<option value="vec">vèneto</option>
<option value="vi">Tiếng Việt</option>
<option value="zh-hans">中文(简体)‎</option>
<option value="zh-hant">中文(繁體)‎</option>
</select>
</p>
</div>
<h2 data-l10n-id="pad.settings.about">About</h2>
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
<a href="https://etherpad.org">Etherpad</a>
</div></div>
<!------------------------->
<!-- IMPORT EXPORT POPUP -->
<!------------------------->
<div id="import_export" class="popup"><div class="popup-content">
<h1 data-l10n-id="pad.importExport.import_export"></h1>
<div class="acl-write">
<h2 data-l10n-id="pad.importExport.import"></h2>
<div class="importmessage" id="importmessageabiword" data-l10n-id="pad.importExport.abiword.innerHTML"></div><br>
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
<div class="importformdiv" id="importformfilediv">
<input type="file" name="file" size="10" id="importfileinput">
<div class="importmessage" id="importmessagefail"></div>
</div>
<div id="import"></div>
<div class="importmessage" id="importmessagesuccess" data-l10n-id="pad.importExport.importSuccessful"></div>
<div class="importformdiv" id="importformsubmitdiv">
<span class="nowrap">
<input type="submit" class="btn btn-primary" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput">
<div alt="" id="importstatusball" class="loadingAnimation" align="top"></div>
</span>
</div>
</form>
</div>
<div id="exportColumn">
<h2 data-l10n-id="pad.importExport.export"></h2>
<a id="exportetherpada" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-powerpoint" id="exportetherpad" data-l10n-id="pad.importExport.exportetherpad"></span>
</a>
<a id="exporthtmla" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-code" id="exporthtml" data-l10n-id="pad.importExport.exporthtml"></span>
</a>
<a id="exportplaina" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file" id="exportplain" data-l10n-id="pad.importExport.exportplain"></span>
</a>
<a id="exportworda" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-word" id="exportword" data-l10n-id="pad.importExport.exportword"></span>
</a>
<a id="exportpdfa" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-pdf" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></span>
</a>
<a id="exportopena" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-alt" id="exportopen" data-l10n-id="pad.importExport.exportopen"></span>
</a>
</div>
</div></div>
<!---------------------------------------------------->
<!-- CONNECTIVITY POPUP (when you get disconnected) -->
<!---------------------------------------------------->
<div id="connectivity" class="popup"><div class="popup-content">
<div class="connected visible">
<h2 data-l10n-id="pad.modals.connected"></h2>
</div>
<div class="reconnecting">
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
<i class='buttonicon buttonicon-spin5 icon-spin'>
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
</i>
</div>
<div class="userdup">
<h1 data-l10n-id="pad.modals.userdup"></h1>
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.userdup.advice"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="unauth">
<h1 data-l10n-id="pad.modals.unauth"></h1>
<p id="defaulttext" data-l10n-id="pad.modals.unauth.explanation"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="looping">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
<p data-l10n-id="pad.modals.looping.cause"></p>
</div>
<div class="initsocketfail">
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
</div>
<div class="slowcommit with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.slowcommit.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="badChangeset with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.badChangeset.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.badChangeset.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="corruptPad">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.corruptPad.explanation"></h2>
<p data-l10n-id="pad.modals.corruptPad.cause"></p>
</div>
<div class="deleted">
<h1 data-l10n-id="pad.modals.deleted"></h1>
<p data-l10n-id="pad.modals.deleted.explanation"></p>
</div>
<div class="rateLimited">
<h1 data-l10n-id="pad.modals.rateLimited"></h1>
<p data-l10n-id="pad.modals.rateLimited.explanation"></p>
</div>
<div class="rejected">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.rejected.explanation"></h2>
<p data-l10n-id="pad.modals.rejected.cause"></p>
</div>
<div class="disconnected with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.disconnected.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
<input type="hidden" class="padId" name="padId">
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
<input type="hidden" class="missedChanges" name="missedChanges">
</form>
</div></div>
<!-------------------------------->
<!-- EMBED POPUP (Share, embed) -->
<!-------------------------------->
<div id="embed" class="popup"><div class="popup-content">
<h1 data-l10n-id="pad.share"></h1>
<div id="embedreadonly" class="acl-write">
<input type="checkbox" id="readonlyinput">
<label for="readonlyinput" data-l10n-id="pad.share.readonly"></label>
</div>
<div id="linkcode">
<h2 data-l10n-id="pad.share.link"></h2>
<input id="linkinput" type="text" value="" onclick="this.select()">
</div>
<div id="embedcode">
<h2 data-l10n-id="pad.share.emebdcode"></h2>
<input id="embedinput" type="text" value="" onclick="this.select()">
</div>
</div></div>
<div class="sticky-container">
<!---------------------------------------------------------------------->
<!-- USERS POPUP (set username, color, see other users names & color) -->
<!---------------------------------------------------------------------->
<div id="users" class="popup"><div class="popup-content">
<div id="connectionstatus"></div>
<div id="myuser">
<div id="mycolorpicker" class="popup"><div class="popup-content">
<div id="colorpicker"></div>
<div class="btn-container">
<button id="mycolorpickersave" data-l10n-id="pad.colorpicker.save" class="btn btn-primary"></button>
<button id="mycolorpickercancel" data-l10n-id="pad.colorpicker.cancel" class="btn btn-default"></button>
<span id="mycolorpickerpreview" class="myswatchboxhoverable"></span>
</div>
</div></div>
<div id="myswatchbox"><div id="myswatch"></div></div>
<div id="myusernameform">
<input type="text" id="myusernameedit" disabled="disabled" data-l10n-id="pad.userlist.entername">
</div>
</div>
<div id="otherusers" aria-role="document">
<table id="otheruserstable" cellspacing="0" cellpadding="0" border="0">
<tr><td></td></tr>
</table>
</div>
<div id="userlistbuttonarea"></div>
</div></div>
<!----------------------------->
<!----------- CHAT ------------>
<!----------------------------->
<div id="chaticon" class="visible" onclick="chat.show();return false;" title="Chat (Alt C)">
<span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat"></span>
<span id="chatcounter">0</span>
</div>
<div id="chatbox">
<div class="chat-content">
<div id="titlebar">
<h1 id ="titlelabel" data-l10n-id="pad.chat"></h1>
<a id="titlecross" class="hide-reduce-btn" onClick="chat.hide();return false;">-&nbsp;</a>
<a id="titlesticky" class="stick-to-screen-btn" onClick="chat.stickToScreen(true);return false;" data-l10n-id="pad.chat.stick.title">&nbsp;&nbsp;</a>
</div>
<div id="chattext" class="thin-scrollbar" aria-live="polite" aria-relevant="additions removals text" role="log" aria-atomic="false">
<div alt="loading.." id="chatloadmessagesball" class="chatloadmessages loadingAnimation" align="top"></div>
<button id="chatloadmessagesbutton" class="chatloadmessages" data-l10n-id="pad.chat.loadmessages"></button>
</div>
<div id="chatinputbox">
<form>
<textarea id="chatinput" maxlength="999" data-l10n-id="pad.chat.writeMessage.placeholder"></textarea>
</form>
</div>
</div>
</div>
</div>
<!------------------------------------------------------------------>
<!-- SKIN VARIANTS BUILDER (Customize rendering, only for admins) -->
<!------------------------------------------------------------------>
<div id="skin-variants" class="popup"><div class="popup-content">
<h1>Skin Builder</h1>
<div class="dropdowns-container">
<p class="dropdown-line">
<label class="skin-variant-container">toolbar</label>
<select class="skin-variant skin-variant-color" data-container="toolbar">
<option value="super-light">Super Light</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="super-dark">Super Dark</option>
</select>
</p>
<p class="dropdown-line">
<label class="skin-variant-container">background</label>
<select class="skin-variant skin-variant-color" data-container="background">
<option value="super-light">Super Light</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="super-dark">Super Dark</option>
</select>
</p>
<p class="dropdown-line">
<label class="skin-variant-container">editor</label>
<select class="skin-variant skin-variant-color" data-container="editor">
<option value="super-light">Super Light</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="super-dark">Super Dark</option>
</select>
</p>
</div>
<p>
<input type="checkbox" id="skin-variant-full-width" class="skin-variant"/>
<label for="skin-variant-full-width">Full Width Editor</label>
</p>
<p>
<label>Result to copy in settings.json</label>
<input id="skin-variants-result" type="text" readonly class="disabled" />
</p>
</div></div>
</div> <!-- End of #editorcontainerbox -->
<!----------------------------->
<!-------- JAVASCRIPT --------->
<!----------------------------->
<script type="text/javascript" src="../static/skins/colibris/pad.js?v=5ba315cd"></script>
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
<script type="module" src="./node_modules/ep_etherpad-lite/templates/padViteBootstrap.js"></script>
</body>
</html>

View file

@ -1,10 +1,18 @@
// vite.config.js // vite.config.js
import { resolve } from 'path' import { resolve } from 'path'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vitePluginRequire from 'vite-plugin-require';
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
export default defineConfig({ export default defineConfig({
base: '/views/', base: '/views/',
plugins: [
viteCommonjs(),
],
build: { build: {
commonjsOptions:{
transformMixedEsModules: true,
},
outDir: resolve(__dirname, '../src/static/oidc'), outDir: resolve(__dirname, '../src/static/oidc'),
rollupOptions: { rollupOptions: {
input: { input: {
@ -14,4 +22,31 @@ export default defineConfig({
}, },
emptyOutDir: true, emptyOutDir: true,
}, },
server:{
proxy:{
'/static':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
},
'/views/manifest.json':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/views/, ''),
},
'/locales.json':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/views/, ''),
},
'/locales':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/views/, ''),
},
}
}
}) })

2
var/js/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.js
*.map