mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 06:03:34 +01:00
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:
parent
33b388b14c
commit
d6d636955c
53 changed files with 2764 additions and 1763 deletions
5
.github/workflows/frontend-admin-tests.yml
vendored
5
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -68,7 +68,7 @@ jobs:
|
|||
# rules.
|
||||
-
|
||||
name: Install all dependencies and symlink for ep_etherpad-lite
|
||||
run: bin/installDeps.sh
|
||||
run: pnpm i
|
||||
#-
|
||||
# name: Install etherpad plugins
|
||||
# run: rm -Rf node_modules/ep_align/static/tests/*
|
||||
|
@ -92,7 +92,6 @@ jobs:
|
|||
- name: Build admin frontend
|
||||
working-directory: admin
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build
|
||||
# name: Run the frontend admin tests
|
||||
# shell: bash
|
||||
|
@ -124,7 +123,7 @@ jobs:
|
|||
- name: Run the frontend admin tests
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run dev &
|
||||
pnpm run prod &
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
|
|
6
.github/workflows/frontend-tests.yml
vendored
6
.github/workflows/frontend-tests.yml
vendored
|
@ -59,7 +59,7 @@ jobs:
|
|||
- name: Run the frontend tests
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run dev &
|
||||
pnpm run prod &
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
|
@ -122,7 +122,7 @@ jobs:
|
|||
- name: Run the frontend tests
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run dev &
|
||||
pnpm run prod &
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
|
@ -192,7 +192,7 @@ jobs:
|
|||
- name: Run the frontend tests
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run dev &
|
||||
pnpm run prod &
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
|
|
7
.github/workflows/windows.yml
vendored
7
.github/workflows/windows.yml
vendored
|
@ -154,11 +154,12 @@ jobs:
|
|||
name: Run Etherpad
|
||||
working-directory: etherpad/src
|
||||
run: |
|
||||
pnpm install cypress
|
||||
.\node_modules\.bin\cypress.cmd install --force
|
||||
pnpm i
|
||||
pnpm exec playwright install --with-deps
|
||||
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
|
||||
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
|
||||
-
|
||||
name: Rename to etherpad-lite-win.zip
|
||||
|
|
|
@ -50,21 +50,13 @@ rm -rf src/node_modules || true
|
|||
#$(try cd ./bin/installDeps.sh)
|
||||
|
||||
# Install admin frontend
|
||||
cd admin
|
||||
try pnpm install
|
||||
try pnpm run build
|
||||
cd ..
|
||||
|
||||
|
||||
|
||||
try pnpm run build:etherpad
|
||||
|
||||
# Nuke the admin folder as it is not needed anymore :D
|
||||
rm -rf admin
|
||||
|
||||
export NODE_ENV=production
|
||||
try pnpm install --production
|
||||
|
||||
|
||||
rm -rf oidc
|
||||
rm -rf src/node_modules
|
||||
|
||||
log "copy the windows settings template..."
|
||||
try cp settings.json.template settings.json
|
||||
|
|
|
@ -78,7 +78,7 @@ jobs:
|
|||
- name: Run the frontend tests
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run dev &
|
||||
pnpm run prod &
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
|
|
|
@ -45,4 +45,4 @@ fi
|
|||
log "Starting Etherpad..."
|
||||
|
||||
# cd src
|
||||
exec pnpm run dev "$@"
|
||||
exec pnpm run prod "$@"
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"plugins": "pnpm --filter bin run plugins",
|
||||
"install-plugins": "pnpm --filter bin run plugins i",
|
||||
"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": {
|
||||
"ep_etherpad-lite": "workspace:./src"
|
||||
|
|
|
@ -152,6 +152,9 @@ importers:
|
|||
cookie-parser:
|
||||
specifier: ^1.4.6
|
||||
version: 1.4.6
|
||||
cross-env:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
cross-spawn:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
|
@ -294,6 +297,9 @@ importers:
|
|||
'@types/http-errors':
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
'@types/jquery':
|
||||
specifier: ^3.5.30
|
||||
version: 3.5.30
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
|
@ -321,6 +327,9 @@ importers:
|
|||
'@types/underscore':
|
||||
specifier: ^1.11.15
|
||||
version: 1.11.15
|
||||
chokidar:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
eslint:
|
||||
specifier: ^9.7.0
|
||||
version: 9.7.0
|
||||
|
@ -360,6 +369,9 @@ importers:
|
|||
|
||||
ui:
|
||||
devDependencies:
|
||||
ep_etherpad-lite:
|
||||
specifier: workspace:../src
|
||||
version: link:../src
|
||||
typescript:
|
||||
specifier: ^5.5.3
|
||||
version: 5.5.3
|
||||
|
@ -1489,6 +1501,9 @@ packages:
|
|||
'@types/http-errors@2.0.4':
|
||||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||
|
||||
'@types/jquery@3.5.30':
|
||||
resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==}
|
||||
|
||||
'@types/jsdom@21.1.7':
|
||||
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
||||
|
||||
|
@ -1576,6 +1591,9 @@ packages:
|
|||
'@types/sinonjs__fake-timers@8.1.5':
|
||||
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
|
||||
|
||||
'@types/sizzle@2.3.8':
|
||||
resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==}
|
||||
|
||||
'@types/superagent@8.1.7':
|
||||
resolution: {integrity: sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==}
|
||||
|
||||
|
@ -2080,6 +2098,11 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
|
||||
cross-env@7.0.3:
|
||||
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
|
||||
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
||||
hasBin: true
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -5456,6 +5479,10 @@ snapshots:
|
|||
|
||||
'@types/http-errors@2.0.4': {}
|
||||
|
||||
'@types/jquery@3.5.30':
|
||||
dependencies:
|
||||
'@types/sizzle': 2.3.8
|
||||
|
||||
'@types/jsdom@21.1.7':
|
||||
dependencies:
|
||||
'@types/node': 20.14.11
|
||||
|
@ -5558,6 +5585,8 @@ snapshots:
|
|||
|
||||
'@types/sinonjs__fake-timers@8.1.5': {}
|
||||
|
||||
'@types/sizzle@2.3.8': {}
|
||||
|
||||
'@types/superagent@8.1.7':
|
||||
dependencies:
|
||||
'@types/cookiejar': 2.1.5
|
||||
|
@ -6137,6 +6166,10 @@ snapshots:
|
|||
optionalDependencies:
|
||||
typescript: 5.5.3
|
||||
|
||||
cross-env@7.0.3:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
"name": "specialpages",
|
||||
"hooks": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -996,7 +996,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
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
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const eejs = require('../../eejs');
|
||||
const fs = require('fs');
|
||||
import path from 'node:path';
|
||||
const eejs = require('../../eejs')
|
||||
import fs from 'node:fs';
|
||||
const fsp = fs.promises;
|
||||
const toolbar = require('../../utils/toolbar');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const settings = require('../../utils/Settings');
|
||||
const util = require('util');
|
||||
import util from 'node:util';
|
||||
const webaccess = require('./webaccess');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
|
||||
import {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) => {
|
||||
// This endpoint is intended to conform to:
|
||||
|
@ -73,49 +82,263 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
});
|
||||
};
|
||||
|
||||
exports.expressCreateServer = (hookName:string, args:any, cb:Function) => {
|
||||
|
||||
|
||||
const convertTypescript = (content: string) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
function handleUpdate() {
|
||||
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
|
||||
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: '/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) => {
|
||||
args.app.get('/', (req: any, res: any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
|
||||
});
|
||||
|
||||
// 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,
|
||||
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);
|
||||
});
|
||||
|
||||
// can be removed when require-kernel is dropped
|
||||
res.header('Feature-Policy', 'sync-xhr \'self\'');
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', {
|
||||
req,
|
||||
toolbar,
|
||||
isReadOnly,
|
||||
}));
|
||||
});
|
||||
// serve timeslider.html under /p/$padname/timeslider
|
||||
args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => {
|
||||
hooks.callAll('padInitToolbar', {
|
||||
toolbar,
|
||||
});
|
||||
|
||||
// serve timeslider.html under /p/$padname/timeslider
|
||||
args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => {
|
||||
hooks.callAll('padInitToolbar', {
|
||||
toolbar,
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||
req,
|
||||
toolbar,
|
||||
entrypoint: "/"+fileNameTimeSlider
|
||||
}));
|
||||
});
|
||||
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||
req,
|
||||
toolbar,
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
await handleLiveReload(args, padString, timeSliderString)
|
||||
}
|
||||
|
||||
// The client occasionally polls this endpoint to get an updated expiration for the express_sid
|
||||
// cookie. This handler must be installed after the express-session middleware.
|
||||
args.app.put('/_extendExpressSessionLifetime', (req:any, res:any) => {
|
||||
args.app.put('/_extendExpressSessionLifetime', (req: any, res: any) => {
|
||||
// express-session automatically calls req.session.touch() so we don't need to do it here.
|
||||
res.json({status: 'ok'});
|
||||
});
|
||||
|
||||
return cb();
|
||||
};
|
||||
|
|
|
@ -300,12 +300,12 @@ const getFileCompressed = async (filename, contentType) => {
|
|||
try {
|
||||
logger.info('Compress CSS file %s.', filename);
|
||||
|
||||
const compressResult = await compressCSS(content);
|
||||
const compressResult = await compressCSS(path.resolve(ROOT_DIR,filename));
|
||||
|
||||
if (compressResult.error) {
|
||||
console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error);
|
||||
} else {
|
||||
content = compressResult.code.toString(); // Convert content obj code to string
|
||||
content = compressResult
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
||||
*/
|
||||
|
||||
const fsp = require('fs').promises;
|
||||
import {expose} from 'threads'
|
||||
import {transform} from 'esbuild';
|
||||
import {build, transform} from 'esbuild';
|
||||
|
||||
/*
|
||||
* Minify JS content
|
||||
|
@ -21,8 +20,23 @@ const compressJS = async (content) => {
|
|||
* @param {string} ROOT_DIR - the root dir of Etherpad
|
||||
*/
|
||||
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({
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* The Toolbar Module creates and renders the toolbars and buttons
|
||||
*/
|
||||
const _ = require('underscore');
|
||||
import {isString, reduce, each, isUndefined, map, first, last, extend, escape} from 'underscore';
|
||||
|
||||
const removeItem = (array: string[], what: string) => {
|
||||
let ax;
|
||||
|
@ -21,7 +21,7 @@ const defaultButtonAttributes = (name: string, overrides?: boolean) => ({
|
|||
const tag = (name: string, attributes: AttributeObj, contents?: string) => {
|
||||
const aStr = tagAttributes(attributes);
|
||||
|
||||
if (_.isString(contents) && contents!.length > 0) {
|
||||
if (isString(contents) && contents!.length > 0) {
|
||||
return `<${name}${aStr}>${contents}</${name}>`;
|
||||
} else {
|
||||
return `<${name}${aStr}></${name}>`;
|
||||
|
@ -34,14 +34,14 @@ type AttributeObj = {
|
|||
}
|
||||
|
||||
const tagAttributes = (attributes: AttributeObj) => {
|
||||
attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
|
||||
if (!_.isUndefined(val)) {
|
||||
attributes = reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
|
||||
if (!isUndefined(val)) {
|
||||
o[name] = val;
|
||||
}
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
return ` ${_.map(attributes, (val: string, name: string) => `${name}="${_.escape(val)}"`).join(' ')}`;
|
||||
return ` ${map(attributes, (val: string, name: string) => `${name}="${escape(val)}"`).join(' ')}`;
|
||||
};
|
||||
|
||||
type ButtonGroupType = {
|
||||
|
@ -58,7 +58,7 @@ class ButtonGroup {
|
|||
|
||||
public static fromArray = function (array: string[]) {
|
||||
const btnGroup = new ButtonGroup();
|
||||
_.each(array, (btnName: string) => {
|
||||
each(array, (btnName: string) => {
|
||||
const button = Button.load(btnName) as Button
|
||||
btnGroup.addButton(button);
|
||||
});
|
||||
|
@ -70,18 +70,19 @@ class ButtonGroup {
|
|||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): string {
|
||||
if (this.buttons && this.buttons.length === 1) {
|
||||
this.buttons[0].grouping = '';
|
||||
} else if (this.buttons && this.buttons.length > 1) {
|
||||
_.first(this.buttons).grouping = 'grouped-left';
|
||||
_.last(this.buttons).grouping = 'grouped-right';
|
||||
_.each(this.buttons.slice(1, -1), (btn: Button) => {
|
||||
first(this.buttons)!.grouping = 'grouped-left';
|
||||
last(this.buttons)!.grouping = 'grouped-right';
|
||||
each(this.buttons.slice(1, -1), (btn: Button) => {
|
||||
btn.grouping = 'grouped-middle';
|
||||
});
|
||||
}
|
||||
|
||||
return _.map(this.buttons, (btn: ButtonGroup) => {
|
||||
// @ts-ignore
|
||||
return map(this.buttons, (btn: ButtonGroup) => {
|
||||
if (btn) return btn.render();
|
||||
}).join('\n');
|
||||
}
|
||||
|
@ -151,8 +152,8 @@ class SelectButton extends Button {
|
|||
select(attributes: AttributeObj) {
|
||||
const options: string[] = [];
|
||||
|
||||
_.each(this.options, (opt: AttributeSelect) => {
|
||||
const a = _.extend({
|
||||
each(this.options, (opt: AttributeSelect) => {
|
||||
const a = extend({
|
||||
value: opt.value,
|
||||
}, opt.attributes);
|
||||
|
||||
|
@ -299,7 +300,7 @@ module.exports = {
|
|||
buttons[0].push('savedrevision');
|
||||
}
|
||||
|
||||
const groups = _.map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render());
|
||||
const groups = map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render());
|
||||
return groups.join(this.separator());
|
||||
},
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"async": "^3.2.5",
|
||||
"axios": "^1.7.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"ejs": "^3.1.10",
|
||||
"esbuild": "^0.23.0",
|
||||
|
@ -87,6 +88,7 @@
|
|||
"@types/express": "^4.17.21",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/http-errors": "^2.0.4",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mocha": "^10.0.7",
|
||||
|
@ -96,6 +98,7 @@
|
|||
"@types/sinon": "^17.0.3",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/underscore": "^1.11.15",
|
||||
"chokidar": "^3.6.0",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-etherpad": "^4.0.4",
|
||||
"etherpad-cli-client": "^3.0.2",
|
||||
|
@ -120,17 +123,17 @@
|
|||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts ../node_modules/ep_*/static/tests/backend/specs/**",
|
||||
"test-utils": "mocha --import=tsx --timeout 5000 --recursive tests/backend/specs/*utils.ts",
|
||||
"test": "cross-env NODE_ENV=production mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts ../node_modules/ep_*/static/tests/backend/specs/**",
|
||||
"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",
|
||||
"dev": "node --require tsx/cjs node/server.ts",
|
||||
"prod": "node --require tsx/cjs node/server.ts",
|
||||
"dev": "cross-env NODE_ENV=development node --require tsx/cjs node/server.ts",
|
||||
"prod": "cross-env NODE_ENV=production node --require tsx/cjs node/server.ts",
|
||||
"ts-check": "tsc --noEmit",
|
||||
"ts-check:watch": "tsc --noEmit --watch",
|
||||
"test-ui": "npx playwright test tests/frontend-new/specs",
|
||||
"test-ui:ui": "npx playwright test tests/frontend-new/specs --ui",
|
||||
"test-admin": "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-ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs",
|
||||
"test-ui:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs --ui",
|
||||
"test-admin": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --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"
|
||||
},
|
||||
"version": "2.1.1",
|
||||
|
|
|
@ -4,7 +4,7 @@ const AttributeMap = require('./AttributeMap');
|
|||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const attributes = require('./attributes');
|
||||
const _ = require('./underscore');
|
||||
const underscore = require("underscore")
|
||||
|
||||
const lineMarkerAttribute = 'lmkr';
|
||||
|
||||
|
@ -45,7 +45,7 @@ const AttributeManager = function (rep, applyChangesetCallback) {
|
|||
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
|
||||
AttributeManager.lineAttributes = lineAttributes;
|
||||
|
||||
AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
||||
AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({
|
||||
|
||||
applyChangeset(changeset) {
|
||||
if (!this.applyChangesetCallback) return changeset;
|
||||
|
@ -335,7 +335,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||
|
||||
const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1])
|
||||
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||
|
||||
// if we have marker and any of attributes don't need to have marker. we need delete it
|
||||
|
|
|
@ -27,9 +27,10 @@
|
|||
const hooks = require('./pluginfw/hooks');
|
||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||
const pluginUtils = require('./pluginfw/shared');
|
||||
|
||||
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
|
||||
const debugLog = (...args) => {};
|
||||
|
||||
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins')
|
||||
const rJQuery = require('ep_etherpad-lite/static/js/rjquery')
|
||||
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
|
||||
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
|
||||
// errors out unless given an absolute URL for a JavaScript-created element.
|
||||
|
@ -257,19 +258,19 @@ const Ace2Editor = function () {
|
|||
|
||||
// <head> tag
|
||||
addStyleTagsFor(innerDocument, includedCSS);
|
||||
const requireKernel = innerDocument.createElement('script');
|
||||
requireKernel.type = 'text/javascript';
|
||||
requireKernel.src =
|
||||
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
||||
innerDocument.head.appendChild(requireKernel);
|
||||
//const requireKernel = innerDocument.createElement('script');
|
||||
//requireKernel.type = 'text/javascript';
|
||||
//requireKernel.src =
|
||||
// absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
||||
//innerDocument.head.appendChild(requireKernel);
|
||||
// Pre-fetch modules to improve load performance.
|
||||
for (const module of ['ace2_inner', 'ace2_common']) {
|
||||
/*for (const module of ['ace2_inner', 'ace2_common']) {
|
||||
const script = innerDocument.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
|
||||
`?callback=require.define&v=${clientVars.randomVersionString}`);
|
||||
innerDocument.head.appendChild(script);
|
||||
}
|
||||
}*/
|
||||
const innerStyle = innerDocument.createElement('style');
|
||||
innerStyle.type = 'text/css';
|
||||
innerStyle.title = 'dynamicsyntax';
|
||||
|
@ -284,7 +285,7 @@ const Ace2Editor = function () {
|
|||
innerDocument.body.classList.add('innerdocbody');
|
||||
innerDocument.body.setAttribute('spellcheck', 'false');
|
||||
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //
|
||||
|
||||
/*
|
||||
debugLog('Ace2Editor.init() waiting for require kernel load');
|
||||
await eventFired(requireKernel, 'load');
|
||||
debugLog('Ace2Editor.init() require kernel loaded');
|
||||
|
@ -292,17 +293,16 @@ const Ace2Editor = function () {
|
|||
require.setRootURI(absUrl('../javascripts/src'));
|
||||
require.setLibraryURI(absUrl('../javascripts/lib'));
|
||||
require.setGlobalKeyPath('require');
|
||||
|
||||
*/
|
||||
// intentially moved before requiring client_plugins to save a 307
|
||||
innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
|
||||
innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||
innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow);
|
||||
innerWindow.Ace2Inner = ace2_inner;
|
||||
innerWindow.plugins = cl_plugins;
|
||||
|
||||
innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
|
||||
innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery;
|
||||
|
||||
debugLog('Ace2Editor.init() waiting for plugins');
|
||||
await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
||||
(err) => err != null ? reject(err) : resolve()));
|
||||
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
||||
(err) => err != null ? reject(err) : resolve()));*/
|
||||
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
|
||||
await innerWindow.Ace2Inner.init(info, {
|
||||
inner: makeCSSManager(innerStyle.sheet),
|
||||
|
|
|
@ -30,6 +30,8 @@ const setAssoc = Ace2Common.setAssoc;
|
|||
const noop = Ace2Common.noop;
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import Scroll from './scroll'
|
||||
|
||||
function Ace2Inner(editorInfo, cssManagers) {
|
||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
|
@ -42,7 +44,6 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const SkipList = require('./skiplist');
|
||||
const undoModule = require('./undomodule').undoModule;
|
||||
const AttributeManager = require('./AttributeManager');
|
||||
const Scroll = require('./scroll');
|
||||
const DEBUG = false;
|
||||
|
||||
const THE_TAB = ' '; // 4
|
||||
|
@ -54,13 +55,16 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
let thisAuthor = '';
|
||||
|
||||
let disposed = false;
|
||||
const outerWin = document.getElementsByName("ace_outer")[0]
|
||||
const targetDoc = outerWin.contentWindow.document.getElementsByName("ace_inner")[0].contentWindow.document
|
||||
const targetBody = targetDoc.body
|
||||
|
||||
const focus = () => {
|
||||
window.focus();
|
||||
targetBody.focus();
|
||||
};
|
||||
|
||||
const outerWin = window.parent;
|
||||
const outerDoc = outerWin.document;
|
||||
const outerDoc = outerWin.contentWindow.document;
|
||||
|
||||
const sideDiv = outerDoc.getElementById('sidediv');
|
||||
const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv');
|
||||
const sideDivInner = outerDoc.getElementById('sidedivinner');
|
||||
|
@ -74,7 +78,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
appendNewSideDivLine();
|
||||
|
||||
const scroll = Scroll.init(outerWin);
|
||||
const scroll = new Scroll(outerWin);
|
||||
|
||||
let outsideKeyDown = noop;
|
||||
let outsideKeyPress = (e) => true;
|
||||
|
@ -415,7 +419,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
const setWraps = (newVal) => {
|
||||
doesWrap = newVal;
|
||||
document.body.classList.toggle('doesWrap', doesWrap);
|
||||
targetBody.classList.toggle('doesWrap', doesWrap);
|
||||
scheduler.setTimeout(() => {
|
||||
inCallStackIfNecessary('setWraps', () => {
|
||||
fastIncorp(7);
|
||||
|
@ -445,7 +449,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const setTextFace = (face) => {
|
||||
document.body.style.fontFamily = face;
|
||||
targetBody.style.fontFamily = face;
|
||||
lineMetricsDiv.style.fontFamily = face;
|
||||
};
|
||||
|
||||
|
@ -456,8 +460,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
const setEditable = (newVal) => {
|
||||
isEditable = newVal;
|
||||
document.body.contentEditable = isEditable ? 'true' : 'false';
|
||||
document.body.classList.toggle('static', !isEditable);
|
||||
targetBody.contentEditable = isEditable ? 'true' : 'false';
|
||||
targetBody.classList.toggle('static', !isEditable);
|
||||
};
|
||||
|
||||
const enforceEditability = () => setEditable(isEditable);
|
||||
|
@ -480,6 +484,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
newText = `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
|
||||
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||
setDocText(newText);
|
||||
});
|
||||
|
@ -640,8 +645,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// These properties are exposed
|
||||
const setters = {
|
||||
wraps: setWraps,
|
||||
showsauthorcolors: (val) => document.body.classList.toggle('authorColors', !!val),
|
||||
showsuserselections: (val) => document.body.classList.toggle('userSelections', !!val),
|
||||
showsauthorcolors: (val) => targetBody.classList.toggle('authorColors', !!val),
|
||||
showsuserselections: (val) => targetBody.classList.toggle('userSelections', !!val),
|
||||
showslinenumbers: (value) => {
|
||||
hasLineNumbers = !!value;
|
||||
sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers);
|
||||
|
@ -654,8 +659,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
styled: setStyled,
|
||||
textface: setTextFace,
|
||||
rtlistrue: (value) => {
|
||||
document.body.classList.toggle('rtl', value);
|
||||
document.body.classList.toggle('ltr', !value);
|
||||
targetBody.classList.toggle('rtl', value);
|
||||
targetBody.classList.toggle('ltr', !value);
|
||||
document.documentElement.dir = value ? 'rtl' : 'ltr';
|
||||
},
|
||||
};
|
||||
|
@ -894,11 +899,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
clearObservedChanges();
|
||||
|
||||
const getCleanNodeByKey = (key) => {
|
||||
let n = document.getElementById(key);
|
||||
let n = targetDoc.getElementById(key);
|
||||
// copying and pasting can lead to duplicate ids
|
||||
while (n && isNodeDirty(n)) {
|
||||
n.id = '';
|
||||
n = document.getElementById(key);
|
||||
n = targetDoc.getElementById(key);
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
@ -980,11 +985,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const observeSuspiciousNodes = () => {
|
||||
// inspired by Firefox bug #473255, where pasting formatted text
|
||||
// causes the cursor to jump away, making the new HTML never found.
|
||||
if (document.body.getElementsByTagName) {
|
||||
const elts = document.body.getElementsByTagName('style');
|
||||
if (targetBody.getElementsByTagName) {
|
||||
const elts = targetBody.getElementsByTagName('style');
|
||||
for (const elt of elts) {
|
||||
const n = topLevel(elt);
|
||||
if (n && n.parentNode === document.body) {
|
||||
if (n && n.parentNode === targetBody) {
|
||||
observeChangesAroundNode(n);
|
||||
}
|
||||
}
|
||||
|
@ -999,8 +1004,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
|
||||
|
||||
// returns true if dom changes were made
|
||||
if (!document.body.firstChild) {
|
||||
document.body.innerHTML = '<div><!-- --></div>';
|
||||
if (!targetBody.firstChild) {
|
||||
targetBody.innerHTML = '<div><!-- --></div>';
|
||||
}
|
||||
|
||||
observeChangesAroundSelection();
|
||||
|
@ -1022,7 +1027,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
j++;
|
||||
}
|
||||
if (!dirtyRangesCheckOut) {
|
||||
for (const bodyNode of document.body.childNodes) {
|
||||
for (const bodyNode of targetBody.childNodes) {
|
||||
if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) {
|
||||
observeChangesAroundNode(bodyNode);
|
||||
}
|
||||
|
@ -1044,11 +1049,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const range = dirtyRanges[i];
|
||||
a = range[0];
|
||||
b = range[1];
|
||||
let firstDirtyNode = (((a === 0) && document.body.firstChild) ||
|
||||
let firstDirtyNode = (((a === 0) && targetBody.firstChild) ||
|
||||
getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
|
||||
firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
|
||||
|
||||
let lastDirtyNode = (((b === rep.lines.length()) && document.body.lastChild) ||
|
||||
let lastDirtyNode = (((b === rep.lines.length()) && targetBody.lastChild) ||
|
||||
getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
|
||||
|
||||
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
|
||||
|
@ -1135,7 +1140,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
callstack: currentCallStack,
|
||||
editorInfo,
|
||||
rep,
|
||||
root: document.body,
|
||||
root: targetBody,
|
||||
point: selection.startPoint,
|
||||
documentAttributeManager,
|
||||
});
|
||||
|
@ -1147,7 +1152,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
callstack: currentCallStack,
|
||||
editorInfo,
|
||||
rep,
|
||||
root: document.body,
|
||||
root: targetBody,
|
||||
point: selection.endPoint,
|
||||
documentAttributeManager,
|
||||
});
|
||||
|
@ -1227,9 +1232,9 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
info.prepareForAdd();
|
||||
entry.lineMarker = info.lineMarker;
|
||||
if (!nodeToAddAfter) {
|
||||
document.body.insertBefore(node, document.body.firstChild);
|
||||
targetBody.insertBefore(node, targetBody.firstChild);
|
||||
} else {
|
||||
document.body.insertBefore(node, nodeToAddAfter.nextSibling);
|
||||
targetBody.insertBefore(node, nodeToAddAfter.nextSibling);
|
||||
}
|
||||
nodeToAddAfter = node;
|
||||
info.notifyAdded();
|
||||
|
@ -1326,7 +1331,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// Turn DOM node selection into [line,char] selection.
|
||||
// This method has to work when the DOM is not pristine,
|
||||
// assuming the point is not in a dirty node.
|
||||
if (point.node === document.body) {
|
||||
if (point.node === targetBody) {
|
||||
if (point.index === 0) {
|
||||
return [0, 0];
|
||||
} else {
|
||||
|
@ -1345,7 +1350,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
col = nodeText(n).length;
|
||||
}
|
||||
let parNode, prevSib;
|
||||
while ((parNode = n.parentNode) !== document.body) {
|
||||
while ((parNode = n.parentNode) !== targetBody) {
|
||||
if ((prevSib = n.previousSibling)) {
|
||||
n = prevSib;
|
||||
col += nodeText(n).length;
|
||||
|
@ -1398,7 +1403,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo));
|
||||
|
||||
for (const k of keysToDelete) {
|
||||
const n = document.getElementById(k);
|
||||
const n = targetDoc.getElementById(k);
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
|
||||
|
@ -2087,7 +2092,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const a = cleanNodeForIndex(i - 1);
|
||||
const b = cleanNodeForIndex(i);
|
||||
if ((!a) || (!b)) return false; // violates precondition
|
||||
if ((a === true) && (b === true)) return !document.body.firstChild;
|
||||
if ((a === true) && (b === true)) return !targetBody.firstChild;
|
||||
if ((a === true) && b.previousSibling) return false;
|
||||
if ((b === true) && a.nextSibling) return false;
|
||||
if ((a === true) || (b === true)) return true;
|
||||
|
@ -2232,7 +2237,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const isNodeDirty = (n) => {
|
||||
if (n.parentNode !== document.body) return true;
|
||||
if (n.parentNode !== targetBody) return true;
|
||||
const data = getAssoc(n, 'dirtiness');
|
||||
if (!data) return true;
|
||||
if (n.id !== data.nodeId) return true;
|
||||
|
@ -2856,7 +2861,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
updateBrowserSelectionFromRep();
|
||||
// get the current caret selection, can't use rep. here because that only gives
|
||||
// us the start position not the current
|
||||
const myselection = document.getSelection();
|
||||
const myselection = targetDoc.getSelection();
|
||||
// get the carets selection offset in px IE 214
|
||||
let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
|
||||
myselection.focusNode.offsetTop;
|
||||
|
@ -2970,13 +2975,13 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// with background doesn't seem to show up...
|
||||
if (isNodeText(p.node) && p.index === p.maxIndex) {
|
||||
let n = p.node;
|
||||
while (!n.nextSibling && n !== document.body && n.parentNode !== document.body) {
|
||||
while (!n.nextSibling && n !== targetBody && n.parentNode !== targetBody) {
|
||||
n = n.parentNode;
|
||||
}
|
||||
if (n.nextSibling &&
|
||||
!(typeof n.nextSibling.tagName === 'string' &&
|
||||
n.nextSibling.tagName.toLowerCase() === 'br') &&
|
||||
n !== p.node && n !== document.body && n.parentNode !== document.body) {
|
||||
n !== p.node && n !== targetBody && n.parentNode !== targetBody) {
|
||||
// found a parent, go to next node and dive in
|
||||
p.node = n.nextSibling;
|
||||
p.maxIndex = nodeMaxIndex(p.node);
|
||||
|
@ -3003,7 +3008,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
}
|
||||
};
|
||||
const browserSelection = window.getSelection();
|
||||
const browserSelection = targetDoc.getSelection();
|
||||
if (browserSelection) {
|
||||
browserSelection.removeAllRanges();
|
||||
if (selection) {
|
||||
|
@ -3078,7 +3083,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// each of which has node (a magicdom node), index, and maxIndex. If the node
|
||||
// is a text node, maxIndex is the length of the text; else maxIndex is 1.
|
||||
// index is between 0 and maxIndex, inclusive.
|
||||
const browserSelection = window.getSelection();
|
||||
const browserSelection = targetDoc.getSelection();
|
||||
if (!browserSelection || browserSelection.type === 'None' ||
|
||||
browserSelection.rangeCount === 0) {
|
||||
return null;
|
||||
|
@ -3096,7 +3101,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (!isInBody(container)) {
|
||||
// command-click in Firefox selects whole document, HEAD and BODY!
|
||||
return {
|
||||
node: document.body,
|
||||
node: targetBody,
|
||||
index: 0,
|
||||
maxIndex: 1,
|
||||
};
|
||||
|
@ -3146,7 +3151,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
browserSelection.anchorOffset === range.endOffset,
|
||||
};
|
||||
|
||||
if (selection.startPoint.node.ownerDocument !== window.document) {
|
||||
if (selection.startPoint.node.ownerDocument !== targetDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -3181,17 +3186,17 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
editorInfo.ace_getInInternationalComposition = () => inInternationalComposition;
|
||||
|
||||
const bindTheEventHandlers = () => {
|
||||
$(document).on('keydown', handleKeyEvent);
|
||||
$(document).on('keypress', handleKeyEvent);
|
||||
$(document).on('keyup', handleKeyEvent);
|
||||
$(document).on('click', handleClick);
|
||||
$(targetDoc).on('keydown', handleKeyEvent);
|
||||
$(targetDoc).on('keypress', handleKeyEvent);
|
||||
$(targetDoc).on('keyup', handleKeyEvent);
|
||||
$(targetDoc).on('click', handleClick);
|
||||
// dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer
|
||||
$(outerDoc).on('click', hideEditBarDropdowns);
|
||||
|
||||
// If non-nullish, pasting on a link should be suppressed.
|
||||
let suppressPasteOnLink = null;
|
||||
|
||||
$(document.body).on('auxclick', (e) => {
|
||||
$(targetBody).on('auxclick', (e) => {
|
||||
if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) {
|
||||
// The user middle-clicked on a link. Usually users do this to open a link in a new tab, but
|
||||
// in X11 (Linux) this will instead paste the contents of the primary selection at the mouse
|
||||
|
@ -3213,7 +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')) {
|
||||
scheduler.clearTimeout(suppressPasteOnLink);
|
||||
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
|
||||
// in Google Chrome. This bug will cause the last character on the last line to
|
||||
// not fire an event when dropped into..
|
||||
$(document).on('drop', (e) => {
|
||||
$(targetBody).on('drop', (e) => {
|
||||
if (e.target.a || e.target.localName === 'a') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
@ -3251,7 +3256,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const lineAfterSelection = lastLineSelected.nextSibling;
|
||||
|
||||
const neighbor = lineBeforeSelection || lineAfterSelection;
|
||||
neighbor.appendChild(document.createElement('style'));
|
||||
neighbor.appendChild(targetDoc.createElement('style'));
|
||||
}
|
||||
|
||||
// Call drop hook
|
||||
|
@ -3263,10 +3268,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
});
|
||||
});
|
||||
|
||||
$(document.documentElement).on('compositionstart', () => {
|
||||
$(targetDoc.documentElement).on('compositionstart', () => {
|
||||
if (inInternationalComposition) return;
|
||||
inInternationalComposition = new Promise((resolve) => {
|
||||
$(document.documentElement).one('compositionend', () => {
|
||||
$(targetDoc.documentElement).one('compositionend', () => {
|
||||
inInternationalComposition = null;
|
||||
resolve();
|
||||
});
|
||||
|
@ -3275,8 +3280,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const topLevel = (n) => {
|
||||
if ((!n) || n === document.body) return null;
|
||||
while (n.parentNode !== document.body) {
|
||||
if ((!n) || n === targetBody) return null;
|
||||
while (n.parentNode !== targetBody) {
|
||||
n = n.parentNode;
|
||||
}
|
||||
return n;
|
||||
|
@ -3436,10 +3441,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// but as it's non-text type the line-height/margins might not be present and it
|
||||
// could be that this breaks a theme that has a different default line height..
|
||||
// So instead of using an integer here we get the value from the Editor CSS.
|
||||
const innerdocbodyStyles = getComputedStyle(document.body);
|
||||
const innerdocbodyStyles = getComputedStyle(targetBody);
|
||||
const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']);
|
||||
|
||||
for (const docLine of document.body.children) {
|
||||
for (const docLine of targetBody.children) {
|
||||
let h;
|
||||
const nextDocLine = docLine.nextElementSibling;
|
||||
if (nextDocLine) {
|
||||
|
@ -3450,7 +3455,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// included on the first line. The default stylesheet doesn't add
|
||||
// extra margins/padding, but plugins might.
|
||||
h = nextDocLine.offsetTop - parseInt(
|
||||
window.getComputedStyle(document.body)
|
||||
window.getComputedStyle(targetBody)
|
||||
.getPropertyValue('padding-top').split('px')[0]);
|
||||
} else {
|
||||
h = nextDocLine.offsetTop - docLine.offsetTop;
|
||||
|
@ -3496,15 +3501,15 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
this.init = async () => {
|
||||
await $.ready;
|
||||
inCallStack('setup', () => {
|
||||
if (browser.firefox) $(document.body).addClass('mozilla');
|
||||
if (browser.safari) $(document.body).addClass('safari');
|
||||
document.body.classList.toggle('authorColors', true);
|
||||
document.body.classList.toggle('doesWrap', doesWrap);
|
||||
if (browser.firefox) $(targetBody).addClass('mozilla');
|
||||
if (browser.safari) $(targetBody).addClass('safari');
|
||||
targetBody.classList.toggle('authorColors', true);
|
||||
targetBody.classList.toggle('doesWrap', doesWrap);
|
||||
|
||||
enforceEditability();
|
||||
|
||||
// set up dom and rep
|
||||
while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
|
||||
while (targetBody.firstChild) targetBody.removeChild(targetBody.firstChild);
|
||||
const oneEntry = createDomLineEntry('');
|
||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||
insertDomLines(null, [oneEntry.domInfo]);
|
||||
|
|
|
@ -32,6 +32,9 @@ const colorutils = require('./colorutils').colorutils;
|
|||
const _ = require('./underscore');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
|
||||
// These parameters were global, now they are injected. A reference to the
|
||||
// Timeslider controller would probably be more appropriate.
|
||||
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
const _ = require('./underscore');
|
||||
const padmodals = require('./pad_modals').padmodals;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||
let BroadcastSlider;
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
// One rep.line(div) can be broken in more than one line in the browser.
|
||||
// This function is useful to get the caret position of the line as
|
||||
// is represented by the browser
|
||||
exports.getPosition = () => {
|
||||
import {Position, RepModel, RepNode} from "./types/RepModel";
|
||||
|
||||
export const getPosition = () => {
|
||||
const range = getSelectionRange();
|
||||
// @ts-ignore
|
||||
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
|
||||
// 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;
|
||||
};
|
||||
|
||||
const createSelectionRange = (range) => {
|
||||
const createSelectionRange = (range: Range) => {
|
||||
const clonedRange = range.cloneRange();
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
const getPositionOfRepLineAtOffset = (node, offset) => {
|
||||
const getPositionOfRepLineAtOffset = (node: any, offset: number) => {
|
||||
// it is not a text node, so we cannot make a selection
|
||||
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
|
||||
return getPositionOfElementOrSelection(node);
|
||||
}
|
||||
|
||||
while (node.length === 0 && node.nextSibling) {
|
||||
node = node.nextSibling;
|
||||
node = node.nextSibling as any;
|
||||
}
|
||||
|
||||
const newRange = new Range();
|
||||
|
@ -48,14 +51,13 @@ const getPositionOfRepLineAtOffset = (node, offset) => {
|
|||
return linePosition;
|
||||
};
|
||||
|
||||
const getPositionOfElementOrSelection = (element) => {
|
||||
const getPositionOfElementOrSelection = (element: Range):Position => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const linePosition = {
|
||||
return {
|
||||
bottom: rect.bottom,
|
||||
height: rect.height,
|
||||
top: rect.top,
|
||||
};
|
||||
return linePosition;
|
||||
} satisfies Position;
|
||||
};
|
||||
|
||||
// here we have two possibilities:
|
||||
|
@ -64,7 +66,7 @@ const getPositionOfElementOrSelection = (element) => {
|
|||
// where is the top of the previous line
|
||||
// [2] the line before is part of another rep line. It's possible this line has different margins
|
||||
// height. So we have to get the exactly position of the line
|
||||
exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
|
||||
export const getPositionTopOfPreviousBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
|
||||
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
|
||||
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
|
||||
|
||||
|
@ -80,7 +82,7 @@ exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
|
|||
return previousLineTop;
|
||||
};
|
||||
|
||||
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
|
||||
const caretLineIsFirstBrowserLine = (caretLineTop: number, rep: RepModel) => {
|
||||
const caretRepLine = rep.selStart[0];
|
||||
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||
const firstRootNode = getFirstRootChildNode(lineNode);
|
||||
|
@ -91,7 +93,7 @@ const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
|
|||
};
|
||||
|
||||
// find the first root node, usually it is a text node
|
||||
const getFirstRootChildNode = (node) => {
|
||||
const getFirstRootChildNode = (node: RepNode) => {
|
||||
if (!node.firstChild) {
|
||||
return node;
|
||||
} 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 lastRootChildNode = getLastRootChildNode(lineNode);
|
||||
|
||||
|
@ -109,7 +111,7 @@ const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
|
|||
return lastRootChildNodePosition;
|
||||
};
|
||||
|
||||
const getLastRootChildNode = (node) => {
|
||||
const getLastRootChildNode = (node: RepNode) => {
|
||||
if (!node.lastChild) {
|
||||
return {
|
||||
node,
|
||||
|
@ -125,7 +127,7 @@ const getLastRootChildNode = (node) => {
|
|||
// So, we can use the caret line to calculate the bottom of the line.
|
||||
// [2] the next line is part of another rep line.
|
||||
// It's possible this line has different dimensions, so we have to get the exactly dimension of it
|
||||
exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
|
||||
export const getBottomOfNextBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
|
||||
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
|
||||
const isCaretLineLastBrowserLine =
|
||||
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
|
||||
|
@ -142,7 +144,7 @@ exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
|
|||
return nextLineBottom;
|
||||
};
|
||||
|
||||
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
|
||||
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop: number, rep: RepModel) => {
|
||||
const caretRepLine = rep.selStart[0];
|
||||
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||
const lastRootChildNode = getLastRootChildNode(lineNode);
|
||||
|
@ -153,7 +155,7 @@ const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
|
|||
return lastRootChildNodePosition.top === caretLineTop;
|
||||
};
|
||||
|
||||
const getPreviousVisibleLine = (line, rep) => {
|
||||
export const getPreviousVisibleLine = (line: number, rep: RepModel): number => {
|
||||
const firstLineOfPad = 0;
|
||||
if (line <= 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;
|
||||
if (line >= lastLineOfThePad) {
|
||||
return lastLineOfThePad;
|
||||
|
@ -177,11 +178,10 @@ const getNextVisibleLine = (line, 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 firstRootChildNode = getFirstRootChildNode(lineNode);
|
||||
|
|
@ -21,10 +21,12 @@ const padcookie = require('./pad_cookie').padcookie;
|
|||
const Tinycon = require('tinycon/tinycon');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const padeditor = require('./pad_editor').padeditor;
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
|
||||
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||
|
||||
|
||||
exports.chat = (() => {
|
||||
let isStuck = false;
|
||||
let userAndChat = false;
|
||||
|
|
|
@ -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
18
src/static/js/l10n.ts
Normal 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()!;
|
||||
});
|
|
@ -24,12 +24,15 @@
|
|||
|
||||
let socket;
|
||||
|
||||
|
||||
// These jQuery things should create local references, but for now `require()`
|
||||
// assigns to the global `$` and augments it with plugins.
|
||||
require('./vendors/jquery');
|
||||
require('./vendors/farbtastic');
|
||||
require('./vendors/gritter');
|
||||
|
||||
import html10n from './vendors/html10n'
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
const chat = require('./chat').chat;
|
||||
const getCollabClient = require('./collab_client').getCollabClient;
|
||||
|
@ -136,7 +139,8 @@ const getParameters = [
|
|||
name: 'lang',
|
||||
checkVal: null,
|
||||
callback: (val) => {
|
||||
window.html10n.localize([val, 'en']);
|
||||
console.log('Val is', val)
|
||||
html10n.localize([val, 'en']);
|
||||
Cookies.set('language', val);
|
||||
},
|
||||
},
|
||||
|
@ -281,6 +285,7 @@ const handshake = async () => {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
socket.on('error', (error) => {
|
||||
// pad.collabClient might be null if the error occurred before the hanshake completed.
|
||||
if (pad.collabClient != null) {
|
||||
|
@ -313,6 +318,15 @@ const handshake = async () => {
|
|||
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
|
||||
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) {
|
||||
padconnectionstatus.disconnected(obj.disconnect);
|
||||
socket.disconnect();
|
||||
|
@ -713,7 +727,7 @@ const pad = {
|
|||
$.ajax(
|
||||
{
|
||||
type: 'post',
|
||||
url: 'ep/pad/connection-diagnostic-info',
|
||||
url: '../ep/pad/connection-diagnostic-info',
|
||||
data: {
|
||||
diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
'use strict';
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
|
||||
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
|
||||
|
|
|
@ -24,9 +24,10 @@
|
|||
const Cookies = require('./pad_utils').Cookies;
|
||||
const padcookie = require('./pad_cookie').padcookie;
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const Ace2Editor = require('./ace').Ace2Editor;
|
||||
import html10n from '../js/vendors/html10n'
|
||||
|
||||
const padeditor = (() => {
|
||||
let Ace2Editor = undefined;
|
||||
let pad = undefined;
|
||||
let settings = undefined;
|
||||
|
||||
|
@ -35,7 +36,6 @@ const padeditor = (() => {
|
|||
// this is accessed directly from other files
|
||||
viewZoom: 100,
|
||||
init: async (initialViewOptions, _pad) => {
|
||||
Ace2Editor = require('./ace').Ace2Editor;
|
||||
pad = _pad;
|
||||
settings = pad.settings;
|
||||
self.ace = new Ace2Editor();
|
||||
|
@ -99,7 +99,7 @@ const padeditor = (() => {
|
|||
$('#languagemenu').val(html10n.getLanguage());
|
||||
$('#languagemenu').on('change', () => {
|
||||
Cookies.set('language', $('#languagemenu').val());
|
||||
window.html10n.localize([$('#languagemenu').val(), 'en']);
|
||||
html10n.localize([$('#languagemenu').val(), 'en']);
|
||||
if ($('select').niceSelect) {
|
||||
$('select').niceSelect('update');
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
|
||||
const padimpexp = (() => {
|
||||
let pad;
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ let pad;
|
|||
|
||||
exports.saveNow = () => {
|
||||
pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
|
||||
$.gritter.add({
|
||||
window.$.gritter.add({
|
||||
// (string | mandatory) the heading of the notification
|
||||
title: html10n.get('pad.savedrevs.marked'),
|
||||
// (string | mandatory) the text inside the notification
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
let myUserInfo = {};
|
||||
|
||||
let colorPickerOpen = false;
|
||||
|
|
|
@ -356,7 +356,6 @@ const padutils = {
|
|||
let globalExceptionHandler = null;
|
||||
padutils.setupGlobalExceptionHandler = () => {
|
||||
if (globalExceptionHandler == null) {
|
||||
require('./vendors/gritter');
|
||||
globalExceptionHandler = (e) => {
|
||||
let type;
|
||||
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
|
||||
// window object.
|
||||
if (typeof window !== 'undefined') {
|
||||
exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({
|
||||
exports.Cookies = require('js-cookie').withAttributes({
|
||||
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
||||
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
||||
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
||||
|
|
|
@ -7,24 +7,13 @@ exports.baseURL = '';
|
|||
|
||||
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
|
||||
|
||||
exports.update = (cb) => {
|
||||
// It appears that this response (see #620) may interrupt the current thread
|
||||
// of execution on Firefox. This schedules the response in the run-loop,
|
||||
// which appears to fix the issue.
|
||||
const callback = () => setTimeout(cb, 0);
|
||||
|
||||
jQuery.getJSON(
|
||||
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`
|
||||
).done((data) => {
|
||||
defs.plugins = data.plugins;
|
||||
defs.parts = data.parts;
|
||||
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');
|
||||
defs.loaded = true;
|
||||
callback();
|
||||
}).fail((err) => {
|
||||
console.error(`Failed to load plugin-definitions: ${err}`);
|
||||
callback();
|
||||
});
|
||||
exports.update = async (modules) => {
|
||||
const data = await jQuery.getJSON(
|
||||
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`);
|
||||
defs.plugins = data.plugins;
|
||||
defs.parts = data.parts;
|
||||
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks', null, modules);
|
||||
defs.loaded = true;
|
||||
};
|
||||
|
||||
const adoptPluginsFromAncestorsOf = (frame) => {
|
||||
|
|
|
@ -9,7 +9,7 @@ const disabledHookReasons = {
|
|||
},
|
||||
};
|
||||
|
||||
const loadFn = (path, hookName) => {
|
||||
const loadFn = (path, hookName, modules) => {
|
||||
let functionName;
|
||||
const parts = path.split(':');
|
||||
|
||||
|
@ -24,7 +24,13 @@ const loadFn = (path, hookName) => {
|
|||
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;
|
||||
|
||||
for (const name of functionName.split('.')) {
|
||||
|
@ -33,7 +39,7 @@ const loadFn = (path, hookName) => {
|
|||
return fn;
|
||||
};
|
||||
|
||||
const extractHooks = (parts, hookSetName, normalizer) => {
|
||||
const extractHooks = (parts, hookSetName, normalizer, modules) => {
|
||||
const hooks = {};
|
||||
for (const part of parts) {
|
||||
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||
|
@ -53,7 +59,7 @@ const extractHooks = (parts, hookSetName, normalizer) => {
|
|||
}
|
||||
let hookFn;
|
||||
try {
|
||||
hookFn = loadFn(hookFnName, hookName);
|
||||
hookFn = loadFn(hookFnName, hookName, modules);
|
||||
if (!hookFn) throw new Error('Not a function');
|
||||
} catch (err) {
|
||||
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
||||
|
|
|
@ -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
338
src/static/js/scroll.ts
Normal 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
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
import io from 'socket.io-client';
|
||||
|
||||
/**
|
||||
* Creates a socket.io connection.
|
||||
|
|
|
@ -31,7 +31,7 @@ const randomString = require('./pad_utils').randomString;
|
|||
const hooks = require('./pluginfw/hooks');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const socketio = require('./socketio');
|
||||
|
||||
import html10n from '../js/vendors/html10n'
|
||||
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
|
||||
|
||||
const init = () => {
|
||||
|
@ -117,6 +117,14 @@ const handleClientVars = (message) => {
|
|||
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
|
||||
BroadcastSlider = require('./broadcast_slider')
|
||||
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
|
||||
|
|
31
src/static/js/types/RepModel.ts
Normal file
31
src/static/js/types/RepModel.ts
Normal 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
|
||||
}
|
7
src/static/js/vendors/farbtastic.js
vendored
7
src/static/js/vendors/farbtastic.js
vendored
|
@ -7,6 +7,7 @@
|
|||
// Licensed under the terms of the GNU General Public License v2.0:
|
||||
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
|
||||
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
|
||||
|
||||
(function ($) {
|
||||
|
||||
var __debug = false;
|
||||
|
@ -172,7 +173,7 @@ $._farbtastic = function (container, options) {
|
|||
angle2 = d2 * Math.PI * 2,
|
||||
// Endpoints
|
||||
x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
|
||||
x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
|
||||
let x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
|
||||
// Midpoint chosen so that the endpoints are tangent to the circle.
|
||||
am = (angle1 + angle2) / 2,
|
||||
tan = 1 / Math.cos((angle2 - angle1) / 2),
|
||||
|
@ -329,8 +330,8 @@ $._farbtastic = function (container, options) {
|
|||
|
||||
// Update the overlay canvas.
|
||||
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
|
||||
for (i in circles) {
|
||||
var c = circles[i];
|
||||
for (let i in circles) {
|
||||
const c = circles[i];
|
||||
fb.ctxOverlay.lineWidth = c.lw;
|
||||
fb.ctxOverlay.strokeStyle = c.c;
|
||||
fb.ctxOverlay.beginPath();
|
||||
|
|
6
src/static/js/vendors/gritter.js
vendored
6
src/static/js/vendors/gritter.js
vendored
|
@ -42,8 +42,8 @@
|
|||
return Gritter.add(params || {});
|
||||
} catch(e) {
|
||||
|
||||
var err = 'Gritter Error: ' + e;
|
||||
(typeof(console) != 'undefined' && console.error) ?
|
||||
const err = 'Gritter Error: ' + e;
|
||||
(typeof(console) != 'undefined' && console.error) ?
|
||||
console.error(err, params) :
|
||||
alert(err);
|
||||
|
||||
|
@ -289,7 +289,7 @@
|
|||
*/
|
||||
_runSetup: function(){
|
||||
|
||||
for(opt in $.gritter.options){
|
||||
for(let opt in $.gritter.options){
|
||||
this[opt] = $.gritter.options[opt];
|
||||
}
|
||||
this._is_setup = 1;
|
||||
|
|
1056
src/static/js/vendors/html10n.js
vendored
1056
src/static/js/vendors/html10n.js
vendored
File diff suppressed because it is too large
Load diff
993
src/static/js/vendors/html10n.ts
vendored
Normal file
993
src/static/js/vendors/html10n.ts
vendored
Normal 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()
|
8
src/static/js/vendors/nice-select.js
vendored
8
src/static/js/vendors/nice-select.js
vendored
|
@ -110,10 +110,10 @@
|
|||
$dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px');
|
||||
}
|
||||
|
||||
$listHeight = $dropdown.find('.list').outerHeight();
|
||||
$top = $dropdown.parent().offset().top;
|
||||
$bottom = $('body').height() - $top;
|
||||
$maxListHeight = $bottom - $dropdown.outerHeight() - 20;
|
||||
let $listHeight = $dropdown.find('.list').outerHeight();
|
||||
let $top = $dropdown.parent().offset().top;
|
||||
let $bottom = $('body').height() - $top;
|
||||
let $maxListHeight = $bottom - $dropdown.outerHeight() - 20;
|
||||
if ($maxListHeight < 200) {
|
||||
$dropdown.addClass('reverse');
|
||||
$maxListHeight = 250;
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
for the JavaScript code in this page.|
|
||||
*/
|
||||
</script>
|
||||
<script src="../static/js/basic_error_handler.js?v=<%=settings.randomVersionString%>"></script>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
@ -53,8 +52,6 @@
|
|||
<% e.end_block(); %>
|
||||
|
||||
<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>
|
||||
<body>
|
||||
<% e.begin_block("body"); %>
|
||||
|
@ -442,67 +439,11 @@
|
|||
|
||||
<% e.begin_block("scripts"); %>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<script src="<%=entrypoint%>"></script>
|
||||
|
||||
<% e.begin_block("customScripts"); %>
|
||||
<script type="text/javascript" src="../static/skins/<%=encodeURI(settings.skinName)%>/pad.js?v=<%=settings.randomVersionString%>"></script>
|
||||
<% 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>
|
||||
<% e.end_block(); %>
|
||||
</body>
|
||||
|
|
45
src/templates/padBootstrap.js
Normal file
45
src/templates/padBootstrap.js
Normal 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');
|
||||
})();
|
41
src/templates/padViteBootstrap.js
Normal file
41
src/templates/padViteBootstrap.js
Normal 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');
|
||||
})();
|
37
src/templates/timeSliderBootstrap.js
Normal file
37
src/templates/timeSliderBootstrap.js
Normal 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()
|
||||
})();
|
|
@ -47,8 +47,6 @@
|
|||
|
||||
<link rel="localizations" type="application/l10n+json" href="../../locales.json" />
|
||||
<% 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(); %>
|
||||
</head>
|
||||
|
||||
|
@ -250,58 +248,14 @@
|
|||
<!-------- 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<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)%>,
|
||||
};
|
||||
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>
|
||||
<script src="<%=entrypoint%>"></script>
|
||||
<% e.end_block(); %>
|
||||
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
||||
</html>
|
||||
|
|
|
@ -24,7 +24,7 @@ s!"points":[^,]*!"points": 1000!
|
|||
' settings.json.template >settings.json
|
||||
|
||||
log "Assuming src/bin/installDeps.sh has already been run"
|
||||
(cd src && npm run dev &
|
||||
(cd src && pnpm run prod &
|
||||
ep_pid=$!)
|
||||
|
||||
log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"build-copy": "tsc && vite build --outDir ../src/static/oidc --emptyOutDir"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ep_etherpad-lite": "workspace:../src",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4"
|
||||
}
|
||||
|
|
686
ui/pad.html
Normal file
686
ui/pad.html
Normal 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;">- </a>
|
||||
<a id="titlesticky" class="stick-to-screen-btn" onClick="chat.stickToScreen(true);return false;" data-l10n-id="pad.chat.stick.title">█ </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>
|
|
@ -1,10 +1,18 @@
|
|||
// vite.config.js
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import vitePluginRequire from 'vite-plugin-require';
|
||||
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
|
||||
|
||||
export default defineConfig({
|
||||
base: '/views/',
|
||||
base: '/views/',
|
||||
plugins: [
|
||||
viteCommonjs(),
|
||||
],
|
||||
build: {
|
||||
commonjsOptions:{
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
outDir: resolve(__dirname, '../src/static/oidc'),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
|
@ -14,4 +22,31 @@ export default defineConfig({
|
|||
},
|
||||
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
2
var/js/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.js
|
||||
*.map
|
Loading…
Reference in a new issue