mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Merge branch 'develop'
This commit is contained in:
commit
c0a30967a7
91 changed files with 13514 additions and 12588 deletions
|
@ -25,3 +25,5 @@ Dockerfile
|
||||||
settings.json
|
settings.json
|
||||||
src/node_modules
|
src/node_modules
|
||||||
admin/node_modules
|
admin/node_modules
|
||||||
|
ui/node_modules
|
||||||
|
node_modules
|
||||||
|
|
5
.github/workflows/frontend-admin-tests.yml
vendored
5
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -68,7 +68,7 @@ jobs:
|
||||||
# rules.
|
# rules.
|
||||||
-
|
-
|
||||||
name: Install all dependencies and symlink for ep_etherpad-lite
|
name: Install all dependencies and symlink for ep_etherpad-lite
|
||||||
run: bin/installDeps.sh
|
run: pnpm i
|
||||||
#-
|
#-
|
||||||
# name: Install etherpad plugins
|
# name: Install etherpad plugins
|
||||||
# run: rm -Rf node_modules/ep_align/static/tests/*
|
# run: rm -Rf node_modules/ep_align/static/tests/*
|
||||||
|
@ -92,7 +92,6 @@ jobs:
|
||||||
- name: Build admin frontend
|
- name: Build admin frontend
|
||||||
working-directory: admin
|
working-directory: admin
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
|
||||||
pnpm run build
|
pnpm run build
|
||||||
# name: Run the frontend admin tests
|
# name: Run the frontend admin tests
|
||||||
# shell: bash
|
# shell: bash
|
||||||
|
@ -124,7 +123,7 @@ jobs:
|
||||||
- name: Run the frontend admin tests
|
- name: Run the frontend admin tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm run dev &
|
pnpm run prod &
|
||||||
connected=false
|
connected=false
|
||||||
can_connect() {
|
can_connect() {
|
||||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||||
|
|
6
.github/workflows/frontend-tests.yml
vendored
6
.github/workflows/frontend-tests.yml
vendored
|
@ -59,7 +59,7 @@ jobs:
|
||||||
- name: Run the frontend tests
|
- name: Run the frontend tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm run dev &
|
pnpm run prod &
|
||||||
connected=false
|
connected=false
|
||||||
can_connect() {
|
can_connect() {
|
||||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||||
|
@ -122,7 +122,7 @@ jobs:
|
||||||
- name: Run the frontend tests
|
- name: Run the frontend tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm run dev &
|
pnpm run prod &
|
||||||
connected=false
|
connected=false
|
||||||
can_connect() {
|
can_connect() {
|
||||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||||
|
@ -192,7 +192,7 @@ jobs:
|
||||||
- name: Run the frontend tests
|
- name: Run the frontend tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm run dev &
|
pnpm run prod &
|
||||||
connected=false
|
connected=false
|
||||||
can_connect() {
|
can_connect() {
|
||||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||||
|
|
109
.github/workflows/windows.yml
vendored
109
.github/workflows/windows.yml
vendored
|
@ -61,111 +61,30 @@ jobs:
|
||||||
name: Run the backend tests
|
name: Run the backend tests
|
||||||
shell: msys2 {0}
|
shell: msys2 {0}
|
||||||
run: cd src && pnpm test
|
run: cd src && pnpm test
|
||||||
-
|
|
||||||
name: Build the .zip
|
|
||||||
shell: msys2 {0}
|
|
||||||
run: bin/buildForWindows.sh
|
|
||||||
-
|
|
||||||
name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: etherpad-win.zip
|
|
||||||
path: etherpad-win.zip
|
|
||||||
|
|
||||||
build-exe:
|
|
||||||
if: |
|
|
||||||
(github.event_name != 'pull_request')
|
|
||||||
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
|
|
||||||
name: Build .exe
|
|
||||||
needs: build-zip
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
-
|
|
||||||
name: Download .zip
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: etherpad-win.zip
|
|
||||||
path: ..
|
|
||||||
-
|
|
||||||
name: Extract .zip
|
|
||||||
working-directory: ..
|
|
||||||
run: 7z x etherpad-win.zip -oetherpad-zip
|
|
||||||
-
|
|
||||||
name: Create installer
|
|
||||||
uses: joncloud/makensis-action@v4.1
|
|
||||||
with:
|
|
||||||
script-file: 'bin/nsis/etherpad.nsi'
|
|
||||||
-
|
|
||||||
name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: etherpad-win.exe
|
|
||||||
path: etherpad-win.exe
|
|
||||||
|
|
||||||
deploy-zip:
|
|
||||||
# run on pushes to any branch
|
|
||||||
# run on PRs from external forks
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
if: |
|
|
||||||
(github.event_name != 'pull_request')
|
|
||||||
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
|
|
||||||
name: Deploy
|
|
||||||
needs: build-zip
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Download zip
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: etherpad-win.zip
|
|
||||||
-
|
|
||||||
name: Extract Etherpad
|
|
||||||
run: 7z x etherpad-win.zip -oetherpad
|
|
||||||
-
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
name: Install pnpm
|
|
||||||
with:
|
|
||||||
version: 9.0.4
|
|
||||||
run_install: false
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
name: Setup pnpm cache
|
|
||||||
with:
|
|
||||||
path: ${{ env.STORE_PATH }}
|
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
- name: Only install direct dependencies
|
|
||||||
run: pnpm config set auto-install-peers false
|
|
||||||
- name: Install all dependencies and symlink for ep_etherpad-lite
|
|
||||||
run: .\bin\installOnWindows.bat
|
|
||||||
working-directory: etherpad
|
|
||||||
-
|
-
|
||||||
name: Run Etherpad
|
name: Run Etherpad
|
||||||
working-directory: etherpad/src
|
working-directory: etherpad/src
|
||||||
run: |
|
run: |
|
||||||
pnpm install cypress
|
pnpm i
|
||||||
.\node_modules\.bin\cypress.cmd install --force
|
pnpm exec playwright install --with-deps
|
||||||
pnpm run prod &
|
pnpm run prod &
|
||||||
curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test
|
curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test
|
||||||
pnpm exec cypress run --config-file ./tests/frontend/cypress/cypress.config.js
|
pnpm exec playwright install chromium --with-deps
|
||||||
# On release, upload windows zip to GitHub release tab
|
pnpm run test-ui --project=chromium
|
||||||
|
# On release, create release
|
||||||
-
|
-
|
||||||
name: Rename to etherpad-lite-win.zip
|
name: Rename to etherpad-lite-win.zip
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: mv etherpad-win.zip etherpad-lite-win.zip
|
run: mv etherpad-win.zip etherpad-lite-win.zip
|
||||||
- name: upload binaries to release
|
- name: Generate Changelog
|
||||||
|
if: ${{startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
working-directory: bin
|
||||||
|
run: pnpm run generateChangelog ${{ github.ref }} > ${{ github.workspace }}-CHANGELOG.txt
|
||||||
|
with:
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
if: ${{startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{startsWith(github.ref, 'refs/tags/v') }}
|
||||||
with:
|
with:
|
||||||
files: etherpad-lite-win.zip
|
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
||||||
|
make_latest: true
|
||||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,3 +1,13 @@
|
||||||
|
# 2.2.0
|
||||||
|
|
||||||
|
### Notable enhancements and fixes
|
||||||
|
|
||||||
|
- Removal of Etherpad require kernel: We finally managed to include esbuild to bundle our frontend code together. So no matter how many plugins your server has it is always one JavaScript file. This boosts performance dramatically.
|
||||||
|
- Added log layoutType: This lets you print the log in either colored or basic (black and white text)
|
||||||
|
- Introduced esbuild for bundling CSS files
|
||||||
|
- Cache all files to be bundled in memory for faster load speed
|
||||||
|
|
||||||
|
|
||||||
# 2.1.1
|
# 2.1.1
|
||||||
|
|
||||||
|
|
||||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -5,11 +5,11 @@
|
||||||
# Author: muxator
|
# Author: muxator
|
||||||
|
|
||||||
FROM node:alpine AS adminbuild
|
FROM node:alpine AS adminbuild
|
||||||
|
RUN npm install -g pnpm@9.0.4
|
||||||
WORKDIR /opt/etherpad-lite
|
WORKDIR /opt/etherpad-lite
|
||||||
COPY ./ ./
|
COPY . .
|
||||||
RUN cd ./admin && npm install -g pnpm@9.0.4 && pnpm install && pnpm run build --outDir ./dist
|
RUN pnpm install
|
||||||
RUN cd ./ui && pnpm install && pnpm run build --outDir ./dist
|
RUN pnpm run build:ui
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine AS build
|
FROM node:alpine AS build
|
||||||
|
@ -115,8 +115,8 @@ COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./
|
||||||
FROM build AS development
|
FROM build AS development
|
||||||
|
|
||||||
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/
|
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/
|
||||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/admin/dist ./src/templates/admin
|
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/ templates/admin./src/templates/admin
|
||||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/ui/dist ./src/static/oidc
|
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||||
|
|
||||||
RUN bin/installDeps.sh && \
|
RUN bin/installDeps.sh && \
|
||||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||||
|
@ -130,8 +130,8 @@ ENV NODE_ENV=production
|
||||||
ENV ETHERPAD_PRODUCTION=true
|
ENV ETHERPAD_PRODUCTION=true
|
||||||
|
|
||||||
COPY --chown=etherpad:etherpad ./src ./src
|
COPY --chown=etherpad:etherpad ./src ./src
|
||||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/admin/dist ./src/templates/admin
|
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/templates/admin ./src/templates/admin
|
||||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/ui/dist ./src/static/oidc
|
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||||
|
|
||||||
RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
|
RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
|
||||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||||
|
|
73
README.md
73
README.md
|
@ -98,76 +98,19 @@ volumes:
|
||||||
etherpad-var:
|
etherpad-var:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
[Node.js](https://nodejs.org/) >= **18.18.2**.
|
[Node.js](https://nodejs.org/) >= **18.18.2**.
|
||||||
|
|
||||||
### GNU/Linux and other UNIX-like systems
|
### Windows, macOS, Linux
|
||||||
|
|
||||||
#### Quick install on Debian/Ubuntu
|
1. Download the latest Node.js runtime from [nodejs.org](https://nodejs.org/).
|
||||||
|
2. Install pnpm: `npm install -g pnpm` (Administrator privileges may be required).
|
||||||
Install the latest Node.js LTS per [official install instructions](https://github.com/nodesource/distributions#installation-instructions), then:
|
3. Clone the repository: `git clone -b master`
|
||||||
```sh
|
4. Run `pnpm i`
|
||||||
git clone --branch master https://github.com/ether/etherpad-lite.git &&
|
5. Run `pnpm run build:etherpad`
|
||||||
cd etherpad-lite &&
|
6. Run `pnpm run prod`
|
||||||
bin/run.sh
|
7. Visit `http://localhost:9001` in your browser.
|
||||||
```
|
|
||||||
|
|
||||||
#### Manual install
|
|
||||||
|
|
||||||
You'll need Git and [Node.js](https://nodejs.org/) installed.
|
|
||||||
|
|
||||||
**As any user (we recommend creating a separate user called etherpad):**
|
|
||||||
|
|
||||||
1. Move to a folder where you want to install Etherpad.
|
|
||||||
2. Clone the Git repository: `git clone --branch master
|
|
||||||
https://github.com/ether/etherpad-lite.git`
|
|
||||||
3. Change into the new directory containing the cloned source code: `cd
|
|
||||||
etherpad-lite`
|
|
||||||
4. Run `bin/run.sh` and open http://127.0.0.1:9001 in your browser.
|
|
||||||
|
|
||||||
To update to the latest released version, execute `git pull origin`. The next
|
|
||||||
start with `bin/run.sh` will update the dependencies.
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
#### Prebuilt Windows package
|
|
||||||
|
|
||||||
This package runs on any Windows machine. You can perform a manual installation
|
|
||||||
via git for development purposes, but as this uses symlinks which performs
|
|
||||||
unreliably on Windows, please stick to the prebuilt package if possible.
|
|
||||||
|
|
||||||
1. [Download the latest Windows package](https://etherpad.org/#download)
|
|
||||||
2. Extract the folder
|
|
||||||
|
|
||||||
Run `start.bat` and open <http://localhost:9001> in your browser.
|
|
||||||
|
|
||||||
#### Manually install on Windows
|
|
||||||
|
|
||||||
You'll need [Node.js](https://nodejs.org) and (optionally, though recommended)
|
|
||||||
git.
|
|
||||||
|
|
||||||
1. Grab the source, either:
|
|
||||||
* download <https://github.com/ether/etherpad-lite/zipball/master>
|
|
||||||
* or `git clone --branch master
|
|
||||||
https://github.com/ether/etherpad-lite.git`
|
|
||||||
2. With a "Run as administrator" command prompt execute
|
|
||||||
`bin\installOnWindows.bat`
|
|
||||||
|
|
||||||
Now, run `start.bat` and open http://localhost:9001 in your browser.
|
|
||||||
|
|
||||||
Update to the latest version with `git pull origin`, then run
|
|
||||||
`bin\installOnWindows.bat`, again.
|
|
||||||
|
|
||||||
If cloning to a subdirectory within another project, you may need to do the
|
|
||||||
following:
|
|
||||||
|
|
||||||
1. Start the server manually (e.g. `node src/node/server.ts`)
|
|
||||||
2. Edit the db `filename` in `settings.json` to the relative directory with
|
|
||||||
the file (e.g. `application/lib/etherpad-lite/var/dirty.db`)
|
|
||||||
3. Add auto-generated files to the main project `.gitignore`
|
|
||||||
|
|
||||||
### Docker container
|
### Docker container
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.1.1",
|
"version": "2.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -18,23 +18,23 @@
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@types/react": "^18.3.2",
|
"@types/react": "^18.3.2",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^7.15.0",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^9.6.0",
|
"eslint": "^9.8.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.7",
|
"eslint-plugin-react-refresh": "^0.4.9",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.12.2",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.426.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.2",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^15.0.1",
|
||||||
"react-router-dom": "^6.24.1",
|
"react-router-dom": "^6.26.0",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.3.3",
|
"vite": "^5.4.0",
|
||||||
"vite-plugin-static-copy": "^1.0.6",
|
"vite-plugin-static-copy": "^1.0.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zustand": "^4.5.4"
|
"zustand": "^4.5.4"
|
||||||
|
|
|
@ -112,19 +112,19 @@ export const PadPage = ()=>{
|
||||||
ascending: !searchParams.ascending
|
ascending: !searchParams.ascending
|
||||||
})
|
})
|
||||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></th>
|
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></th>
|
||||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'lastEdited')} onClick={()=>{
|
|
||||||
setSearchParams({
|
|
||||||
...searchParams,
|
|
||||||
sortBy: 'lastEdited',
|
|
||||||
ascending: !searchParams.ascending
|
|
||||||
})
|
|
||||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_pad-user-count"/></th>
|
|
||||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'userCount')} onClick={()=>{
|
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'userCount')} onClick={()=>{
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
...searchParams,
|
...searchParams,
|
||||||
sortBy: 'userCount',
|
sortBy: 'userCount',
|
||||||
ascending: !searchParams.ascending
|
ascending: !searchParams.ascending
|
||||||
})
|
})
|
||||||
|
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_pad-user-count"/></th>
|
||||||
|
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'lastEdited')} onClick={()=>{
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
sortBy: 'lastEdited',
|
||||||
|
ascending: !searchParams.ascending
|
||||||
|
})
|
||||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_last-edited"/></th>
|
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_last-edited"/></th>
|
||||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'revisionNumber')} onClick={()=>{
|
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'revisionNumber')} onClick={()=>{
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
|
|
|
@ -50,21 +50,13 @@ rm -rf src/node_modules || true
|
||||||
#$(try cd ./bin/installDeps.sh)
|
#$(try cd ./bin/installDeps.sh)
|
||||||
|
|
||||||
# Install admin frontend
|
# Install admin frontend
|
||||||
cd admin
|
|
||||||
try pnpm install
|
try pnpm install
|
||||||
try pnpm run build
|
try pnpm run build:etherpad
|
||||||
cd ..
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Nuke the admin folder as it is not needed anymore :D
|
# Nuke the admin folder as it is not needed anymore :D
|
||||||
rm -rf admin
|
rm -rf admin
|
||||||
|
rm -rf oidc
|
||||||
export NODE_ENV=production
|
rm -rf src/node_modules
|
||||||
try pnpm install --production
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
log "copy the windows settings template..."
|
log "copy the windows settings template..."
|
||||||
try cp settings.json.template settings.json
|
try cp settings.json.template settings.json
|
||||||
|
|
|
@ -25,4 +25,5 @@ if (process.argv.length !== 2) throw new Error('Use: node bin/checkAllPads.js');
|
||||||
console.log(`Pad ${padId}: OK`);
|
console.log(`Pad ${padId}: OK`);
|
||||||
}));
|
}));
|
||||||
console.log('Finished.');
|
console.log('Finished.');
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -11,12 +11,19 @@ process.on('unhandledRejection', (err) => { throw err; });
|
||||||
if (process.argv.length !== 3) throw new Error('Use: node bin/checkPad.js $PADID');
|
if (process.argv.length !== 3) throw new Error('Use: node bin/checkPad.js $PADID');
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const padId = process.argv[2];
|
const padId = process.argv[2];
|
||||||
(async () => {
|
|
||||||
|
const performCheck = async () => {
|
||||||
const db = require('ep_etherpad-lite/node/db/DB');
|
const db = require('ep_etherpad-lite/node/db/DB');
|
||||||
await db.init();
|
await db.init();
|
||||||
|
console.log("Checking if " + padId + " exists")
|
||||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||||
if (!await padManager.doesPadExists(padId)) throw new Error('Pad does not exist');
|
if (!await padManager.doesPadExists(padId)) throw new Error('Pad does not exist');
|
||||||
const pad = await padManager.getPad(padId);
|
const pad = await padManager.getPad(padId);
|
||||||
await pad.check();
|
await pad.check();
|
||||||
console.log('Finished.');
|
console.log('Finished checking pad.');
|
||||||
})();
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
performCheck()
|
||||||
|
.then(e=>console.log("Finished"))
|
||||||
|
.catch(e=>console.log("Finished with errors"))
|
||||||
|
|
|
@ -14,6 +14,7 @@ import path from "node:path";
|
||||||
import querystring from "node:querystring";
|
import querystring from "node:querystring";
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (err) => { throw err; });
|
process.on('unhandledRejection', (err) => { throw err; });
|
||||||
|
@ -53,4 +54,5 @@ const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||||
if (res.data.code === 1) throw new Error(`Error creating session: ${JSON.stringify(res.data)}`);
|
if (res.data.code === 1) throw new Error(`Error creating session: ${JSON.stringify(res.data)}`);
|
||||||
console.log('Session made: ====> create a cookie named sessionID and set the value to',
|
console.log('Session made: ====> create a cookie named sessionID and set the value to',
|
||||||
res.data.data.sessionID);
|
res.data.data.sessionID);
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -49,4 +49,5 @@ const settings = require('ep_etherpad-lite/tests/container/loadSettings').loadSe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`Deleted ${deleteCount} sessions`);
|
console.log(`Deleted ${deleteCount} sessions`);
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -38,4 +38,5 @@ const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
|
||||||
const deleteAttempt = await axios.post(uri);
|
const deleteAttempt = await axios.post(uri);
|
||||||
if (deleteAttempt.data.code === 1) throw new Error(`Error deleting pad ${deleteAttempt.data}`);
|
if (deleteAttempt.data.code === 1) throw new Error(`Error deleting pad ${deleteAttempt.data}`);
|
||||||
console.log('Deleted pad', deleteAttempt.data);
|
console.log('Deleted pad', deleteAttempt.data);
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -60,4 +60,5 @@ const padId = process.argv[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('finished');
|
console.log('finished');
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
41
bin/generateReleaseNotes.ts
Normal file
41
bin/generateReleaseNotes.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import {readFileSync} from "node:fs";
|
||||||
|
|
||||||
|
const changelog = readFileSync('../changelog.md')
|
||||||
|
const changelogText = changelog.toString()
|
||||||
|
const changelogLines = changelogText.split('\n')
|
||||||
|
|
||||||
|
|
||||||
|
let cliArgs = process.argv.slice(2)
|
||||||
|
|
||||||
|
let tagVar = cliArgs[0]
|
||||||
|
|
||||||
|
if (!tagVar) {
|
||||||
|
console.error("No tag provided")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Tag",tagVar)
|
||||||
|
|
||||||
|
tagVar = tagVar.replace("refs/tags/v", "")
|
||||||
|
|
||||||
|
let startNum = -1
|
||||||
|
let endline = 0
|
||||||
|
|
||||||
|
let counter = 0
|
||||||
|
for (const line of changelogLines) {
|
||||||
|
if (line.trim().startsWith("#") && (line.match(new RegExp("#", "g"))||[]).length === 1) {
|
||||||
|
if (startNum !== -1) {
|
||||||
|
endline = counter-1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedLine = line.replace("#","").trim()
|
||||||
|
if(sanitizedLine.includes(tagVar)) {
|
||||||
|
startNum = counter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentReleaseNotes = changelogLines.slice(startNum, endline).join('\n')
|
||||||
|
console.log("Generated changelog",currentReleaseNotes)
|
|
@ -6,7 +6,8 @@ import util from "node:util";
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import ueberDB from "ueberdb2";
|
import {Database} from "ueberdb2";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||||
process.on('unhandledRejection', (err) => { throw err; });
|
process.on('unhandledRejection', (err) => { throw err; });
|
||||||
|
@ -56,7 +57,7 @@ const unescape = (val: string) => {
|
||||||
writeInterval: 100,
|
writeInterval: 100,
|
||||||
json: false, // data is already json encoded
|
json: false, // data is already json encoded
|
||||||
};
|
};
|
||||||
const db = new ueberDB.Database( // eslint-disable-line new-cap
|
const db = new Database( // eslint-disable-line new-cap
|
||||||
settings.dbType,
|
settings.dbType,
|
||||||
settings.dbSettings,
|
settings.dbSettings,
|
||||||
dbWrapperSettings,
|
dbWrapperSettings,
|
||||||
|
@ -96,6 +97,8 @@ const unescape = (val: string) => {
|
||||||
'depended on dbms this may take some time..\n');
|
'depended on dbms this may take some time..\n');
|
||||||
|
|
||||||
const closeDB = util.promisify(db.close.bind(db));
|
const closeDB = util.promisify(db.close.bind(db));
|
||||||
|
// @ts-ignore
|
||||||
await closeDB(null);
|
await closeDB(null);
|
||||||
log(`finished, imported ${keyNo} keys.`);
|
log(`finished, imported ${keyNo} keys.`);
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import ueberDB from "ueberdb2";
|
import {Database} from "ueberdb2";
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||||
|
@ -23,7 +23,7 @@ process.on('unhandledRejection', (err) => { throw err; });
|
||||||
cache: '0', // The cache slows things down when you're mostly writing.
|
cache: '0', // The cache slows things down when you're mostly writing.
|
||||||
writeInterval: 0, // Write directly to the database, don't buffer
|
writeInterval: 0, // Write directly to the database, don't buffer
|
||||||
};
|
};
|
||||||
const db = new ueberDB.Database( // eslint-disable-line new-cap
|
const db = new Database( // eslint-disable-line new-cap
|
||||||
settings.dbType,
|
settings.dbType,
|
||||||
settings.dbSettings,
|
settings.dbSettings,
|
||||||
dbWrapperSettings,
|
dbWrapperSettings,
|
||||||
|
@ -31,7 +31,7 @@ process.on('unhandledRejection', (err) => { throw err; });
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
console.log('Waiting for dirtyDB to parse its file.');
|
console.log('Waiting for dirtyDB to parse its file.');
|
||||||
const dirty = await new ueberDB.Database('dirty',`${__dirname}/../var/dirty.db`);
|
const dirty = new Database('dirty', `${__dirname}/../var/dirty.db`);
|
||||||
await dirty.init();
|
await dirty.init();
|
||||||
const keys = await dirty.findKeys('*', '')
|
const keys = await dirty.findKeys('*', '')
|
||||||
|
|
||||||
|
@ -57,4 +57,5 @@ process.on('unhandledRejection', (err) => { throw err; });
|
||||||
await db.close(null);
|
await db.close(null);
|
||||||
await dirty.close(null);
|
await dirty.close(null);
|
||||||
console.log('Finished.');
|
console.log('Finished.');
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
{
|
{
|
||||||
"name": "bin",
|
"name": "bin",
|
||||||
"version": "2.1.1",
|
"version": "2.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "checkAllPads.js",
|
"main": "checkAllPads.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
"doc": "doc"
|
"doc": "doc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.3",
|
||||||
"ep_etherpad-lite": "workspace:../src",
|
"ep_etherpad-lite": "workspace:../src",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.3",
|
||||||
"tsx": "^4.16.2",
|
"tsx": "^4.17.0",
|
||||||
"ueberdb2": "^4.2.81"
|
"ueberdb2": "^4.2.92"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^22.1.0",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"checkPad": "node --import tsx checkPad.ts",
|
"checkPad": "node --import tsx checkPad.ts",
|
||||||
|
@ -32,7 +32,8 @@
|
||||||
"rebuildPad": "node --import tsx rebuildPad.ts",
|
"rebuildPad": "node --import tsx rebuildPad.ts",
|
||||||
"stalePlugins": "node --import tsx ./plugins/stalePlugins.ts",
|
"stalePlugins": "node --import tsx ./plugins/stalePlugins.ts",
|
||||||
"checkPlugin": "node --import tsx ./plugins/checkPlugin.ts",
|
"checkPlugin": "node --import tsx ./plugins/checkPlugin.ts",
|
||||||
"plugins": "node --import tsx ./plugins.ts"
|
"plugins": "node --import tsx ./plugins.ts",
|
||||||
|
"generateChangelog": "node --import tsx generateReleaseNotes.ts"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
|
|
@ -474,6 +474,6 @@ log4js.configure({
|
||||||
logger.info('No changes.');
|
logger.info('No changes.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Finished');
|
logger.info('Finished');
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -78,7 +78,7 @@ jobs:
|
||||||
- name: Run the frontend tests
|
- name: Run the frontend tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm run dev &
|
pnpm run prod &
|
||||||
connected=false
|
connected=false
|
||||||
can_connect() {
|
can_connect() {
|
||||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
// Returns a list of stale plugins and their authors email
|
// Returns a list of stale plugins and their authors email
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import process from "node:process";
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -19,4 +20,5 @@ const currentTime = new Date();
|
||||||
console.log(`${name}, ${res.data[plugin].data.maintainers[0].email}`);
|
console.log(`${name}, ${res.data[plugin].data.maintainers[0].email}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
|
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
|
||||||
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
process.on('unhandledRejection', (err) => { throw err; });
|
process.on('unhandledRejection', (err) => { throw err; });
|
||||||
|
|
||||||
if (process.argv.length !== 4 && process.argv.length !== 5) {
|
if (process.argv.length !== 4 && process.argv.length !== 5) {
|
||||||
|
@ -82,4 +84,5 @@ const newPadId = process.argv[4] || `${padId}-rebuilt`;
|
||||||
|
|
||||||
await db.shutdown();
|
await db.shutdown();
|
||||||
console.info('finished');
|
console.info('finished');
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -57,4 +57,5 @@ let valueCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info(`Finished: Replaced ${valueCount} values in the database`);
|
console.info(`Finished: Replaced ${valueCount} values in the database`);
|
||||||
|
process.exit(0)
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -45,4 +45,4 @@ fi
|
||||||
log "Starting Etherpad..."
|
log "Starting Etherpad..."
|
||||||
|
|
||||||
# cd src
|
# cd src
|
||||||
exec pnpm run dev "$@"
|
exec pnpm run prod "$@"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitepress": "^1.2.2"
|
"vitepress": "^1.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs:dev": "vitepress dev",
|
"docs:dev": "vitepress dev",
|
||||||
|
|
|
@ -28,7 +28,9 @@
|
||||||
"plugins": "pnpm --filter bin run plugins",
|
"plugins": "pnpm --filter bin run plugins",
|
||||||
"install-plugins": "pnpm --filter bin run plugins i",
|
"install-plugins": "pnpm --filter bin run plugins i",
|
||||||
"remove-plugins": "pnpm --filter bin run remove-plugins",
|
"remove-plugins": "pnpm --filter bin run remove-plugins",
|
||||||
"list-plugins": "pnpm --filter bin run list-plugins"
|
"list-plugins": "pnpm --filter bin run list-plugins",
|
||||||
|
"build:etherpad": "pnpm --filter admin run build-copy && pnpm --filter ui run build-copy",
|
||||||
|
"build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ep_etherpad-lite": "workspace:./src"
|
"ep_etherpad-lite": "workspace:./src"
|
||||||
|
@ -47,6 +49,6 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ether/etherpad-lite.git"
|
"url": "https://github.com/ether/etherpad-lite.git"
|
||||||
},
|
},
|
||||||
"version": "2.1.1",
|
"version": "2.2.0",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
1600
pnpm-lock.yaml
1600
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -648,6 +648,13 @@
|
||||||
*/
|
*/
|
||||||
"loglevel": "INFO",
|
"loglevel": "INFO",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The log layout type to use.
|
||||||
|
*
|
||||||
|
* Valid values: basic, colored
|
||||||
|
*/
|
||||||
|
"logLayoutType": "colored",
|
||||||
|
|
||||||
/* Override any strings found in locale directories */
|
/* Override any strings found in locale directories */
|
||||||
"customLocaleStrings": {},
|
"customLocaleStrings": {},
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,8 @@
|
||||||
"name": "specialpages",
|
"name": "specialpages",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
|
||||||
"expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages",
|
||||||
|
"socketio": "ep_etherpad-lite/node/hooks/express/specialpages"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
|
"Atriwidada",
|
||||||
"Bennylin",
|
"Bennylin",
|
||||||
"IvanLanin",
|
"IvanLanin",
|
||||||
"Marwan Mohamad",
|
"Marwan Mohamad",
|
||||||
|
@ -10,13 +11,38 @@
|
||||||
"admin.page-title": "Dasbor Pengurus - Etherpad",
|
"admin.page-title": "Dasbor Pengurus - Etherpad",
|
||||||
"admin_plugins": "Manajer plugin",
|
"admin_plugins": "Manajer plugin",
|
||||||
"admin_plugins.available": "Plugin yang tersedia",
|
"admin_plugins.available": "Plugin yang tersedia",
|
||||||
|
"admin_plugins.available_not-found": "Tidak ada plugin yang ditemukan.",
|
||||||
|
"admin_plugins.available_fetching": "Mengambil…",
|
||||||
"admin_plugins.available_install.value": "Instal",
|
"admin_plugins.available_install.value": "Instal",
|
||||||
|
"admin_plugins.available_search.placeholder": "Cari plugin yang akan dipasang",
|
||||||
|
"admin_plugins.description": "Deskripsi",
|
||||||
|
"admin_plugins.installed": "Plugin terpasang",
|
||||||
|
"admin_plugins.installed_fetching": "Mengambil plugin yang terpasang…",
|
||||||
|
"admin_plugins.installed_nothing": "Anda belum memasang plugin apa pun.",
|
||||||
|
"admin_plugins.last-update": "Pembaruan terakhir",
|
||||||
|
"admin_plugins.name": "Nama",
|
||||||
|
"admin_plugins.page-title": "Manajer plugin - Etherpad",
|
||||||
"admin_plugins.version": "Versi",
|
"admin_plugins.version": "Versi",
|
||||||
|
"admin_plugins_info": "Informasi penelusuran masalah",
|
||||||
|
"admin_plugins_info.hooks": "Kait terpasang",
|
||||||
|
"admin_plugins_info.hooks_client": "Kait sisi klien",
|
||||||
|
"admin_plugins_info.hooks_server": "Kait sisi server",
|
||||||
|
"admin_plugins_info.parts": "Bagian terpasang",
|
||||||
|
"admin_plugins_info.plugins": "Plugin terpasang",
|
||||||
|
"admin_plugins_info.page-title": "Informasi plugin - Etherpad",
|
||||||
|
"admin_plugins_info.version": "Versi Etherpad",
|
||||||
|
"admin_plugins_info.version_latest": "Versi terakhir yang tersedia",
|
||||||
|
"admin_plugins_info.version_number": "Nomor versi",
|
||||||
"admin_settings": "Pengaturan",
|
"admin_settings": "Pengaturan",
|
||||||
|
"admin_settings.current": "Konfigurasi kini",
|
||||||
|
"admin_settings.current_example-devel": "Contoh templat pengaturan pengembangan",
|
||||||
|
"admin_settings.current_example-prod": "Contoh templat pengaturan produksi",
|
||||||
|
"admin_settings.current_restart.value": "Jalankan ulang Etherpad",
|
||||||
"admin_settings.current_save.value": "Simpan pengaturan",
|
"admin_settings.current_save.value": "Simpan pengaturan",
|
||||||
"admin_settings.page-title": "Pengaturan - Etherpad",
|
"admin_settings.page-title": "Pengaturan - Etherpad",
|
||||||
"index.newPad": "Pad baru",
|
"index.newPad": "Pad baru",
|
||||||
"index.createOpenPad": "atau buat/buka Pad dengan nama:",
|
"index.createOpenPad": "atau buat/buka Pad dengan nama:",
|
||||||
|
"index.openPad": "buka Pad yang ada dengan nama:",
|
||||||
"pad.toolbar.bold.title": "Tebal (Ctrl-B)",
|
"pad.toolbar.bold.title": "Tebal (Ctrl-B)",
|
||||||
"pad.toolbar.italic.title": "Miring (Ctrl-I)",
|
"pad.toolbar.italic.title": "Miring (Ctrl-I)",
|
||||||
"pad.toolbar.underline.title": "Garis bawah (Ctrl-U)",
|
"pad.toolbar.underline.title": "Garis bawah (Ctrl-U)",
|
||||||
|
@ -37,7 +63,7 @@
|
||||||
"pad.colorpicker.save": "Simpan",
|
"pad.colorpicker.save": "Simpan",
|
||||||
"pad.colorpicker.cancel": "Batalkan",
|
"pad.colorpicker.cancel": "Batalkan",
|
||||||
"pad.loading": "Memuat...",
|
"pad.loading": "Memuat...",
|
||||||
"pad.noCookie": "Kuki tidak dapat ditemukan. Izinkan kuki di peramban Anda!",
|
"pad.noCookie": "Kuki tidak dapat ditemukan. Izinkan kuki di peramban Anda! Sesi dan pengaturan Anda tidak akan disimpan antar kunjungan. Ini mungkin karena Etherpad disertakan dalam suatu iFrame dalam beberapa Peramban. Harap pastikan Etherpad ada pada sub domain/domain dengan iFrame induk",
|
||||||
"pad.permissionDenied": "Anda tidak memiliki izin untuk mengakses pad ini",
|
"pad.permissionDenied": "Anda tidak memiliki izin untuk mengakses pad ini",
|
||||||
"pad.settings.padSettings": "Pengaturan Pad",
|
"pad.settings.padSettings": "Pengaturan Pad",
|
||||||
"pad.settings.myView": "Tampilan Saya",
|
"pad.settings.myView": "Tampilan Saya",
|
||||||
|
@ -49,6 +75,7 @@
|
||||||
"pad.settings.fontType": "Jenis fonta:",
|
"pad.settings.fontType": "Jenis fonta:",
|
||||||
"pad.settings.language": "Bahasa:",
|
"pad.settings.language": "Bahasa:",
|
||||||
"pad.settings.about": "Tentang",
|
"pad.settings.about": "Tentang",
|
||||||
|
"pad.settings.poweredBy": "Ditenagai oleh",
|
||||||
"pad.importExport.import_export": "Impor/Ekspor",
|
"pad.importExport.import_export": "Impor/Ekspor",
|
||||||
"pad.importExport.import": "Unggah setiap berkas teks atau dokumen",
|
"pad.importExport.import": "Unggah setiap berkas teks atau dokumen",
|
||||||
"pad.importExport.importSuccessful": "Berhasil!",
|
"pad.importExport.importSuccessful": "Berhasil!",
|
||||||
|
@ -59,9 +86,9 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
"pad.importExport.abiword.innerHTML": "Anda hanya dapat mengimpor dari format teks biasa atau HTML. Untuk fitur impor yang lebih canggih, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">pasanglah AbiWord</a>.",
|
"pad.importExport.abiword.innerHTML": "Anda hanya dapat mengimpor dari format teks polos atau HTML. Untuk fitur impor yang lebih canggih, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">pasanglah AbiWord atau LibreOffice</a>.",
|
||||||
"pad.modals.connected": "Tersambung.",
|
"pad.modals.connected": "Tersambung.",
|
||||||
"pad.modals.reconnecting": "Menyambungkan kembali ke pad Anda...",
|
"pad.modals.reconnecting": "Menyambungkan kembali ke pad Anda…",
|
||||||
"pad.modals.forcereconnect": "Sambung kembali secara paksa",
|
"pad.modals.forcereconnect": "Sambung kembali secara paksa",
|
||||||
"pad.modals.reconnecttimer": "Mencoba menghubungkan ulang",
|
"pad.modals.reconnecttimer": "Mencoba menghubungkan ulang",
|
||||||
"pad.modals.cancel": "Batalkan",
|
"pad.modals.cancel": "Batalkan",
|
||||||
|
@ -83,6 +110,10 @@
|
||||||
"pad.modals.corruptPad.cause": "Hal ini mungkin disebabkan oleh konfigurasi peladen salah atau sesuatu perilaku yang tidak diperkirakan. Silahkan hubungi administrator Anda jika Anda merasakan ini adalah satu kesalahan.",
|
"pad.modals.corruptPad.cause": "Hal ini mungkin disebabkan oleh konfigurasi peladen salah atau sesuatu perilaku yang tidak diperkirakan. Silahkan hubungi administrator Anda jika Anda merasakan ini adalah satu kesalahan.",
|
||||||
"pad.modals.deleted": "Dihapus",
|
"pad.modals.deleted": "Dihapus",
|
||||||
"pad.modals.deleted.explanation": "Pad ini telah dibuang.",
|
"pad.modals.deleted.explanation": "Pad ini telah dibuang.",
|
||||||
|
"pad.modals.rateLimited": "Laju Dibatasi.",
|
||||||
|
"pad.modals.rateLimited.explanation": "Anda mengirim terlalu banyak pesan ke pad ini sehingga itu memutus Anda.",
|
||||||
|
"pad.modals.rejected.explanation": "Server menolak suatu pesan yang dikirim oleh peramban Anda.",
|
||||||
|
"pad.modals.rejected.cause": "Server mungkin telah diperbarui ketika Anda sedang melihat pad, atau mungkin ada bug dalam Etherpad. Cobalah memuat ulang halaman.",
|
||||||
"pad.modals.disconnected": "Sambungan Anda telah diputuskan.",
|
"pad.modals.disconnected": "Sambungan Anda telah diputuskan.",
|
||||||
"pad.modals.disconnected.explanation": "Sambungan ke peladen terputus",
|
"pad.modals.disconnected.explanation": "Sambungan ke peladen terputus",
|
||||||
"pad.modals.disconnected.cause": "Peladen ini mungkin tidak tersedia. Silakan beritahu administrator jika masalah ini berkelanjutan.",
|
"pad.modals.disconnected.cause": "Peladen ini mungkin tidak tersedia. Silakan beritahu administrator jika masalah ini berkelanjutan.",
|
||||||
|
@ -95,6 +126,7 @@
|
||||||
"pad.chat.loadmessages": "Muatkan lebih banyak pesan",
|
"pad.chat.loadmessages": "Muatkan lebih banyak pesan",
|
||||||
"pad.chat.stick.title": "Tempelkan chat ke layar",
|
"pad.chat.stick.title": "Tempelkan chat ke layar",
|
||||||
"pad.chat.writeMessage.placeholder": "Tuliskan pesan Anda di sini",
|
"pad.chat.writeMessage.placeholder": "Tuliskan pesan Anda di sini",
|
||||||
|
"timeslider.followContents": "Ikuti pembaruan isi pad",
|
||||||
"timeslider.pageTitle": "{{appTitle}} Timeslider",
|
"timeslider.pageTitle": "{{appTitle}} Timeslider",
|
||||||
"timeslider.toolbar.returnbutton": "Kembali ke pad",
|
"timeslider.toolbar.returnbutton": "Kembali ke pad",
|
||||||
"timeslider.toolbar.authors": "Pembuat:",
|
"timeslider.toolbar.authors": "Pembuat:",
|
||||||
|
@ -124,7 +156,7 @@
|
||||||
"pad.savedrevs.timeslider": "Anda bisa melihat revisi yang tersimpan dengan mengunjungi timeslider",
|
"pad.savedrevs.timeslider": "Anda bisa melihat revisi yang tersimpan dengan mengunjungi timeslider",
|
||||||
"pad.userlist.entername": "Masukkan nama Anda",
|
"pad.userlist.entername": "Masukkan nama Anda",
|
||||||
"pad.userlist.unnamed": "tanpa nama",
|
"pad.userlist.unnamed": "tanpa nama",
|
||||||
"pad.editbar.clearcolors": "Padamkan warna penulis pada seluruh dokumen?",
|
"pad.editbar.clearcolors": "Bersihkan warna penulis pada seluruh dokumen? Ini tidak dapat dibatalkan",
|
||||||
"pad.impexp.importbutton": "Impor Sekarang",
|
"pad.impexp.importbutton": "Impor Sekarang",
|
||||||
"pad.impexp.importing": "Mengimpor...",
|
"pad.impexp.importing": "Mengimpor...",
|
||||||
"pad.impexp.confirmimport": "Mengimpor berkas akan menimpa teks saat ini di pad ini. Apakah Anda benar-benar ingin melakukannya?",
|
"pad.impexp.confirmimport": "Mengimpor berkas akan menimpa teks saat ini di pad ini. Apakah Anda benar-benar ingin melakukannya?",
|
||||||
|
@ -133,5 +165,6 @@
|
||||||
"pad.impexp.uploadFailed": "Penunggahan gagal, silakan mencoba lagi",
|
"pad.impexp.uploadFailed": "Penunggahan gagal, silakan mencoba lagi",
|
||||||
"pad.impexp.importfailed": "Impor gagal",
|
"pad.impexp.importfailed": "Impor gagal",
|
||||||
"pad.impexp.copypaste": "Silahkan salin tempel",
|
"pad.impexp.copypaste": "Silahkan salin tempel",
|
||||||
"pad.impexp.exportdisabled": "Mengekspor dalam format {{type}} dilarang. Silakan hubungi administrator untuk detilnya."
|
"pad.impexp.exportdisabled": "Mengekspor dalam format {{type}} dilarang. Silakan hubungi administrator untuk detilnya.",
|
||||||
|
"pad.impexp.maxFileSize": "Berkas terlalu besar. Hubungi administrator situs Anda untuk menaikkan ukuran berkas yang diizinkan untuk impor"
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
"pad.settings.fontType": "Jenis fon:",
|
"pad.settings.fontType": "Jenis fon:",
|
||||||
"pad.settings.fontType.normal": "Normal",
|
"pad.settings.fontType.normal": "Normal",
|
||||||
"pad.settings.language": "Bahasa:",
|
"pad.settings.language": "Bahasa:",
|
||||||
|
"pad.settings.poweredBy": "Dikuasakan oleh",
|
||||||
"pad.importExport.import_export": "Import/Eksport",
|
"pad.importExport.import_export": "Import/Eksport",
|
||||||
"pad.importExport.import": "Muat naik sebarang fail teks atau dokumen",
|
"pad.importExport.import": "Muat naik sebarang fail teks atau dokumen",
|
||||||
"pad.importExport.importSuccessful": "Berjaya!",
|
"pad.importExport.importSuccessful": "Berjaya!",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"हिमाल सुबेदी"
|
"हिमाल सुबेदी"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"admin_plugins.description": "विवरण",
|
||||||
"index.newPad": "नयाँ प्याड",
|
"index.newPad": "नयाँ प्याड",
|
||||||
"index.createOpenPad": "नाम सहितको नयाँ प्याड सिर्जना गर्ने / खोल्ने :",
|
"index.createOpenPad": "नाम सहितको नयाँ प्याड सिर्जना गर्ने / खोल्ने :",
|
||||||
"pad.toolbar.bold.title": "मोटो (Ctrl-B)",
|
"pad.toolbar.bold.title": "मोटो (Ctrl-B)",
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ueberDB from 'ueberdb2';
|
import {Database} from 'ueberdb2';
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const stats = require('../stats')
|
const stats = require('../stats')
|
||||||
|
@ -37,7 +37,7 @@ exports.db = null;
|
||||||
* Initializes the database with the settings provided by the settings module
|
* Initializes the database with the settings provided by the settings module
|
||||||
*/
|
*/
|
||||||
exports.init = async () => {
|
exports.init = async () => {
|
||||||
exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger);
|
exports.db = new Database(settings.dbType, settings.dbSettings, null, logger);
|
||||||
await exports.db.init();
|
await exports.db.init();
|
||||||
if (exports.db.metrics != null) {
|
if (exports.db.metrics != null) {
|
||||||
for (const [metric, value] of Object.entries(exports.db.metrics)) {
|
for (const [metric, value] of Object.entries(exports.db.metrics)) {
|
||||||
|
|
|
@ -996,7 +996,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
percentageToScrollWhenUserPressesArrowUp:
|
percentageToScrollWhenUserPressesArrowUp:
|
||||||
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
|
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
|
||||||
},
|
},
|
||||||
initialChangesets: [], // FIXME: REMOVE THIS SHIT
|
initialChangesets: [], // FIXME: REMOVE THIS SHIT,
|
||||||
|
mode: process.env.NODE_ENV
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a username to the clientVars if one avaiable
|
// Add a username to the clientVars if one avaiable
|
||||||
|
|
|
@ -128,6 +128,7 @@ exports.socketio = (hookName: string, {io}: any) => {
|
||||||
maxResult = 0;
|
maxResult = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset to default values if out of bounds
|
||||||
if (query.offset && query.offset < 0) {
|
if (query.offset && query.offset < 0) {
|
||||||
query.offset = 0;
|
query.offset = 0;
|
||||||
} else if (query.offset > maxResult) {
|
} else if (query.offset > maxResult) {
|
||||||
|
@ -135,11 +136,14 @@ exports.socketio = (hookName: string, {io}: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.limit && query.limit < 0) {
|
if (query.limit && query.limit < 0) {
|
||||||
|
// Too small
|
||||||
query.limit = 0;
|
query.limit = 0;
|
||||||
} else if (query.limit > queryPadLimit) {
|
} else if (query.limit > queryPadLimit) {
|
||||||
|
// Too big
|
||||||
query.limit = queryPadLimit;
|
query.limit = queryPadLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (query.sortBy === 'padName') {
|
if (query.sortBy === 'padName') {
|
||||||
result = result.sort((a, b) => {
|
result = result.sort((a, b) => {
|
||||||
if (a < b) return query.ascending ? -1 : 1;
|
if (a < b) return query.ascending ? -1 : 1;
|
||||||
|
@ -160,53 +164,78 @@ exports.socketio = (hookName: string, {io}: any) => {
|
||||||
revisionNumber
|
revisionNumber
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else if (query.sortBy === "revisionNumber") {
|
||||||
const currentWinners: PadQueryResult[] = []
|
const currentWinners: PadQueryResult[] = []
|
||||||
let queryOffsetCounter = 0
|
const padMapping = [] as {padId: string, revisionNumber: number}[]
|
||||||
for (let res of result) {
|
for (let res of result) {
|
||||||
|
|
||||||
const pad = await padManager.getPad(res);
|
const pad = await padManager.getPad(res);
|
||||||
const padType = {
|
const revisionNumber = pad.getHeadRevisionNumber()
|
||||||
padName: res,
|
padMapping.push({padId: res, revisionNumber})
|
||||||
lastEdited: await pad.getLastEdit(),
|
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (currentWinners.length < query.limit) {
|
|
||||||
if (queryOffsetCounter < query.offset) {
|
|
||||||
queryOffsetCounter++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
currentWinners.push({
|
|
||||||
padName: res,
|
|
||||||
lastEdited: await pad.getLastEdit(),
|
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Kick out worst pad and replace by current pad
|
|
||||||
let worstPad = currentWinners.sort((a, b) => {
|
|
||||||
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
|
|
||||||
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
if (worstPad[0] && worstPad[0][query.sortBy] < padType[query.sortBy]) {
|
|
||||||
if (queryOffsetCounter < query.offset) {
|
|
||||||
queryOffsetCounter++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
|
|
||||||
currentWinners.push({
|
|
||||||
padName: res,
|
|
||||||
lastEdited: await pad.getLastEdit(),
|
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
data.results = currentWinners;
|
padMapping.sort((a, b) => {
|
||||||
|
if (a.revisionNumber < b.revisionNumber) return query.ascending ? -1 : 1;
|
||||||
|
if (a.revisionNumber > b.revisionNumber) return query.ascending ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const padRetrieval of padMapping.slice(query.offset, query.offset + query.limit)) {
|
||||||
|
let pad = await padManager.getPad(padRetrieval.padId);
|
||||||
|
currentWinners.push({
|
||||||
|
padName: padRetrieval.padId,
|
||||||
|
lastEdited: await pad.getLastEdit(),
|
||||||
|
userCount: api.padUsersCount(pad.padName).padUsersCount,
|
||||||
|
revisionNumber: padRetrieval.revisionNumber
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data.results = currentWinners;
|
||||||
|
} else if (query.sortBy === "userCount") {
|
||||||
|
const currentWinners: PadQueryResult[] = []
|
||||||
|
const padMapping = [] as {padId: string, userCount: number}[]
|
||||||
|
for (let res of result) {
|
||||||
|
const userCount = api.padUsersCount(res).padUsersCount
|
||||||
|
padMapping.push({padId: res, userCount})
|
||||||
|
}
|
||||||
|
padMapping.sort((a, b) => {
|
||||||
|
if (a.userCount < b.userCount) return query.ascending ? -1 : 1;
|
||||||
|
if (a.userCount > b.userCount) return query.ascending ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const padRetrieval of padMapping.slice(query.offset, query.offset + query.limit)) {
|
||||||
|
let pad = await padManager.getPad(padRetrieval.padId);
|
||||||
|
currentWinners.push({
|
||||||
|
padName: padRetrieval.padId,
|
||||||
|
lastEdited: await pad.getLastEdit(),
|
||||||
|
userCount: padRetrieval.userCount,
|
||||||
|
revisionNumber: pad.getHeadRevisionNumber()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data.results = currentWinners;
|
||||||
|
} else if (query.sortBy === "lastEdited") {
|
||||||
|
const currentWinners: PadQueryResult[] = []
|
||||||
|
const padMapping = [] as {padId: string, lastEdited: string}[]
|
||||||
|
for (let res of result) {
|
||||||
|
const pad = await padManager.getPad(res);
|
||||||
|
const lastEdited = await pad.getLastEdit();
|
||||||
|
padMapping.push({padId: res, lastEdited})
|
||||||
|
}
|
||||||
|
padMapping.sort((a, b) => {
|
||||||
|
if (a.lastEdited < b.lastEdited) return query.ascending ? -1 : 1;
|
||||||
|
if (a.lastEdited > b.lastEdited) return query.ascending ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const padRetrieval of padMapping.slice(query.offset, query.offset + query.limit)) {
|
||||||
|
let pad = await padManager.getPad(padRetrieval.padId);
|
||||||
|
currentWinners.push({
|
||||||
|
padName: padRetrieval.padId,
|
||||||
|
lastEdited: padRetrieval.lastEdited,
|
||||||
|
userCount: api.padUsersCount(pad.padName).padUsersCount,
|
||||||
|
revisionNumber: pad.getHeadRevisionNumber()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data.results = currentWinners;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('results:padLoad', data);
|
socket.emit('results:padLoad', data);
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('path');
|
import path from 'node:path';
|
||||||
const eejs = require('../../eejs');
|
const eejs = require('../../eejs')
|
||||||
const fs = require('fs');
|
import fs from 'node:fs';
|
||||||
const fsp = fs.promises;
|
const fsp = fs.promises;
|
||||||
const toolbar = require('../../utils/toolbar');
|
const toolbar = require('../../utils/toolbar');
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require('../../utils/Settings');
|
||||||
const util = require('util');
|
import util from 'node:util';
|
||||||
const webaccess = require('./webaccess');
|
const webaccess = require('./webaccess');
|
||||||
|
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||||
|
|
||||||
|
import {build, buildSync} from 'esbuild'
|
||||||
|
let ioI: { sockets: { sockets: any[]; }; } | null = null
|
||||||
|
|
||||||
|
exports.socketio = (hookName: string, {io}: any) => {
|
||||||
|
ioI = io
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
// This endpoint is intended to conform to:
|
// This endpoint is intended to conform to:
|
||||||
|
@ -73,49 +82,291 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName:string, args:any, cb:Function) => {
|
|
||||||
// serve index.html under /
|
|
||||||
args.app.get('/', (req:any, res:any) => {
|
const convertTypescript = (content: string) => {
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
|
const outputRaw = buildSync({
|
||||||
|
stdin: {
|
||||||
|
contents: content,
|
||||||
|
resolveDir: path.join(settings.root, 'var','js'),
|
||||||
|
loader: 'js'
|
||||||
|
},
|
||||||
|
alias:{
|
||||||
|
"ep_etherpad-lite/static/js/browser": 'ep_etherpad-lite/static/js/vendors/browser',
|
||||||
|
"ep_etherpad-lite/static/js/nice-select": 'ep_etherpad-lite/static/js/vendors/nice-select'
|
||||||
|
},
|
||||||
|
bundle: true, // Bundle the files together
|
||||||
|
minify: process.env.NODE_ENV === "production", // Minify the output
|
||||||
|
sourcemap: !(process.env.NODE_ENV === "production"), // Generate source maps
|
||||||
|
sourceRoot: settings.root+"/src/static/js/",
|
||||||
|
target: ['es2020'], // Target ECMAScript version
|
||||||
|
metafile: true,
|
||||||
|
write: false, // Do not write to file system,
|
||||||
|
})
|
||||||
|
const output = outputRaw.outputFiles[0].text
|
||||||
|
|
||||||
|
return {
|
||||||
|
output,
|
||||||
|
hash: outputRaw.outputFiles[0].hash.replaceAll('/','2')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLiveReload = async (args: any, padString: string, timeSliderString: string, indexString: any) => {
|
||||||
|
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 (req.path == "/"){
|
||||||
|
routeHandlers['/'](req, res);
|
||||||
|
} else if (routeHandlers[req.path]) {
|
||||||
|
routeHandlers[req.path](req, res);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// serve pad.html under /p
|
function handleUpdate() {
|
||||||
args.app.get('/p/:pad', (req:any, res:any, next:Function) => {
|
|
||||||
// The below might break for pads being rewritten
|
|
||||||
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
|
|
||||||
|
|
||||||
hooks.callAll('padInitToolbar', {
|
convertTypescriptWatched(indexString, (output, hash) => {
|
||||||
toolbar,
|
setRouteHandler('/watch/index', (req: any, res: any) => {
|
||||||
isReadOnly,
|
res.header('Content-Type', 'application/javascript');
|
||||||
|
res.send(output)
|
||||||
|
})
|
||||||
|
setRouteHandler('/', (req: any, res: any) => {
|
||||||
|
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, entrypoint: '/watch/index?hash=' + hash, settings}));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 indexString = eejs.require('ep_etherpad-lite/templates/indexBootstrap.js', {
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeSliderString = eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', {
|
||||||
|
pluginModules: (() => {
|
||||||
|
const pluginModules = new Set();
|
||||||
|
for (const part of plugins.parts) {
|
||||||
|
for (const [, hookFnName] of Object.entries(part.client_hooks || {})) {
|
||||||
|
// @ts-ignore
|
||||||
|
pluginModules.add(hookFnName.split(':')[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...pluginModules];
|
||||||
|
})(),
|
||||||
|
settings,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const outdir = path.join(settings.root, 'var','js')
|
||||||
|
// Create the outdir if it doesn't exist
|
||||||
|
if (!fs.existsSync(outdir)) {
|
||||||
|
fs.mkdirSync(outdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileNamePad: string
|
||||||
|
let fileNameTimeSlider: string
|
||||||
|
let fileNameIndex: string
|
||||||
|
if(process.env.NODE_ENV === "production"){
|
||||||
|
const padSliderWrite = convertTypescript(padString)
|
||||||
|
const timeSliderWrite = convertTypescript(timeSliderString)
|
||||||
|
const indexWrite = convertTypescript(indexString)
|
||||||
|
|
||||||
|
fileNamePad = `padbootstrap-${padSliderWrite.hash}.min.js`
|
||||||
|
fileNameTimeSlider = `timeSliderBootstrap-${timeSliderWrite.hash}.min.js`
|
||||||
|
fileNameIndex = `indexBootstrap-${indexWrite.hash}.min.js`
|
||||||
|
const pathNamePad = path.join(outdir, fileNamePad)
|
||||||
|
const pathNameTimeSlider = path.join(outdir, fileNameTimeSlider)
|
||||||
|
const pathNameIndex = path.join(outdir, 'index.js')
|
||||||
|
|
||||||
|
if (!fs.existsSync(pathNamePad)) {
|
||||||
|
fs.writeFileSync(pathNamePad, padSliderWrite.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(pathNameIndex)) {
|
||||||
|
fs.writeFileSync(pathNameIndex, indexWrite.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(pathNameTimeSlider)) {
|
||||||
|
fs.writeFileSync(pathNameTimeSlider,timeSliderWrite.output)
|
||||||
|
}
|
||||||
|
|
||||||
|
args.app.get("/"+fileNamePad, (req: any, res: any) => {
|
||||||
|
res.sendFile(pathNamePad)
|
||||||
|
})
|
||||||
|
|
||||||
|
args.app.get("/"+fileNameIndex, (req: any, res: any) => {
|
||||||
|
res.sendFile(pathNameIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
args.app.get("/"+fileNameTimeSlider, (req: any, res: any) => {
|
||||||
|
res.sendFile(pathNameTimeSlider)
|
||||||
|
})
|
||||||
|
|
||||||
|
// serve index.html under /
|
||||||
|
args.app.get('/', (req: any, res: any) => {
|
||||||
|
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, settings, entrypoint: "/"+fileNameIndex}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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
|
// serve pad.html under /p
|
||||||
args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => {
|
args.app.get('/p/:pad', (req: any, res: any, next: Function) => {
|
||||||
hooks.callAll('padInitToolbar', {
|
// The below might break for pads being rewritten
|
||||||
toolbar,
|
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
|
||||||
|
|
||||||
|
hooks.callAll('padInitToolbar', {
|
||||||
|
toolbar,
|
||||||
|
isReadOnly
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = eejs.require('ep_etherpad-lite/templates/pad.html', {
|
||||||
|
req,
|
||||||
|
toolbar,
|
||||||
|
isReadOnly,
|
||||||
|
entrypoint: "/"+fileNamePad
|
||||||
|
})
|
||||||
|
res.send(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
// serve timeslider.html under /p/$padname/timeslider
|
||||||
req,
|
args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => {
|
||||||
toolbar,
|
hooks.callAll('padInitToolbar', {
|
||||||
}));
|
toolbar,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||||
|
req,
|
||||||
|
toolbar,
|
||||||
|
entrypoint: "/"+fileNameTimeSlider
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await handleLiveReload(args, padString, timeSliderString, indexString)
|
||||||
|
}
|
||||||
|
|
||||||
// The client occasionally polls this endpoint to get an updated expiration for the express_sid
|
// The client occasionally polls this endpoint to get an updated expiration for the express_sid
|
||||||
// cookie. This handler must be installed after the express-session middleware.
|
// cookie. This handler must be installed after the express-session middleware.
|
||||||
args.app.put('/_extendExpressSessionLifetime', (req:any, res:any) => {
|
args.app.put('/_extendExpressSessionLifetime', (req: any, res: any) => {
|
||||||
// express-session automatically calls req.session.touch() so we don't need to do it here.
|
// express-session automatically calls req.session.touch() so we don't need to do it here.
|
||||||
res.json({status: 'ok'});
|
res.json({status: 'ok'});
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,6 @@ const path = require('path');
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require('../../utils/Settings');
|
||||||
import CachingMiddleware from '../../utils/caching_middleware';
|
import CachingMiddleware from '../../utils/caching_middleware';
|
||||||
const Yajsml = require('etherpad-yajsml');
|
|
||||||
|
|
||||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
||||||
const getTar = async () => {
|
const getTar = async () => {
|
||||||
|
@ -43,24 +42,6 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
// file-specific hacks for ace/require-kernel/etc.
|
// file-specific hacks for ace/require-kernel/etc.
|
||||||
app.all('/static/:filename(*)', minify.minify);
|
app.all('/static/:filename(*)', minify.minify);
|
||||||
|
|
||||||
// Setup middleware that will package JavaScript files served by minify for
|
|
||||||
// CommonJS loader on the client-side.
|
|
||||||
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
|
|
||||||
const jsServer = new (Yajsml.Server)({
|
|
||||||
rootPath: 'javascripts/src/',
|
|
||||||
rootURI: 'http://invalid.invalid/static/js/',
|
|
||||||
libraryPath: 'javascripts/lib/',
|
|
||||||
libraryURI: 'http://invalid.invalid/static/plugins/',
|
|
||||||
requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround.
|
|
||||||
});
|
|
||||||
|
|
||||||
const StaticAssociator = Yajsml.associators.StaticAssociator;
|
|
||||||
const associations = Yajsml.associators.associationsForSimpleMapping(await getTar());
|
|
||||||
const associator = new StaticAssociator(associations);
|
|
||||||
jsServer.setAssociator(associator);
|
|
||||||
|
|
||||||
app.use(jsServer.handle.bind(jsServer));
|
|
||||||
|
|
||||||
// serve plugin definitions
|
// serve plugin definitions
|
||||||
// not very static, but served here so that client can do
|
// not very static, but served here so that client can do
|
||||||
// require("pluginfw/static/js/plugin-definitions.js");
|
// require("pluginfw/static/js/plugin-definitions.js");
|
||||||
|
|
|
@ -26,7 +26,7 @@ const db = require('../db/DB');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const supportedElems = require('../../static/js/contentcollector').supportedElems;
|
const supportedElems = require('../../static/js/contentcollector').supportedElems;
|
||||||
import ueberdb from 'ueberdb2';
|
import {Database} from 'ueberdb2';
|
||||||
|
|
||||||
const logger = log4js.getLogger('ImportEtherpad');
|
const logger = log4js.getLogger('ImportEtherpad');
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
||||||
|
|
||||||
const data = new Map();
|
const data = new Map();
|
||||||
const existingAuthors = new Set();
|
const existingAuthors = new Set();
|
||||||
const padDb = new ueberdb.Database('memory', {data});
|
const padDb = new Database('memory', {data});
|
||||||
await padDb.init();
|
await padDb.init();
|
||||||
try {
|
try {
|
||||||
const processRecord = async (key:string, value: null|{
|
const processRecord = async (key:string, value: null|{
|
||||||
|
|
|
@ -25,7 +25,6 @@ const settings = require('./Settings');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||||
const RequireKernel = require('etherpad-require-kernel');
|
|
||||||
const mime = require('mime-types');
|
const mime = require('mime-types');
|
||||||
const Threads = require('threads');
|
const Threads = require('threads');
|
||||||
const log4js = require('log4js');
|
const log4js = require('log4js');
|
||||||
|
@ -217,12 +216,6 @@ const statFile = async (filename, dirStatLimit) => {
|
||||||
|
|
||||||
if (dirStatLimit < 1 || filename === '' || filename === '/') {
|
if (dirStatLimit < 1 || filename === '' || filename === '/') {
|
||||||
return [null, false];
|
return [null, false];
|
||||||
} else if (filename === 'js/ace.js') {
|
|
||||||
// Sometimes static assets are inlined into this file, so we have to stat
|
|
||||||
// everything.
|
|
||||||
return [await lastModifiedDateOfEverything(), true];
|
|
||||||
} else if (filename === 'js/require-kernel.js') {
|
|
||||||
return [_requireLastModified, true];
|
|
||||||
} else {
|
} else {
|
||||||
let stats;
|
let stats;
|
||||||
try {
|
try {
|
||||||
|
@ -239,37 +232,12 @@ const statFile = async (filename, dirStatLimit) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastModifiedDateOfEverything = async () => {
|
let contentCache = new Map();
|
||||||
const folders2check = [path.join(ROOT_DIR, 'js/'), path.join(ROOT_DIR, 'css/')];
|
|
||||||
let latestModification = null;
|
|
||||||
// go through this two folders
|
|
||||||
await Promise.all(folders2check.map(async (dir) => {
|
|
||||||
// read the files in the folder
|
|
||||||
const files = await fs.readdir(dir);
|
|
||||||
|
|
||||||
// we wanna check the directory itself for changes too
|
|
||||||
files.push('.');
|
|
||||||
|
|
||||||
// go through all files in this folder
|
|
||||||
await Promise.all(files.map(async (filename) => {
|
|
||||||
// get the stat data of this file
|
|
||||||
const stats = await fs.stat(path.join(dir, filename));
|
|
||||||
|
|
||||||
// compare the modification time to the highest found
|
|
||||||
if (latestModification == null || stats.mtime > latestModification) {
|
|
||||||
latestModification = stats.mtime;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
return latestModification;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This should be provided by the module, but until then, just use startup
|
|
||||||
// time.
|
|
||||||
const _requireLastModified = new Date();
|
|
||||||
const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`;
|
|
||||||
|
|
||||||
const getFileCompressed = async (filename, contentType) => {
|
const getFileCompressed = async (filename, contentType) => {
|
||||||
|
if (contentCache.has(filename)) {
|
||||||
|
return contentCache.get(filename);
|
||||||
|
}
|
||||||
let content = await getFile(filename);
|
let content = await getFile(filename);
|
||||||
if (!content || !settings.minify) {
|
if (!content || !settings.minify) {
|
||||||
return content;
|
return content;
|
||||||
|
@ -291,6 +259,7 @@ const getFileCompressed = async (filename, contentType) => {
|
||||||
console.error('getFile() returned an error in ' +
|
console.error('getFile() returned an error in ' +
|
||||||
`getFileCompressed(${filename}, ${contentType}): ${error}`);
|
`getFileCompressed(${filename}, ${contentType}): ${error}`);
|
||||||
}
|
}
|
||||||
|
contentCache.set(filename, content);
|
||||||
resolve(content);
|
resolve(content);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -300,20 +269,27 @@ const getFileCompressed = async (filename, contentType) => {
|
||||||
try {
|
try {
|
||||||
logger.info('Compress CSS file %s.', filename);
|
logger.info('Compress CSS file %s.', filename);
|
||||||
|
|
||||||
content = await compressCSS(filename, ROOT_DIR);
|
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
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
||||||
}
|
}
|
||||||
|
contentCache.set(filename, content);
|
||||||
resolve(content);
|
resolve(content);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
contentCache.set(filename, content);
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFile = async (filename) => {
|
const getFile = async (filename) => {
|
||||||
if (filename === 'js/require-kernel.js') return requireDefinition();
|
|
||||||
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,8 @@
|
||||||
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fsp = require('fs').promises;
|
|
||||||
import path from 'node:path'
|
|
||||||
import {expose} from 'threads'
|
import {expose} from 'threads'
|
||||||
import lightminify from 'lightningcss'
|
import {build, transform} from 'esbuild';
|
||||||
import {transform} from 'esbuild';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Minify JS content
|
* Minify JS content
|
||||||
|
@ -22,23 +19,27 @@ const compressJS = async (content) => {
|
||||||
* @param {string} filename - name of the file
|
* @param {string} filename - name of the file
|
||||||
* @param {string} ROOT_DIR - the root dir of Etherpad
|
* @param {string} ROOT_DIR - the root dir of Etherpad
|
||||||
*/
|
*/
|
||||||
const compressCSS = async (filename, ROOT_DIR) => {
|
const compressCSS = async (content) => {
|
||||||
const absPath = path.resolve(ROOT_DIR, filename);
|
const transformedCSS = await build(
|
||||||
try {
|
{
|
||||||
const basePath = path.dirname(absPath);
|
entryPoints: [content],
|
||||||
const file = await fsp.readFile(absPath, 'utf8');
|
|
||||||
let { code } = lightminify.transform({
|
|
||||||
errorRecovery: true,
|
|
||||||
filename: basePath,
|
|
||||||
minify: true,
|
minify: true,
|
||||||
code: Buffer.from(file, 'utf8')
|
bundle: true,
|
||||||
});
|
loader:{
|
||||||
return code.toString();
|
'.jpg': 'dataurl',
|
||||||
} catch (error) {
|
'.png': 'dataurl',
|
||||||
// on error, just yield the un-minified original, but write a log message
|
'.gif': 'dataurl',
|
||||||
console.error(`Unexpected error minifying ${filename} (${absPath}): ${JSON.stringify(error)}`);
|
'.ttf': 'dataurl',
|
||||||
return await fsp.readFile(absPath, 'utf8');
|
'.otf': 'dataurl',
|
||||||
}
|
'.woff': 'dataurl',
|
||||||
|
'.woff2': 'dataurl',
|
||||||
|
'.eot': 'dataurl',
|
||||||
|
'.svg': 'dataurl'
|
||||||
|
},
|
||||||
|
write: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return transformedCSS.outputFiles[0].text
|
||||||
};
|
};
|
||||||
|
|
||||||
expose({
|
expose({
|
||||||
|
|
|
@ -54,13 +54,14 @@ const nonSettings = [
|
||||||
|
|
||||||
// This is a function to make it easy to create a new instance. It is important to not reuse a
|
// This is a function to make it easy to create a new instance. It is important to not reuse a
|
||||||
// config object after passing it to log4js.configure() because that method mutates the object. :(
|
// config object after passing it to log4js.configure() because that method mutates the object. :(
|
||||||
const defaultLogConfig = (level: string) => ({
|
const defaultLogConfig = (level: string, layoutType: string) => ({
|
||||||
appenders: {console: {type: 'console'}},
|
appenders: {console: {type: 'console', layout: {type: layoutType}}},
|
||||||
categories: {
|
categories: {
|
||||||
default: {appenders: ['console'], level},
|
default: {appenders: ['console'], level},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const defaultLogLevel = 'INFO';
|
const defaultLogLevel = 'INFO';
|
||||||
|
const defaultLogLayoutType = 'colored';
|
||||||
|
|
||||||
const initLogging = (config: any) => {
|
const initLogging = (config: any) => {
|
||||||
// log4js.configure() modifies exports.logconfig so check for equality first.
|
// log4js.configure() modifies exports.logconfig so check for equality first.
|
||||||
|
@ -76,7 +77,7 @@ const initLogging = (config: any) => {
|
||||||
|
|
||||||
// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized
|
// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized
|
||||||
// with the user's chosen log level and logger config after the settings have been loaded.
|
// with the user's chosen log level and logger config after the settings have been loaded.
|
||||||
initLogging(defaultLogConfig(defaultLogLevel));
|
initLogging(defaultLogConfig(defaultLogLevel, defaultLogLayoutType));
|
||||||
|
|
||||||
/* Root path of the installation */
|
/* Root path of the installation */
|
||||||
exports.root = absolutePaths.findEtherpadRoot();
|
exports.root = absolutePaths.findEtherpadRoot();
|
||||||
|
@ -291,6 +292,11 @@ exports.allowUnknownFileEnds = true;
|
||||||
*/
|
*/
|
||||||
exports.loglevel = defaultLogLevel;
|
exports.loglevel = defaultLogLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The log layout type of log4js
|
||||||
|
*/
|
||||||
|
exports.logLayoutType = defaultLogLayoutType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable IP logging
|
* Disable IP logging
|
||||||
*/
|
*/
|
||||||
|
@ -817,7 +823,12 @@ exports.reloadSettings = () => {
|
||||||
storeSettings(credentials);
|
storeSettings(credentials);
|
||||||
|
|
||||||
// Init logging config
|
// Init logging config
|
||||||
exports.logconfig = defaultLogConfig(exports.loglevel ? exports.loglevel : defaultLogLevel);
|
exports.logconfig = defaultLogConfig(
|
||||||
|
exports.loglevel ? exports.loglevel : defaultLogLevel,
|
||||||
|
exports.logLayoutType ? exports.logLayoutType : defaultLogLayoutType
|
||||||
|
);
|
||||||
|
logger.warn("loglevel: " + exports.loglevel);
|
||||||
|
logger.warn("logLayoutType: " + exports.logLayoutType);
|
||||||
initLogging(exports.logconfig);
|
initLogging(exports.logconfig);
|
||||||
|
|
||||||
if (!exports.skinName) {
|
if (!exports.skinName) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/**
|
/**
|
||||||
* The Toolbar Module creates and renders the toolbars and buttons
|
* The Toolbar Module creates and renders the toolbars and buttons
|
||||||
*/
|
*/
|
||||||
const _ = require('underscore');
|
import {isString, reduce, each, isUndefined, map, first, last, extend, escape} from 'underscore';
|
||||||
|
|
||||||
const removeItem = (array: string[], what: string) => {
|
const removeItem = (array: string[], what: string) => {
|
||||||
let ax;
|
let ax;
|
||||||
|
@ -21,7 +21,7 @@ const defaultButtonAttributes = (name: string, overrides?: boolean) => ({
|
||||||
const tag = (name: string, attributes: AttributeObj, contents?: string) => {
|
const tag = (name: string, attributes: AttributeObj, contents?: string) => {
|
||||||
const aStr = tagAttributes(attributes);
|
const aStr = tagAttributes(attributes);
|
||||||
|
|
||||||
if (_.isString(contents) && contents!.length > 0) {
|
if (isString(contents) && contents!.length > 0) {
|
||||||
return `<${name}${aStr}>${contents}</${name}>`;
|
return `<${name}${aStr}>${contents}</${name}>`;
|
||||||
} else {
|
} else {
|
||||||
return `<${name}${aStr}></${name}>`;
|
return `<${name}${aStr}></${name}>`;
|
||||||
|
@ -34,14 +34,14 @@ type AttributeObj = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagAttributes = (attributes: AttributeObj) => {
|
const tagAttributes = (attributes: AttributeObj) => {
|
||||||
attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
|
attributes = reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
|
||||||
if (!_.isUndefined(val)) {
|
if (!isUndefined(val)) {
|
||||||
o[name] = val;
|
o[name] = val;
|
||||||
}
|
}
|
||||||
return o;
|
return o;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return ` ${_.map(attributes, (val: string, name: string) => `${name}="${_.escape(val)}"`).join(' ')}`;
|
return ` ${map(attributes, (val: string, name: string) => `${name}="${escape(val)}"`).join(' ')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ButtonGroupType = {
|
type ButtonGroupType = {
|
||||||
|
@ -58,7 +58,7 @@ class ButtonGroup {
|
||||||
|
|
||||||
public static fromArray = function (array: string[]) {
|
public static fromArray = function (array: string[]) {
|
||||||
const btnGroup = new ButtonGroup();
|
const btnGroup = new ButtonGroup();
|
||||||
_.each(array, (btnName: string) => {
|
each(array, (btnName: string) => {
|
||||||
const button = Button.load(btnName) as Button
|
const button = Button.load(btnName) as Button
|
||||||
btnGroup.addButton(button);
|
btnGroup.addButton(button);
|
||||||
});
|
});
|
||||||
|
@ -70,18 +70,19 @@ class ButtonGroup {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): string {
|
||||||
if (this.buttons && this.buttons.length === 1) {
|
if (this.buttons && this.buttons.length === 1) {
|
||||||
this.buttons[0].grouping = '';
|
this.buttons[0].grouping = '';
|
||||||
} else if (this.buttons && this.buttons.length > 1) {
|
} else if (this.buttons && this.buttons.length > 1) {
|
||||||
_.first(this.buttons).grouping = 'grouped-left';
|
first(this.buttons)!.grouping = 'grouped-left';
|
||||||
_.last(this.buttons).grouping = 'grouped-right';
|
last(this.buttons)!.grouping = 'grouped-right';
|
||||||
_.each(this.buttons.slice(1, -1), (btn: Button) => {
|
each(this.buttons.slice(1, -1), (btn: Button) => {
|
||||||
btn.grouping = 'grouped-middle';
|
btn.grouping = 'grouped-middle';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.map(this.buttons, (btn: ButtonGroup) => {
|
// @ts-ignore
|
||||||
|
return map(this.buttons, (btn: ButtonGroup) => {
|
||||||
if (btn) return btn.render();
|
if (btn) return btn.render();
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
}
|
}
|
||||||
|
@ -151,8 +152,8 @@ class SelectButton extends Button {
|
||||||
select(attributes: AttributeObj) {
|
select(attributes: AttributeObj) {
|
||||||
const options: string[] = [];
|
const options: string[] = [];
|
||||||
|
|
||||||
_.each(this.options, (opt: AttributeSelect) => {
|
each(this.options, (opt: AttributeSelect) => {
|
||||||
const a = _.extend({
|
const a = extend({
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
}, opt.attributes);
|
}, opt.attributes);
|
||||||
|
|
||||||
|
@ -299,7 +300,7 @@ module.exports = {
|
||||||
buttons[0].push('savedrevision');
|
buttons[0].push('savedrevision');
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = _.map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render());
|
const groups = map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render());
|
||||||
return groups.join(this.separator());
|
return groups.join(this.separator());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,30 +32,28 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@etherpad/express-session": "^1.18.2",
|
"@etherpad/express-session": "^1.18.2",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.3",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"esbuild": "^0.23.0",
|
"esbuild": "^0.23.0",
|
||||||
"etherpad-require-kernel": "^1.0.16",
|
|
||||||
"etherpad-yajsml": "0.0.12",
|
|
||||||
"express": "4.19.2",
|
"express": "4.19.2",
|
||||||
"express-rate-limit": "^7.3.1",
|
"express-rate-limit": "^7.4.0",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"find-root": "1.1.0",
|
"find-root": "1.1.0",
|
||||||
"formidable": "^3.5.1",
|
"formidable": "^3.5.1",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.1",
|
||||||
"jsonminify": "0.4.2",
|
"jsonminify": "0.4.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"languages4translatewiki": "0.1.3",
|
"languages4translatewiki": "0.1.3",
|
||||||
"lightningcss": "^1.25.1",
|
|
||||||
"live-plugin-manager": "^1.0.0",
|
"live-plugin-manager": "^1.0.0",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
"lru-cache": "^10.3.0",
|
"lru-cache": "^11.0.0",
|
||||||
"measured-core": "^2.0.0",
|
"measured-core": "^2.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"oidc-provider": "^8.5.1",
|
"oidc-provider": "^8.5.1",
|
||||||
|
@ -66,49 +64,51 @@
|
||||||
"rehype-minify-whitespace": "^6.0.0",
|
"rehype-minify-whitespace": "^6.0.0",
|
||||||
"resolve": "1.22.8",
|
"resolve": "1.22.8",
|
||||||
"security": "1.0.0",
|
"security": "1.0.0",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.3",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"superagent": "^9.0.2",
|
"superagent": "9.0.2",
|
||||||
"threads": "^1.7.0",
|
"threads": "^1.7.0",
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"tsx": "4.16.2",
|
"tsx": "4.17.0",
|
||||||
"ueberdb2": "^4.2.81",
|
"ueberdb2": "^4.2.92",
|
||||||
"underscore": "1.13.6",
|
"underscore": "1.13.7",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"wtfnode": "^0.9.2"
|
"wtfnode": "^0.9.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"etherpad-healthcheck": "../bin/etherpad-healthcheck",
|
"etherpad-healthcheck": "../bin/etherpad-healthcheck",
|
||||||
"etherpad-lite": "node/server.ts"
|
"etherpad-lite": "node/server.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.45.1",
|
"@playwright/test": "^1.46.0",
|
||||||
"@types/async": "^3.2.24",
|
"@types/async": "^3.2.24",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/http-errors": "^2.0.4",
|
"@types/http-errors": "^2.0.4",
|
||||||
|
"@types/jquery": "^3.5.30",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^22.1.0",
|
||||||
"@types/oidc-provider": "^8.5.1",
|
"@types/oidc-provider": "^8.5.1",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/underscore": "^1.11.15",
|
"@types/underscore": "^1.11.15",
|
||||||
"eslint": "^9.6.0",
|
"chokidar": "^3.6.0",
|
||||||
|
"eslint": "^9.8.0",
|
||||||
"eslint-config-etherpad": "^4.0.4",
|
"eslint-config-etherpad": "^4.0.4",
|
||||||
"etherpad-cli-client": "^3.0.2",
|
"etherpad-cli-client": "^3.0.2",
|
||||||
"mocha": "^10.6.0",
|
"mocha": "^10.7.0",
|
||||||
"mocha-froth": "^0.2.10",
|
"mocha-froth": "^0.2.10",
|
||||||
"nodeify": "^1.0.1",
|
"nodeify": "^1.0.1",
|
||||||
"openapi-schema-validation": "^0.4.2",
|
"openapi-schema-validation": "^0.4.2",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "^2.7.0",
|
||||||
"sinon": "^18.0.0",
|
"sinon": "^18.0.0",
|
||||||
"split-grid": "^1.0.11",
|
"split-grid": "^1.0.11",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.2",
|
"node": ">=18.18.2",
|
||||||
|
@ -121,19 +121,19 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts ../node_modules/ep_*/static/tests/backend/specs/**",
|
"test": "cross-env NODE_ENV=production mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts ../node_modules/ep_*/static/tests/backend/specs/**",
|
||||||
"test-utils": "mocha --import=tsx --timeout 5000 --recursive tests/backend/specs/*utils.ts",
|
"test-utils": "cross-env NODE_ENV=production mocha --import=tsx --timeout 5000 --recursive tests/backend/specs/*utils.ts",
|
||||||
"test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api",
|
"test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api",
|
||||||
"dev": "node --require tsx/cjs node/server.ts",
|
"dev": "cross-env NODE_ENV=development node --require tsx/cjs node/server.ts",
|
||||||
"prod": "node --require tsx/cjs node/server.ts",
|
"prod": "cross-env NODE_ENV=production node --require tsx/cjs node/server.ts",
|
||||||
"ts-check": "tsc --noEmit",
|
"ts-check": "tsc --noEmit",
|
||||||
"ts-check:watch": "tsc --noEmit --watch",
|
"ts-check:watch": "tsc --noEmit --watch",
|
||||||
"test-ui": "npx playwright test tests/frontend-new/specs",
|
"test-ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs",
|
||||||
"test-ui:ui": "npx playwright test tests/frontend-new/specs --ui",
|
"test-ui:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs --ui",
|
||||||
"test-admin": "npx playwright test tests/frontend-new/admin-spec --workers 1",
|
"test-admin": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --workers 1",
|
||||||
"test-admin:ui": "npx playwright test tests/frontend-new/admin-spec --ui --workers 1",
|
"test-admin:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --ui --workers 1",
|
||||||
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts"
|
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts"
|
||||||
},
|
},
|
||||||
"version": "2.1.1",
|
"version": "2.2.0",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default defineConfig({
|
||||||
expect: { timeout: defaultExpectTimeout },
|
expect: { timeout: defaultExpectTimeout },
|
||||||
timeout: defaultTestTimeout,
|
timeout: defaultTestTimeout,
|
||||||
retries: 2,
|
retries: 2,
|
||||||
workers: 20,
|
workers: 5,
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
|
|
@ -4,7 +4,7 @@ const AttributeMap = require('./AttributeMap');
|
||||||
const Changeset = require('./Changeset');
|
const Changeset = require('./Changeset');
|
||||||
const ChangesetUtils = require('./ChangesetUtils');
|
const ChangesetUtils = require('./ChangesetUtils');
|
||||||
const attributes = require('./attributes');
|
const attributes = require('./attributes');
|
||||||
const _ = require('./underscore');
|
const underscore = require("underscore")
|
||||||
|
|
||||||
const lineMarkerAttribute = 'lmkr';
|
const lineMarkerAttribute = 'lmkr';
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ const AttributeManager = function (rep, applyChangesetCallback) {
|
||||||
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
|
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
|
||||||
AttributeManager.lineAttributes = lineAttributes;
|
AttributeManager.lineAttributes = lineAttributes;
|
||||||
|
|
||||||
AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({
|
||||||
|
|
||||||
applyChangeset(changeset) {
|
applyChangeset(changeset) {
|
||||||
if (!this.applyChangesetCallback) return changeset;
|
if (!this.applyChangesetCallback) return changeset;
|
||||||
|
@ -335,7 +335,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
||||||
|
|
||||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||||
|
|
||||||
const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1])
|
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||||
|
|
||||||
// if we have marker and any of attributes don't need to have marker. we need delete it
|
// if we have marker and any of attributes don't need to have marker. we need delete it
|
||||||
|
|
|
@ -27,9 +27,10 @@
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||||
const pluginUtils = require('./pluginfw/shared');
|
const pluginUtils = require('./pluginfw/shared');
|
||||||
|
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
|
||||||
const debugLog = (...args) => {};
|
const debugLog = (...args) => {};
|
||||||
|
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins')
|
||||||
|
const rJQuery = require('ep_etherpad-lite/static/js/rjquery')
|
||||||
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
|
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
|
||||||
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
|
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
|
||||||
// errors out unless given an absolute URL for a JavaScript-created element.
|
// errors out unless given an absolute URL for a JavaScript-created element.
|
||||||
|
@ -257,19 +258,19 @@ const Ace2Editor = function () {
|
||||||
|
|
||||||
// <head> tag
|
// <head> tag
|
||||||
addStyleTagsFor(innerDocument, includedCSS);
|
addStyleTagsFor(innerDocument, includedCSS);
|
||||||
const requireKernel = innerDocument.createElement('script');
|
//const requireKernel = innerDocument.createElement('script');
|
||||||
requireKernel.type = 'text/javascript';
|
//requireKernel.type = 'text/javascript';
|
||||||
requireKernel.src =
|
//requireKernel.src =
|
||||||
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
// absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
||||||
innerDocument.head.appendChild(requireKernel);
|
//innerDocument.head.appendChild(requireKernel);
|
||||||
// Pre-fetch modules to improve load performance.
|
// Pre-fetch modules to improve load performance.
|
||||||
for (const module of ['ace2_inner', 'ace2_common']) {
|
/*for (const module of ['ace2_inner', 'ace2_common']) {
|
||||||
const script = innerDocument.createElement('script');
|
const script = innerDocument.createElement('script');
|
||||||
script.type = 'text/javascript';
|
script.type = 'text/javascript';
|
||||||
script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
|
script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
|
||||||
`?callback=require.define&v=${clientVars.randomVersionString}`);
|
`?callback=require.define&v=${clientVars.randomVersionString}`);
|
||||||
innerDocument.head.appendChild(script);
|
innerDocument.head.appendChild(script);
|
||||||
}
|
}*/
|
||||||
const innerStyle = innerDocument.createElement('style');
|
const innerStyle = innerDocument.createElement('style');
|
||||||
innerStyle.type = 'text/css';
|
innerStyle.type = 'text/css';
|
||||||
innerStyle.title = 'dynamicsyntax';
|
innerStyle.title = 'dynamicsyntax';
|
||||||
|
@ -284,7 +285,7 @@ const Ace2Editor = function () {
|
||||||
innerDocument.body.classList.add('innerdocbody');
|
innerDocument.body.classList.add('innerdocbody');
|
||||||
innerDocument.body.setAttribute('spellcheck', 'false');
|
innerDocument.body.setAttribute('spellcheck', 'false');
|
||||||
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //
|
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //
|
||||||
|
/*
|
||||||
debugLog('Ace2Editor.init() waiting for require kernel load');
|
debugLog('Ace2Editor.init() waiting for require kernel load');
|
||||||
await eventFired(requireKernel, 'load');
|
await eventFired(requireKernel, 'load');
|
||||||
debugLog('Ace2Editor.init() require kernel loaded');
|
debugLog('Ace2Editor.init() require kernel loaded');
|
||||||
|
@ -292,17 +293,16 @@ const Ace2Editor = function () {
|
||||||
require.setRootURI(absUrl('../javascripts/src'));
|
require.setRootURI(absUrl('../javascripts/src'));
|
||||||
require.setLibraryURI(absUrl('../javascripts/lib'));
|
require.setLibraryURI(absUrl('../javascripts/lib'));
|
||||||
require.setGlobalKeyPath('require');
|
require.setGlobalKeyPath('require');
|
||||||
|
*/
|
||||||
// intentially moved before requiring client_plugins to save a 307
|
// intentially moved before requiring client_plugins to save a 307
|
||||||
innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
|
innerWindow.Ace2Inner = ace2_inner;
|
||||||
innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
innerWindow.plugins = cl_plugins;
|
||||||
innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow);
|
|
||||||
|
|
||||||
innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
|
innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery;
|
||||||
|
|
||||||
debugLog('Ace2Editor.init() waiting for plugins');
|
debugLog('Ace2Editor.init() waiting for plugins');
|
||||||
await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
||||||
(err) => err != null ? reject(err) : resolve()));
|
(err) => err != null ? reject(err) : resolve()));*/
|
||||||
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
|
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
|
||||||
await innerWindow.Ace2Inner.init(info, {
|
await innerWindow.Ace2Inner.init(info, {
|
||||||
inner: makeCSSManager(innerStyle.sheet),
|
inner: makeCSSManager(innerStyle.sheet),
|
||||||
|
|
|
@ -30,6 +30,8 @@ const setAssoc = Ace2Common.setAssoc;
|
||||||
const noop = Ace2Common.noop;
|
const noop = Ace2Common.noop;
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
|
||||||
|
import Scroll from './scroll'
|
||||||
|
|
||||||
function Ace2Inner(editorInfo, cssManagers) {
|
function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
|
@ -42,7 +44,6 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const SkipList = require('./skiplist');
|
const SkipList = require('./skiplist');
|
||||||
const undoModule = require('./undomodule').undoModule;
|
const undoModule = require('./undomodule').undoModule;
|
||||||
const AttributeManager = require('./AttributeManager');
|
const AttributeManager = require('./AttributeManager');
|
||||||
const Scroll = require('./scroll');
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
|
||||||
const THE_TAB = ' '; // 4
|
const THE_TAB = ' '; // 4
|
||||||
|
@ -54,13 +55,16 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
let thisAuthor = '';
|
let thisAuthor = '';
|
||||||
|
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
const outerWin = document.getElementsByName("ace_outer")[0]
|
||||||
|
const targetDoc = outerWin.contentWindow.document.getElementsByName("ace_inner")[0].contentWindow.document
|
||||||
|
const targetBody = targetDoc.body
|
||||||
|
|
||||||
const focus = () => {
|
const focus = () => {
|
||||||
window.focus();
|
targetBody.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const outerWin = window.parent;
|
const outerDoc = outerWin.contentWindow.document;
|
||||||
const outerDoc = outerWin.document;
|
|
||||||
const sideDiv = outerDoc.getElementById('sidediv');
|
const sideDiv = outerDoc.getElementById('sidediv');
|
||||||
const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv');
|
const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv');
|
||||||
const sideDivInner = outerDoc.getElementById('sidedivinner');
|
const sideDivInner = outerDoc.getElementById('sidedivinner');
|
||||||
|
@ -74,7 +78,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
appendNewSideDivLine();
|
appendNewSideDivLine();
|
||||||
|
|
||||||
const scroll = Scroll.init(outerWin);
|
const scroll = new Scroll(outerWin);
|
||||||
|
|
||||||
let outsideKeyDown = noop;
|
let outsideKeyDown = noop;
|
||||||
let outsideKeyPress = (e) => true;
|
let outsideKeyPress = (e) => true;
|
||||||
|
@ -415,7 +419,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
|
|
||||||
const setWraps = (newVal) => {
|
const setWraps = (newVal) => {
|
||||||
doesWrap = newVal;
|
doesWrap = newVal;
|
||||||
document.body.classList.toggle('doesWrap', doesWrap);
|
targetBody.classList.toggle('doesWrap', doesWrap);
|
||||||
scheduler.setTimeout(() => {
|
scheduler.setTimeout(() => {
|
||||||
inCallStackIfNecessary('setWraps', () => {
|
inCallStackIfNecessary('setWraps', () => {
|
||||||
fastIncorp(7);
|
fastIncorp(7);
|
||||||
|
@ -445,7 +449,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTextFace = (face) => {
|
const setTextFace = (face) => {
|
||||||
document.body.style.fontFamily = face;
|
targetBody.style.fontFamily = face;
|
||||||
lineMetricsDiv.style.fontFamily = face;
|
lineMetricsDiv.style.fontFamily = face;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -456,8 +460,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
|
|
||||||
const setEditable = (newVal) => {
|
const setEditable = (newVal) => {
|
||||||
isEditable = newVal;
|
isEditable = newVal;
|
||||||
document.body.contentEditable = isEditable ? 'true' : 'false';
|
targetBody.contentEditable = isEditable ? 'true' : 'false';
|
||||||
document.body.classList.toggle('static', !isEditable);
|
targetBody.classList.toggle('static', !isEditable);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enforceEditability = () => setEditable(isEditable);
|
const enforceEditability = () => setEditable(isEditable);
|
||||||
|
@ -480,6 +484,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
newText = `${lines.join('\n')}\n`;
|
newText = `${lines.join('\n')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||||
setDocText(newText);
|
setDocText(newText);
|
||||||
});
|
});
|
||||||
|
@ -640,8 +645,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// These properties are exposed
|
// These properties are exposed
|
||||||
const setters = {
|
const setters = {
|
||||||
wraps: setWraps,
|
wraps: setWraps,
|
||||||
showsauthorcolors: (val) => document.body.classList.toggle('authorColors', !!val),
|
showsauthorcolors: (val) => targetBody.classList.toggle('authorColors', !!val),
|
||||||
showsuserselections: (val) => document.body.classList.toggle('userSelections', !!val),
|
showsuserselections: (val) => targetBody.classList.toggle('userSelections', !!val),
|
||||||
showslinenumbers: (value) => {
|
showslinenumbers: (value) => {
|
||||||
hasLineNumbers = !!value;
|
hasLineNumbers = !!value;
|
||||||
sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers);
|
sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers);
|
||||||
|
@ -654,8 +659,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
styled: setStyled,
|
styled: setStyled,
|
||||||
textface: setTextFace,
|
textface: setTextFace,
|
||||||
rtlistrue: (value) => {
|
rtlistrue: (value) => {
|
||||||
document.body.classList.toggle('rtl', value);
|
targetBody.classList.toggle('rtl', value);
|
||||||
document.body.classList.toggle('ltr', !value);
|
targetBody.classList.toggle('ltr', !value);
|
||||||
document.documentElement.dir = value ? 'rtl' : 'ltr';
|
document.documentElement.dir = value ? 'rtl' : 'ltr';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -894,11 +899,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
clearObservedChanges();
|
clearObservedChanges();
|
||||||
|
|
||||||
const getCleanNodeByKey = (key) => {
|
const getCleanNodeByKey = (key) => {
|
||||||
let n = document.getElementById(key);
|
let n = targetDoc.getElementById(key);
|
||||||
// copying and pasting can lead to duplicate ids
|
// copying and pasting can lead to duplicate ids
|
||||||
while (n && isNodeDirty(n)) {
|
while (n && isNodeDirty(n)) {
|
||||||
n.id = '';
|
n.id = '';
|
||||||
n = document.getElementById(key);
|
n = targetDoc.getElementById(key);
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
};
|
};
|
||||||
|
@ -980,11 +985,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const observeSuspiciousNodes = () => {
|
const observeSuspiciousNodes = () => {
|
||||||
// inspired by Firefox bug #473255, where pasting formatted text
|
// inspired by Firefox bug #473255, where pasting formatted text
|
||||||
// causes the cursor to jump away, making the new HTML never found.
|
// causes the cursor to jump away, making the new HTML never found.
|
||||||
if (document.body.getElementsByTagName) {
|
if (targetBody.getElementsByTagName) {
|
||||||
const elts = document.body.getElementsByTagName('style');
|
const elts = targetBody.getElementsByTagName('style');
|
||||||
for (const elt of elts) {
|
for (const elt of elts) {
|
||||||
const n = topLevel(elt);
|
const n = topLevel(elt);
|
||||||
if (n && n.parentNode === document.body) {
|
if (n && n.parentNode === targetBody) {
|
||||||
observeChangesAroundNode(n);
|
observeChangesAroundNode(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -999,8 +1004,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
|
if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
|
||||||
|
|
||||||
// returns true if dom changes were made
|
// returns true if dom changes were made
|
||||||
if (!document.body.firstChild) {
|
if (!targetBody.firstChild) {
|
||||||
document.body.innerHTML = '<div><!-- --></div>';
|
targetBody.innerHTML = '<div><!-- --></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
observeChangesAroundSelection();
|
observeChangesAroundSelection();
|
||||||
|
@ -1022,7 +1027,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
j++;
|
j++;
|
||||||
}
|
}
|
||||||
if (!dirtyRangesCheckOut) {
|
if (!dirtyRangesCheckOut) {
|
||||||
for (const bodyNode of document.body.childNodes) {
|
for (const bodyNode of targetBody.childNodes) {
|
||||||
if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) {
|
if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) {
|
||||||
observeChangesAroundNode(bodyNode);
|
observeChangesAroundNode(bodyNode);
|
||||||
}
|
}
|
||||||
|
@ -1044,11 +1049,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const range = dirtyRanges[i];
|
const range = dirtyRanges[i];
|
||||||
a = range[0];
|
a = range[0];
|
||||||
b = range[1];
|
b = range[1];
|
||||||
let firstDirtyNode = (((a === 0) && document.body.firstChild) ||
|
let firstDirtyNode = (((a === 0) && targetBody.firstChild) ||
|
||||||
getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
|
getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
|
||||||
firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
|
firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
|
||||||
|
|
||||||
let lastDirtyNode = (((b === rep.lines.length()) && document.body.lastChild) ||
|
let lastDirtyNode = (((b === rep.lines.length()) && targetBody.lastChild) ||
|
||||||
getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
|
getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
|
||||||
|
|
||||||
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
|
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
|
||||||
|
@ -1135,7 +1140,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
callstack: currentCallStack,
|
callstack: currentCallStack,
|
||||||
editorInfo,
|
editorInfo,
|
||||||
rep,
|
rep,
|
||||||
root: document.body,
|
root: targetBody,
|
||||||
point: selection.startPoint,
|
point: selection.startPoint,
|
||||||
documentAttributeManager,
|
documentAttributeManager,
|
||||||
});
|
});
|
||||||
|
@ -1147,7 +1152,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
callstack: currentCallStack,
|
callstack: currentCallStack,
|
||||||
editorInfo,
|
editorInfo,
|
||||||
rep,
|
rep,
|
||||||
root: document.body,
|
root: targetBody,
|
||||||
point: selection.endPoint,
|
point: selection.endPoint,
|
||||||
documentAttributeManager,
|
documentAttributeManager,
|
||||||
});
|
});
|
||||||
|
@ -1227,9 +1232,9 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
info.prepareForAdd();
|
info.prepareForAdd();
|
||||||
entry.lineMarker = info.lineMarker;
|
entry.lineMarker = info.lineMarker;
|
||||||
if (!nodeToAddAfter) {
|
if (!nodeToAddAfter) {
|
||||||
document.body.insertBefore(node, document.body.firstChild);
|
targetBody.insertBefore(node, targetBody.firstChild);
|
||||||
} else {
|
} else {
|
||||||
document.body.insertBefore(node, nodeToAddAfter.nextSibling);
|
targetBody.insertBefore(node, nodeToAddAfter.nextSibling);
|
||||||
}
|
}
|
||||||
nodeToAddAfter = node;
|
nodeToAddAfter = node;
|
||||||
info.notifyAdded();
|
info.notifyAdded();
|
||||||
|
@ -1326,7 +1331,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// Turn DOM node selection into [line,char] selection.
|
// Turn DOM node selection into [line,char] selection.
|
||||||
// This method has to work when the DOM is not pristine,
|
// This method has to work when the DOM is not pristine,
|
||||||
// assuming the point is not in a dirty node.
|
// assuming the point is not in a dirty node.
|
||||||
if (point.node === document.body) {
|
if (point.node === targetBody) {
|
||||||
if (point.index === 0) {
|
if (point.index === 0) {
|
||||||
return [0, 0];
|
return [0, 0];
|
||||||
} else {
|
} else {
|
||||||
|
@ -1345,7 +1350,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
col = nodeText(n).length;
|
col = nodeText(n).length;
|
||||||
}
|
}
|
||||||
let parNode, prevSib;
|
let parNode, prevSib;
|
||||||
while ((parNode = n.parentNode) !== document.body) {
|
while ((parNode = n.parentNode) !== targetBody) {
|
||||||
if ((prevSib = n.previousSibling)) {
|
if ((prevSib = n.previousSibling)) {
|
||||||
n = prevSib;
|
n = prevSib;
|
||||||
col += nodeText(n).length;
|
col += nodeText(n).length;
|
||||||
|
@ -1398,7 +1403,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo));
|
insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo));
|
||||||
|
|
||||||
for (const k of keysToDelete) {
|
for (const k of keysToDelete) {
|
||||||
const n = document.getElementById(k);
|
const n = targetDoc.getElementById(k);
|
||||||
n.parentNode.removeChild(n);
|
n.parentNode.removeChild(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2087,7 +2092,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const a = cleanNodeForIndex(i - 1);
|
const a = cleanNodeForIndex(i - 1);
|
||||||
const b = cleanNodeForIndex(i);
|
const b = cleanNodeForIndex(i);
|
||||||
if ((!a) || (!b)) return false; // violates precondition
|
if ((!a) || (!b)) return false; // violates precondition
|
||||||
if ((a === true) && (b === true)) return !document.body.firstChild;
|
if ((a === true) && (b === true)) return !targetBody.firstChild;
|
||||||
if ((a === true) && b.previousSibling) return false;
|
if ((a === true) && b.previousSibling) return false;
|
||||||
if ((b === true) && a.nextSibling) return false;
|
if ((b === true) && a.nextSibling) return false;
|
||||||
if ((a === true) || (b === true)) return true;
|
if ((a === true) || (b === true)) return true;
|
||||||
|
@ -2232,7 +2237,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNodeDirty = (n) => {
|
const isNodeDirty = (n) => {
|
||||||
if (n.parentNode !== document.body) return true;
|
if (n.parentNode !== targetBody) return true;
|
||||||
const data = getAssoc(n, 'dirtiness');
|
const data = getAssoc(n, 'dirtiness');
|
||||||
if (!data) return true;
|
if (!data) return true;
|
||||||
if (n.id !== data.nodeId) return true;
|
if (n.id !== data.nodeId) return true;
|
||||||
|
@ -2856,7 +2861,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
updateBrowserSelectionFromRep();
|
updateBrowserSelectionFromRep();
|
||||||
// get the current caret selection, can't use rep. here because that only gives
|
// get the current caret selection, can't use rep. here because that only gives
|
||||||
// us the start position not the current
|
// us the start position not the current
|
||||||
const myselection = document.getSelection();
|
const myselection = targetDoc.getSelection();
|
||||||
// get the carets selection offset in px IE 214
|
// get the carets selection offset in px IE 214
|
||||||
let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
|
let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
|
||||||
myselection.focusNode.offsetTop;
|
myselection.focusNode.offsetTop;
|
||||||
|
@ -2970,13 +2975,13 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// with background doesn't seem to show up...
|
// with background doesn't seem to show up...
|
||||||
if (isNodeText(p.node) && p.index === p.maxIndex) {
|
if (isNodeText(p.node) && p.index === p.maxIndex) {
|
||||||
let n = p.node;
|
let n = p.node;
|
||||||
while (!n.nextSibling && n !== document.body && n.parentNode !== document.body) {
|
while (!n.nextSibling && n !== targetBody && n.parentNode !== targetBody) {
|
||||||
n = n.parentNode;
|
n = n.parentNode;
|
||||||
}
|
}
|
||||||
if (n.nextSibling &&
|
if (n.nextSibling &&
|
||||||
!(typeof n.nextSibling.tagName === 'string' &&
|
!(typeof n.nextSibling.tagName === 'string' &&
|
||||||
n.nextSibling.tagName.toLowerCase() === 'br') &&
|
n.nextSibling.tagName.toLowerCase() === 'br') &&
|
||||||
n !== p.node && n !== document.body && n.parentNode !== document.body) {
|
n !== p.node && n !== targetBody && n.parentNode !== targetBody) {
|
||||||
// found a parent, go to next node and dive in
|
// found a parent, go to next node and dive in
|
||||||
p.node = n.nextSibling;
|
p.node = n.nextSibling;
|
||||||
p.maxIndex = nodeMaxIndex(p.node);
|
p.maxIndex = nodeMaxIndex(p.node);
|
||||||
|
@ -3003,7 +3008,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const browserSelection = window.getSelection();
|
const browserSelection = targetDoc.getSelection();
|
||||||
if (browserSelection) {
|
if (browserSelection) {
|
||||||
browserSelection.removeAllRanges();
|
browserSelection.removeAllRanges();
|
||||||
if (selection) {
|
if (selection) {
|
||||||
|
@ -3078,7 +3083,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// each of which has node (a magicdom node), index, and maxIndex. If the node
|
// each of which has node (a magicdom node), index, and maxIndex. If the node
|
||||||
// is a text node, maxIndex is the length of the text; else maxIndex is 1.
|
// is a text node, maxIndex is the length of the text; else maxIndex is 1.
|
||||||
// index is between 0 and maxIndex, inclusive.
|
// index is between 0 and maxIndex, inclusive.
|
||||||
const browserSelection = window.getSelection();
|
const browserSelection = targetDoc.getSelection();
|
||||||
if (!browserSelection || browserSelection.type === 'None' ||
|
if (!browserSelection || browserSelection.type === 'None' ||
|
||||||
browserSelection.rangeCount === 0) {
|
browserSelection.rangeCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -3096,7 +3101,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (!isInBody(container)) {
|
if (!isInBody(container)) {
|
||||||
// command-click in Firefox selects whole document, HEAD and BODY!
|
// command-click in Firefox selects whole document, HEAD and BODY!
|
||||||
return {
|
return {
|
||||||
node: document.body,
|
node: targetBody,
|
||||||
index: 0,
|
index: 0,
|
||||||
maxIndex: 1,
|
maxIndex: 1,
|
||||||
};
|
};
|
||||||
|
@ -3146,7 +3151,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
browserSelection.anchorOffset === range.endOffset,
|
browserSelection.anchorOffset === range.endOffset,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selection.startPoint.node.ownerDocument !== window.document) {
|
if (selection.startPoint.node.ownerDocument !== targetDoc) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3181,17 +3186,17 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
editorInfo.ace_getInInternationalComposition = () => inInternationalComposition;
|
editorInfo.ace_getInInternationalComposition = () => inInternationalComposition;
|
||||||
|
|
||||||
const bindTheEventHandlers = () => {
|
const bindTheEventHandlers = () => {
|
||||||
$(document).on('keydown', handleKeyEvent);
|
$(targetDoc).on('keydown', handleKeyEvent);
|
||||||
$(document).on('keypress', handleKeyEvent);
|
$(targetDoc).on('keypress', handleKeyEvent);
|
||||||
$(document).on('keyup', handleKeyEvent);
|
$(targetDoc).on('keyup', handleKeyEvent);
|
||||||
$(document).on('click', handleClick);
|
$(targetDoc).on('click', handleClick);
|
||||||
// dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer
|
// dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer
|
||||||
$(outerDoc).on('click', hideEditBarDropdowns);
|
$(outerDoc).on('click', hideEditBarDropdowns);
|
||||||
|
|
||||||
// If non-nullish, pasting on a link should be suppressed.
|
// If non-nullish, pasting on a link should be suppressed.
|
||||||
let suppressPasteOnLink = null;
|
let suppressPasteOnLink = null;
|
||||||
|
|
||||||
$(document.body).on('auxclick', (e) => {
|
$(targetBody).on('auxclick', (e) => {
|
||||||
if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) {
|
if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) {
|
||||||
// The user middle-clicked on a link. Usually users do this to open a link in a new tab, but
|
// The user middle-clicked on a link. Usually users do this to open a link in a new tab, but
|
||||||
// in X11 (Linux) this will instead paste the contents of the primary selection at the mouse
|
// in X11 (Linux) this will instead paste the contents of the primary selection at the mouse
|
||||||
|
@ -3213,7 +3218,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document.body).on('paste', (e) => {
|
$(targetBody).on('paste', (e) => {
|
||||||
if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) {
|
if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) {
|
||||||
scheduler.clearTimeout(suppressPasteOnLink);
|
scheduler.clearTimeout(suppressPasteOnLink);
|
||||||
suppressPasteOnLink = null;
|
suppressPasteOnLink = null;
|
||||||
|
@ -3233,7 +3238,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// We reference document here, this is because if we don't this will expose a bug
|
// We reference document here, this is because if we don't this will expose a bug
|
||||||
// in Google Chrome. This bug will cause the last character on the last line to
|
// in Google Chrome. This bug will cause the last character on the last line to
|
||||||
// not fire an event when dropped into..
|
// not fire an event when dropped into..
|
||||||
$(document).on('drop', (e) => {
|
$(targetBody).on('drop', (e) => {
|
||||||
if (e.target.a || e.target.localName === 'a') {
|
if (e.target.a || e.target.localName === 'a') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -3251,7 +3256,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const lineAfterSelection = lastLineSelected.nextSibling;
|
const lineAfterSelection = lastLineSelected.nextSibling;
|
||||||
|
|
||||||
const neighbor = lineBeforeSelection || lineAfterSelection;
|
const neighbor = lineBeforeSelection || lineAfterSelection;
|
||||||
neighbor.appendChild(document.createElement('style'));
|
neighbor.appendChild(targetDoc.createElement('style'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call drop hook
|
// Call drop hook
|
||||||
|
@ -3263,10 +3268,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document.documentElement).on('compositionstart', () => {
|
$(targetDoc.documentElement).on('compositionstart', () => {
|
||||||
if (inInternationalComposition) return;
|
if (inInternationalComposition) return;
|
||||||
inInternationalComposition = new Promise((resolve) => {
|
inInternationalComposition = new Promise((resolve) => {
|
||||||
$(document.documentElement).one('compositionend', () => {
|
$(targetDoc.documentElement).one('compositionend', () => {
|
||||||
inInternationalComposition = null;
|
inInternationalComposition = null;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
@ -3275,8 +3280,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const topLevel = (n) => {
|
const topLevel = (n) => {
|
||||||
if ((!n) || n === document.body) return null;
|
if ((!n) || n === targetBody) return null;
|
||||||
while (n.parentNode !== document.body) {
|
while (n.parentNode !== targetBody) {
|
||||||
n = n.parentNode;
|
n = n.parentNode;
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
|
@ -3436,10 +3441,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// but as it's non-text type the line-height/margins might not be present and it
|
// but as it's non-text type the line-height/margins might not be present and it
|
||||||
// could be that this breaks a theme that has a different default line height..
|
// could be that this breaks a theme that has a different default line height..
|
||||||
// So instead of using an integer here we get the value from the Editor CSS.
|
// So instead of using an integer here we get the value from the Editor CSS.
|
||||||
const innerdocbodyStyles = getComputedStyle(document.body);
|
const innerdocbodyStyles = getComputedStyle(targetBody);
|
||||||
const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']);
|
const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']);
|
||||||
|
|
||||||
for (const docLine of document.body.children) {
|
for (const docLine of targetBody.children) {
|
||||||
let h;
|
let h;
|
||||||
const nextDocLine = docLine.nextElementSibling;
|
const nextDocLine = docLine.nextElementSibling;
|
||||||
if (nextDocLine) {
|
if (nextDocLine) {
|
||||||
|
@ -3450,7 +3455,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// included on the first line. The default stylesheet doesn't add
|
// included on the first line. The default stylesheet doesn't add
|
||||||
// extra margins/padding, but plugins might.
|
// extra margins/padding, but plugins might.
|
||||||
h = nextDocLine.offsetTop - parseInt(
|
h = nextDocLine.offsetTop - parseInt(
|
||||||
window.getComputedStyle(document.body)
|
window.getComputedStyle(targetBody)
|
||||||
.getPropertyValue('padding-top').split('px')[0]);
|
.getPropertyValue('padding-top').split('px')[0]);
|
||||||
} else {
|
} else {
|
||||||
h = nextDocLine.offsetTop - docLine.offsetTop;
|
h = nextDocLine.offsetTop - docLine.offsetTop;
|
||||||
|
@ -3496,15 +3501,15 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
this.init = async () => {
|
this.init = async () => {
|
||||||
await $.ready;
|
await $.ready;
|
||||||
inCallStack('setup', () => {
|
inCallStack('setup', () => {
|
||||||
if (browser.firefox) $(document.body).addClass('mozilla');
|
if (browser.firefox) $(targetBody).addClass('mozilla');
|
||||||
if (browser.safari) $(document.body).addClass('safari');
|
if (browser.safari) $(targetBody).addClass('safari');
|
||||||
document.body.classList.toggle('authorColors', true);
|
targetBody.classList.toggle('authorColors', true);
|
||||||
document.body.classList.toggle('doesWrap', doesWrap);
|
targetBody.classList.toggle('doesWrap', doesWrap);
|
||||||
|
|
||||||
enforceEditability();
|
enforceEditability();
|
||||||
|
|
||||||
// set up dom and rep
|
// set up dom and rep
|
||||||
while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
|
while (targetBody.firstChild) targetBody.removeChild(targetBody.firstChild);
|
||||||
const oneEntry = createDomLineEntry('');
|
const oneEntry = createDomLineEntry('');
|
||||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||||
insertDomLines(null, [oneEntry.domInfo]);
|
insertDomLines(null, [oneEntry.domInfo]);
|
||||||
|
|
|
@ -32,6 +32,9 @@ const colorutils = require('./colorutils').colorutils;
|
||||||
const _ = require('./underscore');
|
const _ = require('./underscore');
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
|
|
||||||
|
|
||||||
// These parameters were global, now they are injected. A reference to the
|
// These parameters were global, now they are injected. A reference to the
|
||||||
// Timeslider controller would probably be more appropriate.
|
// Timeslider controller would probably be more appropriate.
|
||||||
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {
|
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
const _ = require('./underscore');
|
const _ = require('./underscore');
|
||||||
const padmodals = require('./pad_modals').padmodals;
|
const padmodals = require('./pad_modals').padmodals;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
|
|
||||||
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||||
let BroadcastSlider;
|
let BroadcastSlider;
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
// One rep.line(div) can be broken in more than one line in the browser.
|
// One rep.line(div) can be broken in more than one line in the browser.
|
||||||
// This function is useful to get the caret position of the line as
|
// This function is useful to get the caret position of the line as
|
||||||
// is represented by the browser
|
// is represented by the browser
|
||||||
exports.getPosition = () => {
|
import {Position, RepModel, RepNode} from "./types/RepModel";
|
||||||
|
|
||||||
|
export const getPosition = () => {
|
||||||
const range = getSelectionRange();
|
const range = getSelectionRange();
|
||||||
|
// @ts-ignore
|
||||||
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
|
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
|
||||||
// When there's a <br> or any element that has no height, we can't get the dimension of the
|
// When there's a <br> or any element that has no height, we can't get the dimension of the
|
||||||
// element where the caret is. As we can't get the element height, we create a text node to get
|
// element where the caret is. As we can't get the element height, we create a text node to get
|
||||||
|
@ -18,7 +21,7 @@ exports.getPosition = () => {
|
||||||
return line;
|
return line;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSelectionRange = (range) => {
|
const createSelectionRange = (range: Range) => {
|
||||||
const clonedRange = range.cloneRange();
|
const clonedRange = range.cloneRange();
|
||||||
|
|
||||||
// we set the selection start and end to avoid error when user selects a text bigger than
|
// we set the selection start and end to avoid error when user selects a text bigger than
|
||||||
|
@ -30,14 +33,14 @@ const createSelectionRange = (range) => {
|
||||||
return clonedRange;
|
return clonedRange;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPositionOfRepLineAtOffset = (node, offset) => {
|
const getPositionOfRepLineAtOffset = (node: any, offset: number) => {
|
||||||
// it is not a text node, so we cannot make a selection
|
// it is not a text node, so we cannot make a selection
|
||||||
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
|
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
|
||||||
return getPositionOfElementOrSelection(node);
|
return getPositionOfElementOrSelection(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (node.length === 0 && node.nextSibling) {
|
while (node.length === 0 && node.nextSibling) {
|
||||||
node = node.nextSibling;
|
node = node.nextSibling as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRange = new Range();
|
const newRange = new Range();
|
||||||
|
@ -48,14 +51,13 @@ const getPositionOfRepLineAtOffset = (node, offset) => {
|
||||||
return linePosition;
|
return linePosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPositionOfElementOrSelection = (element) => {
|
const getPositionOfElementOrSelection = (element: Range):Position => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const linePosition = {
|
return {
|
||||||
bottom: rect.bottom,
|
bottom: rect.bottom,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
top: rect.top,
|
top: rect.top,
|
||||||
};
|
} satisfies Position;
|
||||||
return linePosition;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// here we have two possibilities:
|
// here we have two possibilities:
|
||||||
|
@ -64,7 +66,7 @@ const getPositionOfElementOrSelection = (element) => {
|
||||||
// where is the top of the previous line
|
// where is the top of the previous line
|
||||||
// [2] the line before is part of another rep line. It's possible this line has different margins
|
// [2] the line before is part of another rep line. It's possible this line has different margins
|
||||||
// height. So we have to get the exactly position of the line
|
// height. So we have to get the exactly position of the line
|
||||||
exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
|
export const getPositionTopOfPreviousBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
|
||||||
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
|
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
|
||||||
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
|
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
|
||||||
|
|
||||||
|
@ -80,7 +82,7 @@ exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
|
||||||
return previousLineTop;
|
return previousLineTop;
|
||||||
};
|
};
|
||||||
|
|
||||||
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
|
const caretLineIsFirstBrowserLine = (caretLineTop: number, rep: RepModel) => {
|
||||||
const caretRepLine = rep.selStart[0];
|
const caretRepLine = rep.selStart[0];
|
||||||
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||||
const firstRootNode = getFirstRootChildNode(lineNode);
|
const firstRootNode = getFirstRootChildNode(lineNode);
|
||||||
|
@ -91,7 +93,7 @@ const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// find the first root node, usually it is a text node
|
// find the first root node, usually it is a text node
|
||||||
const getFirstRootChildNode = (node) => {
|
const getFirstRootChildNode = (node: RepNode) => {
|
||||||
if (!node.firstChild) {
|
if (!node.firstChild) {
|
||||||
return node;
|
return node;
|
||||||
} else {
|
} else {
|
||||||
|
@ -99,7 +101,7 @@ const getFirstRootChildNode = (node) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
|
const getDimensionOfLastBrowserLineOfRepLine = (line: number, rep: RepModel) => {
|
||||||
const lineNode = rep.lines.atIndex(line).lineNode;
|
const lineNode = rep.lines.atIndex(line).lineNode;
|
||||||
const lastRootChildNode = getLastRootChildNode(lineNode);
|
const lastRootChildNode = getLastRootChildNode(lineNode);
|
||||||
|
|
||||||
|
@ -109,7 +111,7 @@ const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
|
||||||
return lastRootChildNodePosition;
|
return lastRootChildNodePosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLastRootChildNode = (node) => {
|
const getLastRootChildNode = (node: RepNode) => {
|
||||||
if (!node.lastChild) {
|
if (!node.lastChild) {
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
@ -125,7 +127,7 @@ const getLastRootChildNode = (node) => {
|
||||||
// So, we can use the caret line to calculate the bottom of the line.
|
// So, we can use the caret line to calculate the bottom of the line.
|
||||||
// [2] the next line is part of another rep line.
|
// [2] the next line is part of another rep line.
|
||||||
// It's possible this line has different dimensions, so we have to get the exactly dimension of it
|
// It's possible this line has different dimensions, so we have to get the exactly dimension of it
|
||||||
exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
|
export const getBottomOfNextBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
|
||||||
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
|
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
|
||||||
const isCaretLineLastBrowserLine =
|
const isCaretLineLastBrowserLine =
|
||||||
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
|
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
|
||||||
|
@ -142,7 +144,7 @@ exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
|
||||||
return nextLineBottom;
|
return nextLineBottom;
|
||||||
};
|
};
|
||||||
|
|
||||||
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
|
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop: number, rep: RepModel) => {
|
||||||
const caretRepLine = rep.selStart[0];
|
const caretRepLine = rep.selStart[0];
|
||||||
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||||
const lastRootChildNode = getLastRootChildNode(lineNode);
|
const lastRootChildNode = getLastRootChildNode(lineNode);
|
||||||
|
@ -153,7 +155,7 @@ const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
|
||||||
return lastRootChildNodePosition.top === caretLineTop;
|
return lastRootChildNodePosition.top === caretLineTop;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPreviousVisibleLine = (line, rep) => {
|
export const getPreviousVisibleLine = (line: number, rep: RepModel): number => {
|
||||||
const firstLineOfPad = 0;
|
const firstLineOfPad = 0;
|
||||||
if (line <= firstLineOfPad) {
|
if (line <= firstLineOfPad) {
|
||||||
return firstLineOfPad;
|
return firstLineOfPad;
|
||||||
|
@ -165,9 +167,8 @@ const getPreviousVisibleLine = (line, rep) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.getPreviousVisibleLine = getPreviousVisibleLine;
|
|
||||||
|
|
||||||
const getNextVisibleLine = (line, rep) => {
|
export const getNextVisibleLine = (line: number, rep: RepModel): number => {
|
||||||
const lastLineOfThePad = rep.lines.length() - 1;
|
const lastLineOfThePad = rep.lines.length() - 1;
|
||||||
if (line >= lastLineOfThePad) {
|
if (line >= lastLineOfThePad) {
|
||||||
return lastLineOfThePad;
|
return lastLineOfThePad;
|
||||||
|
@ -177,11 +178,10 @@ const getNextVisibleLine = (line, rep) => {
|
||||||
return getNextVisibleLine(line + 1, rep);
|
return getNextVisibleLine(line + 1, rep);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
exports.getNextVisibleLine = getNextVisibleLine;
|
|
||||||
|
|
||||||
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
|
const isLineVisible = (line: number, rep: RepModel) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
|
||||||
|
|
||||||
const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => {
|
const getDimensionOfFirstBrowserLineOfRepLine = (line: number, rep: RepModel) => {
|
||||||
const lineNode = rep.lines.atIndex(line).lineNode;
|
const lineNode = rep.lines.atIndex(line).lineNode;
|
||||||
const firstRootChildNode = getFirstRootChildNode(lineNode);
|
const firstRootChildNode = getFirstRootChildNode(lineNode);
|
||||||
|
|
|
@ -21,10 +21,12 @@ const padcookie = require('./pad_cookie').padcookie;
|
||||||
const Tinycon = require('tinycon/tinycon');
|
const Tinycon = require('tinycon/tinycon');
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
const padeditor = require('./pad_editor').padeditor;
|
const padeditor = require('./pad_editor').padeditor;
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
|
|
||||||
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
|
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
|
||||||
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||||
|
|
||||||
|
|
||||||
exports.chat = (() => {
|
exports.chat = (() => {
|
||||||
let isStuck = false;
|
let isStuck = false;
|
||||||
let userAndChat = false;
|
let userAndChat = false;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
const randomPadName = () => {
|
const randomPadName = () => {
|
||||||
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
|
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
|
||||||
// using the PRNG below
|
// using the PRNG below
|
||||||
|
@ -28,8 +29,7 @@ const randomPadName = () => {
|
||||||
// make room for 8-bit integer values that span from 0 to 255.
|
// make room for 8-bit integer values that span from 0 to 255.
|
||||||
const randomarray = new Uint8Array(stringLength);
|
const randomarray = new Uint8Array(stringLength);
|
||||||
// use browser's PRNG to generate a "unique" sequence
|
// use browser's PRNG to generate a "unique" sequence
|
||||||
const cryptoObj = window.crypto || window.msCrypto; // for IE 11
|
crypto.getRandomValues(randomarray);
|
||||||
cryptoObj.getRandomValues(randomarray);
|
|
||||||
let randomstring = '';
|
let randomstring = '';
|
||||||
for (let i = 0; i < stringLength; i++) {
|
for (let i = 0; i < stringLength; i++) {
|
||||||
// instead of writing "Math.floor(randomarray[i]/256*64)"
|
// instead of writing "Math.floor(randomarray[i]/256*64)"
|
||||||
|
@ -42,9 +42,9 @@ const randomPadName = () => {
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
$('#go2Name').on('submit', () => {
|
$('#go2Name').on('submit', () => {
|
||||||
const padname = $('#padname').val();
|
const padname = $('#padname').val() as string;
|
||||||
if (padname.length > 0) {
|
if (padname.length > 0) {
|
||||||
window.location = `p/${encodeURIComponent(padname.trim())}`;
|
window.location.href = `p/${encodeURIComponent(padname.trim())}`;
|
||||||
} else {
|
} else {
|
||||||
alert('Please enter a name');
|
alert('Please enter a name');
|
||||||
}
|
}
|
||||||
|
@ -52,10 +52,11 @@ $(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#button').on('click', () => {
|
$('#button').on('click', () => {
|
||||||
window.location = `p/${randomPadName()}`;
|
window.location.href = `p/${randomPadName()}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// start the custom js
|
// start the custom js
|
||||||
|
// @ts-ignore
|
||||||
if (typeof window.customStart === 'function') window.customStart();
|
if (typeof window.customStart === 'function') window.customStart();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
let socket;
|
||||||
|
|
||||||
|
|
||||||
// These jQuery things should create local references, but for now `require()`
|
// These jQuery things should create local references, but for now `require()`
|
||||||
// assigns to the global `$` and augments it with plugins.
|
// assigns to the global `$` and augments it with plugins.
|
||||||
require('./vendors/jquery');
|
require('./vendors/jquery');
|
||||||
require('./vendors/farbtastic');
|
require('./vendors/farbtastic');
|
||||||
require('./vendors/gritter');
|
require('./vendors/gritter');
|
||||||
|
|
||||||
|
import html10n from './vendors/html10n'
|
||||||
|
|
||||||
const Cookies = require('./pad_utils').Cookies;
|
const Cookies = require('./pad_utils').Cookies;
|
||||||
const chat = require('./chat').chat;
|
const chat = require('./chat').chat;
|
||||||
const getCollabClient = require('./collab_client').getCollabClient;
|
const getCollabClient = require('./collab_client').getCollabClient;
|
||||||
|
@ -136,7 +139,8 @@ const getParameters = [
|
||||||
name: 'lang',
|
name: 'lang',
|
||||||
checkVal: null,
|
checkVal: null,
|
||||||
callback: (val) => {
|
callback: (val) => {
|
||||||
window.html10n.localize([val, 'en']);
|
console.log('Val is', val)
|
||||||
|
html10n.localize([val, 'en']);
|
||||||
Cookies.set('language', val);
|
Cookies.set('language', val);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -281,6 +285,7 @@ const handshake = async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
socket.on('error', (error) => {
|
socket.on('error', (error) => {
|
||||||
// pad.collabClient might be null if the error occurred before the hanshake completed.
|
// pad.collabClient might be null if the error occurred before the hanshake completed.
|
||||||
if (pad.collabClient != null) {
|
if (pad.collabClient != null) {
|
||||||
|
@ -313,6 +318,15 @@ const handshake = async () => {
|
||||||
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
|
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
|
||||||
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
||||||
}
|
}
|
||||||
|
if(window.clientVars.mode === "development") {
|
||||||
|
console.warn('Enabling development mode with live update')
|
||||||
|
socket.on('liveupdate', ()=>{
|
||||||
|
|
||||||
|
console.log('Live reload update received')
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
} else if (obj.disconnect) {
|
} else if (obj.disconnect) {
|
||||||
padconnectionstatus.disconnected(obj.disconnect);
|
padconnectionstatus.disconnected(obj.disconnect);
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
|
@ -713,7 +727,7 @@ const pad = {
|
||||||
$.ajax(
|
$.ajax(
|
||||||
{
|
{
|
||||||
type: 'post',
|
type: 'post',
|
||||||
url: 'ep/pad/connection-diagnostic-info',
|
url: '../ep/pad/connection-diagnostic-info',
|
||||||
data: {
|
data: {
|
||||||
diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
|
diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
|
|
||||||
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
|
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
|
||||||
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
|
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
|
||||||
|
|
|
@ -24,9 +24,10 @@
|
||||||
const Cookies = require('./pad_utils').Cookies;
|
const Cookies = require('./pad_utils').Cookies;
|
||||||
const padcookie = require('./pad_cookie').padcookie;
|
const padcookie = require('./pad_cookie').padcookie;
|
||||||
const padutils = require('./pad_utils').padutils;
|
const padutils = require('./pad_utils').padutils;
|
||||||
|
const Ace2Editor = require('./ace').Ace2Editor;
|
||||||
|
import html10n from '../js/vendors/html10n'
|
||||||
|
|
||||||
const padeditor = (() => {
|
const padeditor = (() => {
|
||||||
let Ace2Editor = undefined;
|
|
||||||
let pad = undefined;
|
let pad = undefined;
|
||||||
let settings = undefined;
|
let settings = undefined;
|
||||||
|
|
||||||
|
@ -35,7 +36,6 @@ const padeditor = (() => {
|
||||||
// this is accessed directly from other files
|
// this is accessed directly from other files
|
||||||
viewZoom: 100,
|
viewZoom: 100,
|
||||||
init: async (initialViewOptions, _pad) => {
|
init: async (initialViewOptions, _pad) => {
|
||||||
Ace2Editor = require('./ace').Ace2Editor;
|
|
||||||
pad = _pad;
|
pad = _pad;
|
||||||
settings = pad.settings;
|
settings = pad.settings;
|
||||||
self.ace = new Ace2Editor();
|
self.ace = new Ace2Editor();
|
||||||
|
@ -99,7 +99,7 @@ const padeditor = (() => {
|
||||||
$('#languagemenu').val(html10n.getLanguage());
|
$('#languagemenu').val(html10n.getLanguage());
|
||||||
$('#languagemenu').on('change', () => {
|
$('#languagemenu').on('change', () => {
|
||||||
Cookies.set('language', $('#languagemenu').val());
|
Cookies.set('language', $('#languagemenu').val());
|
||||||
window.html10n.localize([$('#languagemenu').val(), 'en']);
|
html10n.localize([$('#languagemenu').val(), 'en']);
|
||||||
if ($('select').niceSelect) {
|
if ($('select').niceSelect) {
|
||||||
$('select').niceSelect('update');
|
$('select').niceSelect('update');
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
|
|
||||||
|
|
||||||
const padimpexp = (() => {
|
const padimpexp = (() => {
|
||||||
let pad;
|
let pad;
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ let pad;
|
||||||
|
|
||||||
exports.saveNow = () => {
|
exports.saveNow = () => {
|
||||||
pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
|
pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
|
||||||
$.gritter.add({
|
window.$.gritter.add({
|
||||||
// (string | mandatory) the heading of the notification
|
// (string | mandatory) the heading of the notification
|
||||||
title: html10n.get('pad.savedrevs.marked'),
|
title: html10n.get('pad.savedrevs.marked'),
|
||||||
// (string | mandatory) the text inside the notification
|
// (string | mandatory) the text inside the notification
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
const padutils = require('./pad_utils').padutils;
|
const padutils = require('./pad_utils').padutils;
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
let myUserInfo = {};
|
let myUserInfo = {};
|
||||||
|
|
||||||
let colorPickerOpen = false;
|
let colorPickerOpen = false;
|
||||||
|
|
|
@ -356,7 +356,6 @@ const padutils = {
|
||||||
let globalExceptionHandler = null;
|
let globalExceptionHandler = null;
|
||||||
padutils.setupGlobalExceptionHandler = () => {
|
padutils.setupGlobalExceptionHandler = () => {
|
||||||
if (globalExceptionHandler == null) {
|
if (globalExceptionHandler == null) {
|
||||||
require('./vendors/gritter');
|
|
||||||
globalExceptionHandler = (e) => {
|
globalExceptionHandler = (e) => {
|
||||||
let type;
|
let type;
|
||||||
let err;
|
let err;
|
||||||
|
@ -443,7 +442,7 @@ const inThirdPartyIframe = () => {
|
||||||
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
||||||
// window object.
|
// window object.
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({
|
exports.Cookies = require('js-cookie').withAttributes({
|
||||||
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
||||||
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
||||||
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
||||||
|
|
|
@ -7,24 +7,13 @@ exports.baseURL = '';
|
||||||
|
|
||||||
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
|
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
|
||||||
|
|
||||||
exports.update = (cb) => {
|
exports.update = async (modules) => {
|
||||||
// It appears that this response (see #620) may interrupt the current thread
|
const data = await jQuery.getJSON(
|
||||||
// of execution on Firefox. This schedules the response in the run-loop,
|
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`);
|
||||||
// which appears to fix the issue.
|
defs.plugins = data.plugins;
|
||||||
const callback = () => setTimeout(cb, 0);
|
defs.parts = data.parts;
|
||||||
|
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks', null, modules);
|
||||||
jQuery.getJSON(
|
defs.loaded = true;
|
||||||
`${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();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const adoptPluginsFromAncestorsOf = (frame) => {
|
const adoptPluginsFromAncestorsOf = (frame) => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ const disabledHookReasons = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFn = (path, hookName) => {
|
const loadFn = (path, hookName, modules) => {
|
||||||
let functionName;
|
let functionName;
|
||||||
const parts = path.split(':');
|
const parts = path.split(':');
|
||||||
|
|
||||||
|
@ -24,7 +24,13 @@ const loadFn = (path, hookName) => {
|
||||||
functionName = parts[1];
|
functionName = parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
let fn = require(path);
|
let fn
|
||||||
|
if (modules === undefined || !("get" in modules)) {
|
||||||
|
fn = require(/* webpackIgnore: true */ path);
|
||||||
|
} else {
|
||||||
|
fn = modules.get(path);
|
||||||
|
}
|
||||||
|
|
||||||
functionName = functionName ? functionName : hookName;
|
functionName = functionName ? functionName : hookName;
|
||||||
|
|
||||||
for (const name of functionName.split('.')) {
|
for (const name of functionName.split('.')) {
|
||||||
|
@ -33,7 +39,7 @@ const loadFn = (path, hookName) => {
|
||||||
return fn;
|
return fn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractHooks = (parts, hookSetName, normalizer) => {
|
const extractHooks = (parts, hookSetName, normalizer, modules) => {
|
||||||
const hooks = {};
|
const hooks = {};
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
|
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||||
|
@ -53,7 +59,7 @@ const extractHooks = (parts, hookSetName, normalizer) => {
|
||||||
}
|
}
|
||||||
let hookFn;
|
let hookFn;
|
||||||
try {
|
try {
|
||||||
hookFn = loadFn(hookFnName, hookName);
|
hookFn = loadFn(hookFnName, hookName, modules);
|
||||||
if (!hookFn) throw new Error('Not a function');
|
if (!hookFn) throw new Error('Not a function');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
||||||
|
|
|
@ -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.
|
* Creates a socket.io connection.
|
||||||
|
|
|
@ -31,7 +31,7 @@ const randomString = require('./pad_utils').randomString;
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
const padutils = require('./pad_utils').padutils;
|
const padutils = require('./pad_utils').padutils;
|
||||||
const socketio = require('./socketio');
|
const socketio = require('./socketio');
|
||||||
|
import html10n from '../js/vendors/html10n'
|
||||||
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
|
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
|
@ -117,6 +117,14 @@ const handleClientVars = (message) => {
|
||||||
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(window.clientVars.mode === "development") {
|
||||||
|
console.warn('Enabling development mode with live update')
|
||||||
|
socket.on('liveupdate', ()=>{
|
||||||
|
console.log('Doing live reload')
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// load all script that doesn't work without the clientVars
|
// load all script that doesn't work without the clientVars
|
||||||
BroadcastSlider = require('./broadcast_slider')
|
BroadcastSlider = require('./broadcast_slider')
|
||||||
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
|
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
|
||||||
|
|
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:
|
// Licensed under the terms of the GNU General Public License v2.0:
|
||||||
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
|
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
|
||||||
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
|
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
|
||||||
|
|
||||||
(function ($) {
|
(function ($) {
|
||||||
|
|
||||||
var __debug = false;
|
var __debug = false;
|
||||||
|
@ -172,7 +173,7 @@ $._farbtastic = function (container, options) {
|
||||||
angle2 = d2 * Math.PI * 2,
|
angle2 = d2 * Math.PI * 2,
|
||||||
// Endpoints
|
// Endpoints
|
||||||
x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
|
x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
|
||||||
x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
|
let x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
|
||||||
// Midpoint chosen so that the endpoints are tangent to the circle.
|
// Midpoint chosen so that the endpoints are tangent to the circle.
|
||||||
am = (angle1 + angle2) / 2,
|
am = (angle1 + angle2) / 2,
|
||||||
tan = 1 / Math.cos((angle2 - angle1) / 2),
|
tan = 1 / Math.cos((angle2 - angle1) / 2),
|
||||||
|
@ -329,8 +330,8 @@ $._farbtastic = function (container, options) {
|
||||||
|
|
||||||
// Update the overlay canvas.
|
// Update the overlay canvas.
|
||||||
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
|
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
|
||||||
for (i in circles) {
|
for (let i in circles) {
|
||||||
var c = circles[i];
|
const c = circles[i];
|
||||||
fb.ctxOverlay.lineWidth = c.lw;
|
fb.ctxOverlay.lineWidth = c.lw;
|
||||||
fb.ctxOverlay.strokeStyle = c.c;
|
fb.ctxOverlay.strokeStyle = c.c;
|
||||||
fb.ctxOverlay.beginPath();
|
fb.ctxOverlay.beginPath();
|
||||||
|
|
6
src/static/js/vendors/gritter.js
vendored
6
src/static/js/vendors/gritter.js
vendored
|
@ -42,8 +42,8 @@
|
||||||
return Gritter.add(params || {});
|
return Gritter.add(params || {});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
||||||
var err = 'Gritter Error: ' + e;
|
const err = 'Gritter Error: ' + e;
|
||||||
(typeof(console) != 'undefined' && console.error) ?
|
(typeof(console) != 'undefined' && console.error) ?
|
||||||
console.error(err, params) :
|
console.error(err, params) :
|
||||||
alert(err);
|
alert(err);
|
||||||
|
|
||||||
|
@ -289,7 +289,7 @@
|
||||||
*/
|
*/
|
||||||
_runSetup: function(){
|
_runSetup: function(){
|
||||||
|
|
||||||
for(opt in $.gritter.options){
|
for(let opt in $.gritter.options){
|
||||||
this[opt] = $.gritter.options[opt];
|
this[opt] = $.gritter.options[opt];
|
||||||
}
|
}
|
||||||
this._is_setup = 1;
|
this._is_setup = 1;
|
||||||
|
|
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
997
src/static/js/vendors/html10n.ts
vendored
Normal file
997
src/static/js/vendors/html10n.ts
vendored
Normal file
|
@ -0,0 +1,997 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const html10n = new Html10n()
|
||||||
|
export default html10n
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.html10n = html10n
|
19158
src/static/js/vendors/jquery.js
vendored
19158
src/static/js/vendors/jquery.js
vendored
File diff suppressed because it is too large
Load diff
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');
|
$dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px');
|
||||||
}
|
}
|
||||||
|
|
||||||
$listHeight = $dropdown.find('.list').outerHeight();
|
let $listHeight = $dropdown.find('.list').outerHeight();
|
||||||
$top = $dropdown.parent().offset().top;
|
let $top = $dropdown.parent().offset().top;
|
||||||
$bottom = $('body').height() - $top;
|
let $bottom = $('body').height() - $top;
|
||||||
$maxListHeight = $bottom - $dropdown.outerHeight() - 20;
|
let $maxListHeight = $bottom - $dropdown.outerHeight() - 20;
|
||||||
if ($maxListHeight < 200) {
|
if ($maxListHeight < 200) {
|
||||||
$dropdown.addClass('reverse');
|
$dropdown.addClass('reverse');
|
||||||
$maxListHeight = 250;
|
$maxListHeight = 250;
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
<%
|
|
||||||
var settings = require("ep_etherpad-lite/node/utils/Settings");
|
|
||||||
%>
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
|
@ -10,12 +7,7 @@
|
||||||
<meta name="referrer" content="no-referrer">
|
<meta name="referrer" content="no-referrer">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
|
||||||
<link rel="shortcut icon" href="favicon.ico">
|
<link rel="shortcut icon" href="favicon.ico">
|
||||||
|
|
||||||
<link rel="localizations" type="application/l10n+json" href="locales.json">
|
<link rel="localizations" type="application/l10n+json" href="locales.json">
|
||||||
<script type="text/javascript" src="static/js/vendors/html10n.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<script type="text/javascript" src="static/js/l10n.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<script src="static/js/vendors/jquery.js"></script>
|
|
||||||
<script src="static/js/index.js"></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
|
@ -157,6 +149,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% e.end_block(); %>
|
<% e.end_block(); %>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="<%=entrypoint%>"></script>
|
||||||
|
|
||||||
<% e.begin_block("indexCustomScripts"); %>
|
<% e.begin_block("indexCustomScripts"); %>
|
||||||
<script src="static/skins/<%=encodeURI(settings.skinName)%>/index.js?v=<%=settings.randomVersionString%>"></script>
|
<script src="static/skins/<%=encodeURI(settings.skinName)%>/index.js?v=<%=settings.randomVersionString%>"></script>
|
||||||
|
|
6
src/templates/indexBootstrap.js
Normal file
6
src/templates/indexBootstrap.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
|
||||||
|
require('ep_etherpad-lite/static/js/l10n')
|
||||||
|
require('ep_etherpad-lite/static/js/index')
|
||||||
|
})()
|
|
@ -34,7 +34,6 @@
|
||||||
for the JavaScript code in this page.|
|
for the JavaScript code in this page.|
|
||||||
*/
|
*/
|
||||||
</script>
|
</script>
|
||||||
<script src="../static/js/basic_error_handler.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
@ -53,8 +52,6 @@
|
||||||
<% e.end_block(); %>
|
<% e.end_block(); %>
|
||||||
|
|
||||||
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
|
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
|
||||||
<script type="text/javascript" src="../static/js/vendors/html10n.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<script type="text/javascript" src="../static/js/l10n.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<% e.begin_block("body"); %>
|
<% e.begin_block("body"); %>
|
||||||
|
@ -442,67 +439,11 @@
|
||||||
|
|
||||||
<% e.begin_block("scripts"); %>
|
<% e.begin_block("scripts"); %>
|
||||||
|
|
||||||
<script type="text/javascript" src="../static/js/require-kernel.js?v=<%=settings.randomVersionString%>"></script>
|
<script src="<%=entrypoint%>"></script>
|
||||||
<script type="text/javascript" src="../socket.io/socket.io.js"></script>
|
|
||||||
|
|
||||||
<!-- Include base packages manually (this help with debugging) -->
|
|
||||||
<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
|
|
||||||
|
|
||||||
<% e.begin_block("customScripts"); %>
|
<% e.begin_block("customScripts"); %>
|
||||||
<script type="text/javascript" src="../static/skins/<%=encodeURI(settings.skinName)%>/pad.js?v=<%=settings.randomVersionString%>"></script>
|
<script type="text/javascript" src="../static/skins/<%=encodeURI(settings.skinName)%>/pad.js?v=<%=settings.randomVersionString%>"></script>
|
||||||
<% e.end_block(); %>
|
<% e.end_block(); %>
|
||||||
|
|
||||||
<!-- Bootstrap page -->
|
|
||||||
<script type="text/javascript">
|
|
||||||
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt
|
|
||||||
var clientVars = {
|
|
||||||
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the
|
|
||||||
// server sends the CLIENT_VARS message.
|
|
||||||
randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>,
|
|
||||||
};
|
|
||||||
(function () {
|
|
||||||
var pathComponents = location.pathname.split('/');
|
|
||||||
|
|
||||||
// Strip 'p' and the padname from the pathname and set as baseURL
|
|
||||||
var baseURL = pathComponents.slice(0,pathComponents.length-2).join('/') + '/';
|
|
||||||
|
|
||||||
require.setRootURI(baseURL + "javascripts/src");
|
|
||||||
require.setLibraryURI(baseURL + "javascripts/lib");
|
|
||||||
require.setGlobalKeyPath("require");
|
|
||||||
|
|
||||||
$ = jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; // Expose jQuery #HACK
|
|
||||||
browser = require('ep_etherpad-lite/static/js/vendors/browser');
|
|
||||||
|
|
||||||
var plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
|
||||||
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
|
||||||
|
|
||||||
plugins.baseURL = baseURL;
|
|
||||||
plugins.update(function () {
|
|
||||||
// Mechanism for tests to register hook functions (install fake plugins).
|
|
||||||
window._postPluginUpdateForTestingDone = false;
|
|
||||||
if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting();
|
|
||||||
window._postPluginUpdateForTestingDone = true;
|
|
||||||
// Call documentReady hook
|
|
||||||
$(function() {
|
|
||||||
hooks.aCallAll('documentReady');
|
|
||||||
});
|
|
||||||
|
|
||||||
var pad = require('ep_etherpad-lite/static/js/pad');
|
|
||||||
pad.baseURL = baseURL;
|
|
||||||
pad.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* TODO: These globals shouldn't exist. */
|
|
||||||
pad = require('ep_etherpad-lite/static/js/pad').pad;
|
|
||||||
chat = require('ep_etherpad-lite/static/js/chat').chat;
|
|
||||||
padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
|
|
||||||
padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
|
|
||||||
require('ep_etherpad-lite/static/js/skin_variants');
|
|
||||||
|
|
||||||
}());
|
|
||||||
// @license-end
|
|
||||||
</script>
|
|
||||||
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
||||||
<% e.end_block(); %>
|
<% e.end_block(); %>
|
||||||
</body>
|
</body>
|
||||||
|
|
45
src/templates/padBootstrap.js
Normal file
45
src/templates/padBootstrap.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
require('ep_etherpad-lite/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('ep_etherpad-lite/static/js/rjquery').jQuery;
|
||||||
|
window.browser = require('ep_etherpad-lite/static/js/vendors/browser');
|
||||||
|
const pad = require('ep_etherpad-lite/static/js/pad');
|
||||||
|
pad.baseURL = basePath;
|
||||||
|
window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||||
|
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||||
|
|
||||||
|
// TODO: These globals shouldn't exist.
|
||||||
|
window.pad = pad.pad;
|
||||||
|
window.chat = require('ep_etherpad-lite/static/js/chat').chat;
|
||||||
|
window.padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
|
||||||
|
window.padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
|
||||||
|
require('ep_etherpad-lite/static/js/skin_variants');
|
||||||
|
require('ep_etherpad-lite/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('ep_etherpad-lite/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" />
|
<link rel="localizations" type="application/l10n+json" href="../../locales.json" />
|
||||||
<% e.begin_block("timesliderScripts"); %>
|
<% e.begin_block("timesliderScripts"); %>
|
||||||
<script type="text/javascript" src="../../static/js/vendors/html10n.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<script type="text/javascript" src="../../static/js/l10n.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<% e.end_block(); %>
|
<% e.end_block(); %>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -250,58 +248,14 @@
|
||||||
<!-------- JAVASCRIPT --------->
|
<!-------- JAVASCRIPT --------->
|
||||||
<!----------------------------->
|
<!----------------------------->
|
||||||
|
|
||||||
<script type="text/javascript" src="../../static/js/require-kernel.js?v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
|
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
|
||||||
|
|
||||||
<!-- Include base packages manually (this help with debugging) -->
|
<!-- Include base packages manually (this help with debugging) -->
|
||||||
<script type="text/javascript" src="../../javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
|
|
||||||
<script type="text/javascript" src="../../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define&v=<%=settings.randomVersionString%>"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="../../static/skins/<%=encodeURI(settings.skinName)%>/timeslider.js?v=<%=settings.randomVersionString%>"></script>
|
<script type="text/javascript" src="../../static/skins/<%=encodeURI(settings.skinName)%>/timeslider.js?v=<%=settings.randomVersionString%>"></script>
|
||||||
|
|
||||||
<!-- Bootstrap -->
|
<!-- Bootstrap -->
|
||||||
<script type="text/javascript" >
|
<script src="<%=entrypoint%>"></script>
|
||||||
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt
|
|
||||||
var clientVars = {
|
|
||||||
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the
|
|
||||||
// server sends the CLIENT_VARS message.
|
|
||||||
randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>,
|
|
||||||
};
|
|
||||||
let BroadcastSlider;
|
|
||||||
(function () {
|
|
||||||
const pathComponents = location.pathname.split('/');
|
|
||||||
|
|
||||||
// Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL
|
|
||||||
const baseURL = pathComponents.slice(0,pathComponents.length-3).join('/') + '/';
|
|
||||||
|
|
||||||
|
|
||||||
require.setRootURI(baseURL + "javascripts/src");
|
|
||||||
require.setLibraryURI(baseURL + "javascripts/lib");
|
|
||||||
require.setGlobalKeyPath("require");
|
|
||||||
|
|
||||||
$ = jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; // Expose jQuery #HACK
|
|
||||||
browser = require('ep_etherpad-lite/static/js/vendors/browser');
|
|
||||||
|
|
||||||
const plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
|
||||||
const socket = require('ep_etherpad-lite/static/js/timeslider').socket;
|
|
||||||
BroadcastSlider = require('ep_etherpad-lite/static/js/timeslider').BroadcastSlider;
|
|
||||||
plugins.baseURL = baseURL;
|
|
||||||
|
|
||||||
plugins.update(function () {
|
|
||||||
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
|
||||||
const timeslider = require('ep_etherpad-lite/static/js/timeslider')
|
|
||||||
timeslider.baseURL = baseURL;
|
|
||||||
timeslider.init();
|
|
||||||
|
|
||||||
/* TODO: These globals shouldn't exist. */
|
|
||||||
const padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
|
|
||||||
const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
|
|
||||||
|
|
||||||
padeditbar.init()
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
// @license-end
|
|
||||||
</script>
|
|
||||||
<% e.end_block(); %>
|
<% e.end_block(); %>
|
||||||
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import {MapArrayType} from "../../../node/types/MapType";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* caching_middleware is responsible for serving everything under path `/javascripts/`
|
|
||||||
* That includes packages as defined in `src/node/utils/tar.json` and probably also plugin code
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const common = require('../common');
|
|
||||||
import {strict as assert} from 'assert';
|
|
||||||
import queryString from 'querystring';
|
|
||||||
const settings = require('../../../node/utils/Settings');
|
|
||||||
import {it, describe} from 'mocha'
|
|
||||||
|
|
||||||
let agent: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hack! Returns true if the resource is not plaintext
|
|
||||||
* The file should start with the callback method, so we need the
|
|
||||||
* URL.
|
|
||||||
*
|
|
||||||
* @param {string} fileContent the response body
|
|
||||||
* @param {URL} resource resource URI
|
|
||||||
* @returns {boolean} if it is plaintext
|
|
||||||
*/
|
|
||||||
const isPlaintextResponse = (fileContent: string, resource:string): boolean => {
|
|
||||||
// callback=require.define&v=1234
|
|
||||||
const query = (new URL(resource, 'http://localhost')).search.slice(1);
|
|
||||||
// require.define
|
|
||||||
const jsonp = queryString.parse(query).callback;
|
|
||||||
|
|
||||||
// returns true if the first letters in fileContent equal the content of `jsonp`
|
|
||||||
return fileContent.substring(0, jsonp!.length) === jsonp;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
type RequestType = {
|
|
||||||
_shouldUnzip: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hack to disable `superagent`'s auto unzip functionality
|
|
||||||
*
|
|
||||||
* @param {Request} request
|
|
||||||
*/
|
|
||||||
const disableAutoDeflate = (request: RequestType) => {
|
|
||||||
request._shouldUnzip = () => false;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe(__filename, function () {
|
|
||||||
const backups:MapArrayType<any> = {};
|
|
||||||
const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved
|
|
||||||
const packages = [
|
|
||||||
'/javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define',
|
|
||||||
'/javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define',
|
|
||||||
'/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define',
|
|
||||||
'/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define',
|
|
||||||
];
|
|
||||||
|
|
||||||
before(async function () {
|
|
||||||
agent = await common.init();
|
|
||||||
backups.settings = {};
|
|
||||||
backups.settings.minify = settings.minify;
|
|
||||||
});
|
|
||||||
after(async function () {
|
|
||||||
Object.assign(settings, backups.settings);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const minify of [false, true]) {
|
|
||||||
context(`when minify is ${minify}`, function () {
|
|
||||||
before(async function () {
|
|
||||||
settings.minify = minify;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('gets packages uncompressed without Accept-Encoding gzip', function () {
|
|
||||||
for (const resource of packages) {
|
|
||||||
it(resource, async function () {
|
|
||||||
await agent.get(resource)
|
|
||||||
.set('Accept-Encoding', fantasyEncoding)
|
|
||||||
.use(disableAutoDeflate)
|
|
||||||
.expect(200)
|
|
||||||
.expect('Content-Type', /application\/javascript/)
|
|
||||||
.expect((res:any) => {
|
|
||||||
assert.equal(res.header['content-encoding'], undefined);
|
|
||||||
assert(isPlaintextResponse(res.text, resource));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('gets packages compressed with Accept-Encoding gzip', function () {
|
|
||||||
for (const resource of packages) {
|
|
||||||
it(resource, async function () {
|
|
||||||
await agent.get(resource)
|
|
||||||
.set('Accept-Encoding', 'gzip')
|
|
||||||
.use(disableAutoDeflate)
|
|
||||||
.expect(200)
|
|
||||||
.expect('Content-Type', /application\/javascript/)
|
|
||||||
.expect('Content-Encoding', 'gzip')
|
|
||||||
.expect((res:any) => {
|
|
||||||
assert(!isPlaintextResponse(res.text, resource));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not cache content-encoding headers', async function () {
|
|
||||||
await agent.get(packages[0])
|
|
||||||
.set('Accept-Encoding', fantasyEncoding)
|
|
||||||
.expect(200)
|
|
||||||
.expect((res:any) => assert.equal(res.header['content-encoding'], undefined));
|
|
||||||
await agent.get(packages[0])
|
|
||||||
.set('Accept-Encoding', 'gzip')
|
|
||||||
.expect(200)
|
|
||||||
.expect('Content-Encoding', 'gzip');
|
|
||||||
await agent.get(packages[0])
|
|
||||||
.set('Accept-Encoding', fantasyEncoding)
|
|
||||||
.expect(200)
|
|
||||||
.expect((res:any) => assert.equal(res.header['content-encoding'], undefined));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -14,7 +14,6 @@
|
||||||
<div id="iframe-container"></div>
|
<div id="iframe-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../../static/js/require-kernel.js"></script>
|
|
||||||
<script src="../../static/js/vendors/jquery.js"></script>
|
<script src="../../static/js/vendors/jquery.js"></script>
|
||||||
<script src="lib/sendkeys.js"></script>
|
<script src="lib/sendkeys.js"></script>
|
||||||
<script src="../../static/js/vendors/browser.js"></script>
|
<script src="../../static/js/vendors/browser.js"></script>
|
||||||
|
|
|
@ -187,7 +187,6 @@ $(() => (async () => {
|
||||||
// mutates the module definition function to temporarily replace Mocha's functions with
|
// mutates the module definition function to temporarily replace Mocha's functions with
|
||||||
// placeholders. The placeholders make it possible to defer the actual Mocha function calls until
|
// placeholders. The placeholders make it possible to defer the actual Mocha function calls until
|
||||||
// after the modules are all loaded in parallel. require.setGlobalKeyPath() is used to coax
|
// after the modules are all loaded in parallel. require.setGlobalKeyPath() is used to coax
|
||||||
// require-kernel into using the wrapper define() method instead of require.define().
|
|
||||||
|
|
||||||
// Per-module log of attempted Mocha function calls. Key is module path, value is an array of
|
// Per-module log of attempted Mocha function calls. Key is module path, value is an array of
|
||||||
// [functionName, argsArray] arrays.
|
// [functionName, argsArray] arrays.
|
||||||
|
|
|
@ -24,7 +24,7 @@ s!"points":[^,]*!"points": 1000!
|
||||||
' settings.json.template >settings.json
|
' settings.json.template >settings.json
|
||||||
|
|
||||||
log "Assuming src/bin/installDeps.sh has already been run"
|
log "Assuming src/bin/installDeps.sh has already been run"
|
||||||
(cd src && npm run dev &
|
(cd src && pnpm run prod &
|
||||||
ep_pid=$!)
|
ep_pid=$!)
|
||||||
|
|
||||||
log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
|
log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
|
||||||
|
|
|
@ -6,10 +6,12 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"build-copy": "tsc && vite build --outDir ../src/static/oidc --emptyOutDir"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.3",
|
"ep_etherpad-lite": "workspace:../src",
|
||||||
"vite": "^5.3.3"
|
"typescript": "^5.5.4",
|
||||||
|
"vite": "^5.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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>
|
|
@ -3,8 +3,11 @@ import { resolve } from 'path'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/views/',
|
base: '/views/',
|
||||||
build: {
|
build: {
|
||||||
|
commonjsOptions:{
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
},
|
||||||
outDir: resolve(__dirname, '../src/static/oidc'),
|
outDir: resolve(__dirname, '../src/static/oidc'),
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
@ -14,4 +17,31 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
},
|
},
|
||||||
|
server:{
|
||||||
|
proxy:{
|
||||||
|
'/static':{
|
||||||
|
target: 'http://localhost:9001',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/views/manifest.json':{
|
||||||
|
target: 'http://localhost:9001',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/views/, ''),
|
||||||
|
},
|
||||||
|
'/locales.json':{
|
||||||
|
target: 'http://localhost:9001',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/views/, ''),
|
||||||
|
},
|
||||||
|
'/locales':{
|
||||||
|
target: 'http://localhost:9001',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/views/, ''),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
2
var/js/.gitignore
vendored
Normal file
2
var/js/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.js
|
||||||
|
*.map
|
Loading…
Reference in a new issue