Compare commits
11 Commits
develop
...
rhansen-se
Author | SHA1 | Date |
---|---|---|
Richard Hansen | bd5fae97b5 | |
Richard Hansen | 216f5af6a9 | |
Richard Hansen | 9ff6e82c72 | |
Richard Hansen | 39506fcde1 | |
Richard Hansen | cff205dbd7 | |
Richard Hansen | ed991419b8 | |
Richard Hansen | 0900354980 | |
Richard Hansen | ea556c4150 | |
Richard Hansen | 7147195ac6 | |
Richard Hansen | 75591c9946 | |
Richard Hansen | 4d6ec1909a |
|
@ -24,7 +24,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
|
@ -61,7 +61,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
|
@ -122,7 +122,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -155,7 +155,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
cache-to: type=gha,mode=max
|
||||
-
|
||||
name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
|
|
@ -12,11 +12,10 @@ jobs:
|
|||
name: with plugins
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# node: [16, 19, 20] >> Disabled node 16 and 18 because they do not work
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [19, 20]
|
||||
node: [16, 18, 20]
|
||||
|
||||
steps:
|
||||
-
|
||||
|
@ -29,7 +28,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
|
@ -67,9 +66,6 @@ jobs:
|
|||
-
|
||||
name: Write custom settings.json that enables the Admin UI tests
|
||||
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json"
|
||||
-
|
||||
name: increase maxHttpBufferSize
|
||||
run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 100000/' settings.json"
|
||||
-
|
||||
name: Disable import/export rate limiting
|
||||
run: |
|
||||
|
@ -78,7 +74,7 @@ jobs:
|
|||
name: Remove standard frontend test files, so only admin tests are run
|
||||
run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs
|
||||
-
|
||||
uses: saucelabs/sauce-connect-action@v2.3.5
|
||||
uses: saucelabs/sauce-connect-action@v2.3.4
|
||||
with:
|
||||
username: ${{ secrets.SAUCE_USERNAME }}
|
||||
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -45,7 +45,7 @@ jobs:
|
|||
run: |
|
||||
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json
|
||||
-
|
||||
uses: saucelabs/sauce-connect-action@v2.3.5
|
||||
uses: saucelabs/sauce-connect-action@v2.3.4
|
||||
with:
|
||||
username: ${{ secrets.SAUCE_USERNAME }}
|
||||
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
|
@ -78,7 +78,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -133,7 +133,7 @@ jobs:
|
|||
name: Remove standard frontend test files, so only plugin tests are run
|
||||
run: rm src/tests/frontend/specs/*
|
||||
-
|
||||
uses: saucelabs/sauce-connect-action@v2.3.5
|
||||
uses: saucelabs/sauce-connect-action@v2.3.4
|
||||
with:
|
||||
username: ${{ secrets.SAUCE_USERNAME }}
|
||||
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -29,7 +29,7 @@ jobs:
|
|||
src/bin/doc/package-lock.json
|
||||
-
|
||||
name: Install lockfile-lint
|
||||
run: npm install --no-save lockfile-lint --legacy-peer-deps
|
||||
run: npm install --no-save lockfile-lint
|
||||
-
|
||||
name: Run lockfile-lint on package-lock.json
|
||||
run: >
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -50,7 +50,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -107,7 +107,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
with:
|
||||
ref: master
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
|
@ -90,7 +90,7 @@ jobs:
|
|||
run: cd src && npm test
|
||||
-
|
||||
name: Install Cypress
|
||||
run: cd src && npm install cypress --legacy-peer-deps
|
||||
run: cd src && npm install cypress
|
||||
-
|
||||
name: Run Etherpad & Test Frontend
|
||||
run: |
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -106,7 +106,7 @@ jobs:
|
|||
name: Extract Etherpad
|
||||
run: 7z x etherpad-win.zip -oetherpad
|
||||
-
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
@ -115,7 +115,7 @@ jobs:
|
|||
etherpad/src/bin/doc/package-lock.json
|
||||
-
|
||||
name: Install Cypress
|
||||
run: cd etherpad && cd src && npm install cypress --legacy-peer-deps
|
||||
run: cd etherpad && cd src && npm install cypress
|
||||
-
|
||||
name: Run Etherpad
|
||||
run: |
|
||||
|
@ -130,6 +130,6 @@ jobs:
|
|||
run: mv etherpad-win.zip etherpad-lite-win.zip
|
||||
- name: upload binaries to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{startsWith(github.ref, 'refs/tags/v') }}
|
||||
if: ${{startsWith(github.ref, 'refs/tags/') }}
|
||||
with:
|
||||
files: etherpad-lite-win.zip
|
||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -1,14 +1,3 @@
|
|||
# 1.9.4
|
||||
|
||||
### Compability changes
|
||||
|
||||
* Log4js has been updated to the latest version. As it involved a bump of 6 major version.
|
||||
A lot has changed since then. Most notably the console appender has been deprecated. You can find out more about it [here](https://github.com/log4js-node/log4js-node)
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
* Fix for MySQL: The logger calls were incorrectly configured leading to a crash when e.g. somebody uses a different encoding than standard MySQL encoding.
|
||||
|
||||
# 1.9.3
|
||||
|
||||
### Compability changes
|
||||
|
@ -17,15 +6,6 @@
|
|||
in the importExportRateLimiting is set to always trigger. So set it to your desired value.
|
||||
If you haven't changed that value in the settings.json you are all set.
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
* Bugfixes
|
||||
* Fix etherpad crashing with mongodb database
|
||||
|
||||
* Enhancements
|
||||
* Add surrealdb database support. You can find out more about this database [here](https://surrealdb.com).
|
||||
* Make sqlite faster: The sqlite library has been switched to better-sqlite3. This should lead to better performance.
|
||||
|
||||
# 1.9.2
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
|
|
@ -17,9 +17,6 @@ RUN \
|
|||
}
|
||||
ENV TIMEZONE=${TIMEZONE}
|
||||
|
||||
# Control the configuration file to be copied into the container.
|
||||
ARG SETTINGS=./settings.json.docker
|
||||
|
||||
# plugins to install while building the container. By default no plugins are
|
||||
# installed.
|
||||
# If given a value, it has to be a space-separated, quoted list of plugin names.
|
||||
|
@ -104,7 +101,7 @@ RUN { [ -z "${ETHERPAD_PLUGINS}" ] || \
|
|||
rm -rf ~/.npm
|
||||
|
||||
# Copy the configuration file.
|
||||
COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json
|
||||
COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json
|
||||
|
||||
# Fix group permissions
|
||||
RUN chmod -R g=u .
|
||||
|
|
|
@ -283,7 +283,7 @@ Things in context:
|
|||
This hook is called on the client side whenever a user joins or changes. This
|
||||
can be used to create notifications or an alternate user list.
|
||||
|
||||
=== chatNewMessage
|
||||
=== `chatNewMessage`
|
||||
|
||||
Called from: `src/static/js/chat.js`
|
||||
|
||||
|
@ -319,7 +319,7 @@ Context properties:
|
|||
* `duration`: How long (in milliseconds) to display the gritter notification (0
|
||||
to disable).
|
||||
|
||||
=== chatSendMessage
|
||||
=== `chatSendMessage`
|
||||
|
||||
Called from: `src/static/js/chat.js`
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ Things in context:
|
|||
|
||||
If this hook returns an error, the callback to the install function gets an error, too. This seems useful for adding in features when a particular plugin is installed.
|
||||
|
||||
=== init_<plugin name>
|
||||
=== `init_<plugin name>`
|
||||
|
||||
Called from: `src/static/js/pluginfw/plugins.js`
|
||||
|
||||
|
@ -62,7 +62,7 @@ Context properties:
|
|||
* `logger`: An object with the following `console`-like methods: `debug`,
|
||||
`info`, `log`, `warn`, `error`.
|
||||
|
||||
=== expressPreSession
|
||||
=== `expressPreSession`
|
||||
|
||||
Called from: `src/node/hooks/express.js`
|
||||
|
||||
|
@ -92,7 +92,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
};
|
||||
----
|
||||
|
||||
=== expressConfigure
|
||||
=== `expressConfigure`
|
||||
|
||||
Called from: `src/node/hooks/express.js`
|
||||
|
||||
|
@ -107,7 +107,7 @@ Context properties:
|
|||
* `app`: The Express https://expressjs.com/en/4x/api.html==app[Application]
|
||||
object.
|
||||
|
||||
=== expressCreateServer
|
||||
=== `expressCreateServer`
|
||||
|
||||
Called from: `src/node/hooks/express.js`
|
||||
|
||||
|
@ -210,7 +210,7 @@ Things in context:
|
|||
This hook gets called when the access to the concrete pad is being checked.
|
||||
Return `false` to deny access.
|
||||
|
||||
=== getAuthorId
|
||||
=== `getAuthorId`
|
||||
|
||||
Called from `src/node/db/AuthorManager.js`
|
||||
|
||||
|
@ -267,7 +267,7 @@ exports.getAuthorId = async (hookName, context) => {
|
|||
};
|
||||
----
|
||||
|
||||
=== padCreate
|
||||
=== `padCreate`
|
||||
|
||||
Called from: `src/node/db/Pad.js`
|
||||
|
||||
|
@ -279,7 +279,7 @@ Context properties:
|
|||
* `authorId`: The ID of the author who created the pad.
|
||||
* `author` (**deprecated**): Synonym of `authorId`.
|
||||
|
||||
=== padDefaultContent
|
||||
=== `padDefaultContent`
|
||||
|
||||
Called from `src/node/db/Pad.js`
|
||||
|
||||
|
@ -304,7 +304,7 @@ Context properties:
|
|||
be updated to match. Plugins must check the value of the `type` property
|
||||
before reading this value.
|
||||
|
||||
=== padLoad
|
||||
=== `padLoad`
|
||||
|
||||
Called from: `src/node/db/PadManager.js`
|
||||
|
||||
|
@ -315,7 +315,7 @@ Context properties:
|
|||
* `pad`: The Pad object.
|
||||
|
||||
[#_padupdate]
|
||||
=== padUpdate
|
||||
=== `padUpdate`
|
||||
|
||||
Called from: `src/node/db/Pad.js`
|
||||
|
||||
|
@ -329,7 +329,7 @@ Context properties:
|
|||
* `revs`: The index of the new revision.
|
||||
* `changeset`: The changeset of this revision (see <<_padupdate>>).
|
||||
|
||||
=== padCopy
|
||||
=== `padCopy`
|
||||
|
||||
Called from: `src/node/db/Pad.js`
|
||||
|
||||
|
@ -355,7 +355,7 @@ Usage examples:
|
|||
|
||||
* https://github.com/ether/ep_comments_page
|
||||
|
||||
=== padRemove
|
||||
=== `padRemove`
|
||||
|
||||
Called from: `src/node/db/Pad.js`
|
||||
|
||||
|
@ -370,7 +370,7 @@ Usage examples:
|
|||
|
||||
* https://github.com/ether/ep_comments_page
|
||||
|
||||
=== padCheck
|
||||
=== `padCheck`
|
||||
|
||||
Called from: `src/node/db/Pad.js`
|
||||
|
||||
|
@ -393,7 +393,7 @@ Things in context:
|
|||
|
||||
I have no idea what this is useful for, someone else will have to add this description.
|
||||
|
||||
=== preAuthorize
|
||||
=== `preAuthorize`
|
||||
|
||||
Called from: `src/node/hooks/express/webaccess.js`
|
||||
|
||||
|
@ -700,7 +700,7 @@ exports.authzFailure = (hookName, context, cb) => {
|
|||
};
|
||||
----
|
||||
|
||||
=== handleMessage
|
||||
=== `handleMessage`
|
||||
|
||||
Called from: `src/node/handler/PadMessageHandler.js`
|
||||
|
||||
|
@ -733,7 +733,7 @@ exports.handleMessage = async (hookName, {message, socket}) => {
|
|||
};
|
||||
----
|
||||
|
||||
=== handleMessageSecurity
|
||||
=== `handleMessageSecurity`
|
||||
|
||||
Called from: `src/node/handler/PadMessageHandler.js`
|
||||
|
||||
|
@ -819,7 +819,7 @@ exports.clientVars = (hookName, context, callback) => {
|
|||
};
|
||||
----
|
||||
|
||||
=== getLineHTMLForExport
|
||||
=== `getLineHTMLForExport`
|
||||
|
||||
Called from: `src/node/utils/ExportHtml.js`
|
||||
|
||||
|
@ -968,7 +968,7 @@ exports.exportHtmlAdditionalTagsWithData = function(hook, pad, cb){
|
|||
};
|
||||
----
|
||||
|
||||
=== exportEtherpadAdditionalContent
|
||||
=== `exportEtherpadAdditionalContent`
|
||||
|
||||
Called from `src/node/utils/ExportEtherpad.js` and
|
||||
`src/node/utils/ImportEtherpad.js`.
|
||||
|
@ -990,7 +990,7 @@ Example:
|
|||
exports.exportEtherpadAdditionalContent = () => ['comments'];
|
||||
----
|
||||
|
||||
=== exportEtherpad
|
||||
=== `exportEtherpad`
|
||||
|
||||
Called from `src/node/utils/ExportEtherpad.js`.
|
||||
|
||||
|
@ -1013,7 +1013,7 @@ Context properties:
|
|||
should not assume that it is either the pad's real writable ID or its
|
||||
read-only ID.
|
||||
|
||||
=== importEtherpad
|
||||
=== `importEtherpad`
|
||||
|
||||
Called from `src/node/utils/ImportEtherpad.js`.
|
||||
|
||||
|
@ -1032,7 +1032,7 @@ Context properties:
|
|||
modified.
|
||||
* `srcPadId`: The pad ID used for the pad-specific information in `data`.
|
||||
|
||||
=== import
|
||||
=== `import`
|
||||
|
||||
Called from: `src/node/handler/ImportHandler.js`
|
||||
|
||||
|
@ -1062,7 +1062,7 @@ exports.import = async (hookName, {fileEnding, ImportError}) => {
|
|||
};
|
||||
----
|
||||
|
||||
=== userJoin
|
||||
=== `userJoin`
|
||||
|
||||
Called from: `src/node/handler/PadMessageHandler.js`
|
||||
|
||||
|
@ -1086,7 +1086,7 @@ exports.userJoin = async (hookName, {authorId, displayName, padId}) => {
|
|||
};
|
||||
```
|
||||
|
||||
=== userLeave
|
||||
=== `userLeave`
|
||||
|
||||
Called from: `src/node/handler/PadMessageHandler.js`
|
||||
|
||||
|
@ -1110,7 +1110,7 @@ exports.userLeave = async (hookName, {author, padId}) => {
|
|||
};
|
||||
----
|
||||
|
||||
=== chatNewMessage
|
||||
=== `chatNewMessage`
|
||||
|
||||
Called from: `src/node/handler/PadMessageHandler.js`
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
body {
|
||||
border-top: solid #44b492 5pt;
|
||||
line-height: 150%;
|
||||
font-family: "Quicksand", sans-serif;
|
||||
line-height:150%;
|
||||
font-family: 'Quicksand',sans-serif;
|
||||
color: #313b4a;
|
||||
max-width: 1440px;
|
||||
max-width:800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
@ -12,25 +12,24 @@ a {
|
|||
color: #555;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
h1,h2 {
|
||||
color: #44b492;
|
||||
line-height: 100%;
|
||||
line-height:100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 48px;
|
||||
font-size: 48px ;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
h4{
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
h5{
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
|
@ -40,7 +39,7 @@ a:hover {
|
|||
|
||||
pre {
|
||||
background-color: #e0e0e0;
|
||||
padding: 20px;
|
||||
padding:20px;
|
||||
}
|
||||
|
||||
code {
|
||||
|
@ -51,9 +50,7 @@ img {
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
table, th, td {
|
||||
text-align: left;
|
||||
border: 1px solid gray;
|
||||
border-collapse: collapse;
|
||||
|
@ -61,7 +58,7 @@ td {
|
|||
|
||||
th {
|
||||
padding: 0.5em;
|
||||
background: #eee;
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
td {
|
||||
|
|
|
@ -19,7 +19,7 @@ Cookies used by Etherpad.
|
|||
| Session
|
||||
| true
|
||||
| true
|
||||
| Session ID of the https://expressjs.com[Express web framework]. When Etherpad is behind a reverse proxy, and an administrator wants to use session stickiness, he may use this cookie. If you are behind a reverse proxy, please remember to set `trustProxy: true` in `settings.json`. Set in https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/hooks/express/webaccess.js#L131[webaccess.js#L131].
|
||||
| Session ID of the https://expressjs.com[Express web framework]. When Etherpad is behind a reverse proxy, and an administrator wants to use session stickiness, he may use this cookie. If you are behind a reverse proxy, please remember to set `trustProxy: true` in `settings.json`. Set in [webaccess.js#L131](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/hooks/express/webaccess.js#L131).
|
||||
|
||||
|
||||
|language
|
||||
|
@ -29,7 +29,7 @@ Cookies used by Etherpad.
|
|||
| Session
|
||||
| false
|
||||
| true
|
||||
| The language of the UI (e.g.: `en-GB`, `it`). Set in https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_editor.js#L111[pad_editor.js#L111].
|
||||
| The language of the UI (e.g.: `en-GB`, `it`). Set in [pad_editor.js#L111](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_editor.js#L111).
|
||||
|
||||
|
||||
|prefs / prefsHttp
|
||||
|
@ -39,7 +39,7 @@ Cookies used by Etherpad.
|
|||
| year 3000
|
||||
| false
|
||||
| true
|
||||
| Client-side preferences (e.g.: font family, chat always visible, show authorship colors, ...). Set in https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_cookie.js#L49[pad_cookie.js#L49]. `prefs` is used if Etherpad is accessed over HTTPS, `prefsHttp` if accessed over HTTP. For more info see https://github.com/ether/etherpad-lite/issues/3179.
|
||||
| Client-side preferences (e.g.: font family, chat always visible, show authorship colors, ...). Set in [pad_cookie.js#L49](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_cookie.js#L49). `prefs` is used if Etherpad is accessed over HTTPS, `prefsHttp` if accessed over HTTP. For more info see https://github.com/ether/etherpad-lite/issues/3179.
|
||||
|
||||
|
||||
|
||||
|
@ -50,7 +50,7 @@ Cookies used by Etherpad.
|
|||
| 60 days
|
||||
| false
|
||||
| true
|
||||
| A random token representing the author, of the form `t.randomstring_of_lenght_20`. The random string is generated by the client, at https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L55-L66[pad.js#L55-L66]. This cookie is always set by the client at https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L153-L158[pad.js#L153-L158] without any solicitation from the server. It is used for all the pads accessed via the web UI (not used for the HTTP API). On the server side, its value is accessed at https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/db/SecurityManager.js#L33[SecurityManager.js#L33].
|
||||
| A random token representing the author, of the form `t.randomstring_of_lenght_20`. The random string is generated by the client, at (https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L55-L66[pad.js#L55-L66]). This cookie is always set by the client (at (https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L153-L158[pad.js#L153-L158])) without any solicitation from the server. It is used for all the pads accessed via the web UI (not used for the HTTP API). On the server side, its value is accessed at [SecurityManager.js#L33](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/db/SecurityManager.js#L33).
|
||||
|===
|
||||
|
||||
For more info, visit the related discussion at https://github.com/ether/etherpad-lite/issues/3563.
|
||||
|
|
|
@ -20,13 +20,12 @@ Translations will be send back to us regularly and will eventually appear in the
|
|||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"pad.modals.connected": "Connecté.",
|
||||
"pad.modals.uderdup": "Ouvrir dans une nouvelle fenêtre.",
|
||||
"pad.toolbar.unindent.title": "Dèsindenter",
|
||||
"pad.toolbar.undo.title": "Annuler (Ctrl-Z)",
|
||||
"timeslider.pageTitle": "{{appTitle}} Curseur temporel",
|
||||
...
|
||||
{ "pad.modals.connected": "Connecté."
|
||||
, "pad.modals.uderdup": "Ouvrir dans une nouvelle fenêtre."
|
||||
, "pad.toolbar.unindent.title": "Dèsindenter"
|
||||
, "pad.toolbar.undo.title": "Annuler (Ctrl-Z)"
|
||||
, "timeslider.pageTitle": "{{appTitle}} Curseur temporel",
|
||||
, ...
|
||||
}
|
||||
----
|
||||
|
||||
|
@ -72,7 +71,7 @@ alert(window._('pad.chat'));
|
|||
----
|
||||
==== 2. Create translate files in the locales directory of your plugin
|
||||
|
||||
* The name of the file must be the language code of the language it contains translations for (see https://joker-x.github.io/languages4translatewiki/test/[supported lang codes]; e.g. en ? English, es ? Spanish...)
|
||||
* The name of the file must be the language code of the language it contains translations for (see https://joker-x.github.com/languages4translatewiki/test/[supported lang codes]; e.g. en ? English, es ? Spanish...)
|
||||
* The extension of the file must be `.json`
|
||||
* The default language is English, so your plugin should always provide `en.json`
|
||||
* In order to avoid naming conflicts, your message keys should start with the name of your plugin followed by a dot (see below)
|
||||
|
@ -81,8 +80,7 @@ alert(window._('pad.chat'));
|
|||
|
||||
[source, json]
|
||||
----
|
||||
{
|
||||
"ep_your-plugin.h1": "Heading 1"
|
||||
{ "ep_your-plugin.h1": "Heading 1"
|
||||
}
|
||||
----
|
||||
|
||||
|
@ -90,8 +88,7 @@ alert(window._('pad.chat'));
|
|||
|
||||
[source, json]
|
||||
----
|
||||
{
|
||||
"ep_your-plugin.h1": "Título 1"
|
||||
{ "ep_your-plugin.h1": "Título 1"
|
||||
}
|
||||
----
|
||||
|
||||
|
@ -106,9 +103,8 @@ For example, if you want to replace `Chat` with `Notes`, simply add...
|
|||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"ep_your-plugin.h1": "Heading 1",
|
||||
"pad.chat": "Notes"
|
||||
{ "ep_your-plugin.h1": "Heading 1"
|
||||
, "pad.chat": "Notes"
|
||||
}
|
||||
----
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ translations into `locales/`, though, in order to have them integrated. (See
|
|||
Your plugin definition goes into `ep.json`. In this file you register your hook
|
||||
functions, indicate the parts of your plugin and the order of execution. (A
|
||||
documentation of all available events to hook into can be found in chapter
|
||||
<<Server-side hooks>>.)
|
||||
[hooks](#all_hooks).)
|
||||
|
||||
[source,json]
|
||||
----
|
||||
|
@ -88,7 +88,7 @@ A hook function registration maps a hook name to a hook function specification.
|
|||
The hook function specification looks like `ep_example/file.js:functionName`. It
|
||||
consists of two parts separated by a colon: a module name to `require()` and the
|
||||
name of a function exported by the named module. See
|
||||
https://nodejs.org/docs/latest/api/modules.html#modules_module_exports[`module.exports`]
|
||||
[`module.exports`](https://nodejs.org/docs/latest/api/modules.html#modules_module_exports)
|
||||
for how to export a function.
|
||||
|
||||
For the module name you can omit the `.js` suffix, and if the file is `index.js`
|
||||
|
|
|
@ -207,15 +207,15 @@
|
|||
|
||||
"dbType": "${DB_TYPE:dirty}",
|
||||
"dbSettings": {
|
||||
"host": "${DB_HOST:undefined}",
|
||||
"port": "${DB_PORT:undefined}",
|
||||
"database": "${DB_NAME:undefined}",
|
||||
"user": "${DB_USER:undefined}",
|
||||
"password": "${DB_PASS:undefined}",
|
||||
"charset": "${DB_CHARSET:undefined}",
|
||||
"filename": "${DB_FILENAME:var/dirty.db}",
|
||||
"host": "${DB_HOST:undefined}",
|
||||
"port": "${DB_PORT:undefined}",
|
||||
"database": "${DB_NAME:undefined}",
|
||||
"user": "${DB_USER:undefined}",
|
||||
"password": "${DB_PASS:undefined}",
|
||||
"charset": "${DB_CHARSET:undefined}",
|
||||
"filename": "${DB_FILENAME:var/dirty.db}",
|
||||
"collection": "${DB_COLLECTION:undefined}",
|
||||
"url": "${DB_URL:undefined}"
|
||||
"url": "${DB_URL:undefined}"
|
||||
},
|
||||
|
||||
/*
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"requires": true,
|
||||
"dependencies": {
|
||||
"marked": {
|
||||
"version": "9.1.4",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.4.tgz",
|
||||
"integrity": "sha512-Mq83CCaClhXqhf8sLQ57c1unNelHEuFadK36ga+GeXR4FeT/5ssaC5PaCRVqMA74VYorzYRqdAaxxteIanh3Kw=="
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-9.0.0.tgz",
|
||||
"integrity": "sha512-37yoTpjU+TSXb9OBYY5n78z/CqXh76KiQj9xsKxEdztzU9fRLmbWO5YqKxgCVGKlNdexppnbKTkwB3RipVri8w=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"node": ">=12.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "^9.1.4"
|
||||
"marked": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {},
|
||||
|
|
|
@ -14,7 +14,7 @@ cd /D node_modules
|
|||
mklink /D "ep_etherpad-lite" "..\src"
|
||||
|
||||
cd /D "ep_etherpad-lite"
|
||||
cmd /C npm ci --legacy-peer-deps || exit /B 1
|
||||
cmd /C npm ci || exit /B 1
|
||||
|
||||
cd /D "%~dp0\..\.."
|
||||
|
||||
|
|
|
@ -9,12 +9,9 @@ const childProcess = require('child_process');
|
|||
const log4js = require('log4js');
|
||||
const path = require('path');
|
||||
const semver = require('semver');
|
||||
const {exec} = require('child_process');
|
||||
const {exec} = require("child_process");
|
||||
|
||||
log4js.configure({appenders: {console: {type: 'console'}},
|
||||
categories: {
|
||||
default: {appenders: ['console'], level: 'info'},
|
||||
}});
|
||||
log4js.replaceConsole();
|
||||
|
||||
/*
|
||||
|
||||
|
@ -81,11 +78,11 @@ const assertUpstreamOk = (branch, opts = {}) => {
|
|||
};
|
||||
|
||||
// Check if asciidoctor is installed
|
||||
exec('asciidoctor -v', (err, stdout) => {
|
||||
if (err) {
|
||||
console.log('Please install asciidoctor');
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/');
|
||||
process.exit(1);
|
||||
exec('asciidoctor -v', (err,stdout)=>{
|
||||
if (err){
|
||||
console.log('Please install asciidoctor')
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/')
|
||||
process.exit(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -186,7 +183,7 @@ try {
|
|||
run('git pull --ff-only', {cwd: '../ether.github.com/'});
|
||||
console.log('Committing documentation...');
|
||||
run(`cp -R out/doc/ ../ether.github.com/public/doc/v'${newVersion}'`);
|
||||
run(`npm version ${newVersion}`, {cwd: '../ether.github.com'});
|
||||
run(`npm version ${newVersion}`, {cwd:'../ether.github.com'})
|
||||
run('git add .', {cwd: '../ether.github.com/'});
|
||||
run(`git commit -m '${newVersion} docs'`, {cwd: '../ether.github.com/'});
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Xuacu",
|
||||
"YoaR"
|
||||
"Xuacu"
|
||||
]
|
||||
},
|
||||
"index.newPad": "Nuevu bloc",
|
||||
|
@ -26,9 +25,9 @@
|
|||
"pad.toolbar.embed.title": "Compartir ya incrustar esti bloc",
|
||||
"pad.toolbar.showusers.title": "Amosar los usuarios d'esti bloc",
|
||||
"pad.colorpicker.save": "Guardar",
|
||||
"pad.colorpicker.cancel": "Zarrar",
|
||||
"pad.colorpicker.cancel": "Encaboxar",
|
||||
"pad.loading": "Cargando...",
|
||||
"pad.noCookie": "Nun pudo alcontrase la cookie. ¡Por favor, permite les cookies nel navegador! La sesión y preferencies nun se guarden ente visites. Esto pue debese a qu'Etherpad inclúyese nun iFrame en dalgunos restoladores. Asegúrate de qu'Etherpad tea nel mesmu subdominiu/dominiu que l'iFrame padre",
|
||||
"pad.noCookie": "Nun pudo alcontrase la cookie. ¡Por favor, permite les cookies nel navegador! La sesión y preferencies nun se guarden ente visites. Esto pué debese a qu'Etherpad inclúyese nun iFrame en dalgunos restoladores. Asegúrate de qu'Etherpad tea nel mesmu subdominiu/dominiu que la iFrame padre",
|
||||
"pad.permissionDenied": "Nun tienes permisu pa entrar a esti bloc",
|
||||
"pad.settings.padSettings": "Configuración del bloc",
|
||||
"pad.settings.myView": "la mio vista",
|
||||
|
@ -57,7 +56,7 @@
|
|||
"pad.modals.reconnecting": "Reconeutando col to bloc...",
|
||||
"pad.modals.forcereconnect": "Forzar la reconexón",
|
||||
"pad.modals.reconnecttimer": "Tentando reconeutar en",
|
||||
"pad.modals.cancel": "Zarrar",
|
||||
"pad.modals.cancel": "Encaboxar",
|
||||
"pad.modals.userdup": "Abiertu n'otra ventana",
|
||||
"pad.modals.userdup.explanation": "Esti bloc paez que ta abiertu en más d'una ventana del navegador d'esti ordenador.",
|
||||
"pad.modals.userdup.advice": "Reconeutar pa usar esta ventana.",
|
||||
|
|
|
@ -4,29 +4,13 @@
|
|||
"Christian List",
|
||||
"Joedalton",
|
||||
"Peter Alberti",
|
||||
"Peterleth",
|
||||
"Saederup92",
|
||||
"Steenth"
|
||||
]
|
||||
},
|
||||
"admin.page-title": "Admin Dashboard - Etherpad",
|
||||
"admin_plugins": "Plugin manager",
|
||||
"admin_plugins.available": "Tilgængelige Plugins",
|
||||
"admin_plugins.available_not-found": "Ingen plugins fundet.",
|
||||
"admin_plugins.available_fetching": "Henter...",
|
||||
"admin_plugins.available_install.value": "Installer",
|
||||
"admin_plugins.available_search.placeholder": "Søg efter plugins der kan installeres",
|
||||
"admin_plugins.description": "Beskrivelse",
|
||||
"admin_plugins.installed": "Installerede plugins",
|
||||
"admin_plugins.installed_fetching": "Henter installerede plugins...",
|
||||
"admin_plugins.installed_nothing": "Du har ikke installeret nogen plugins endnu.",
|
||||
"admin_plugins.installed_uninstall.value": "Afinstaller",
|
||||
"admin_plugins.last-update": "Sidst opdateret",
|
||||
"admin_plugins.name": "Navn",
|
||||
"admin_plugins.page-title": "Plugin manager - Etherpad",
|
||||
"admin_plugins.version": "Version",
|
||||
"admin_plugins_info": "Fejlfindingsoplysninger",
|
||||
"admin_plugins_info.hooks": "Installerede hooks",
|
||||
"admin_settings": "Indstillinger",
|
||||
"index.newPad": "Ny Pad",
|
||||
"index.createOpenPad": "eller opret/åbn en Pad med navnet:",
|
||||
|
|
|
@ -16,33 +16,8 @@
|
|||
"admin_plugins.available_not-found": "Įskiepių nerasta.",
|
||||
"admin_plugins.available_fetching": "Gaunama…",
|
||||
"admin_plugins.available_install.value": "Įdiegti",
|
||||
"admin_plugins.available_search.placeholder": "Ieškokite įskiepių įdiegimui",
|
||||
"admin_plugins.description": "Aprašymas",
|
||||
"admin_plugins.installed": "Įdiegti įskiepiai",
|
||||
"admin_plugins.installed_fetching": "Gaunami įdiegti papildiniai…",
|
||||
"admin_plugins.installed_nothing": "Dar neįdiegėte jokių papildinių.",
|
||||
"admin_plugins.installed_uninstall.value": "Išinstaluoti",
|
||||
"admin_plugins.last-update": "Paskutinis atnaujinimas",
|
||||
"admin_plugins.name": "Pavadinimas",
|
||||
"admin_plugins.page-title": "Papildinių tvarkyklė – Etherpad",
|
||||
"admin_plugins.version": "Versija",
|
||||
"admin_plugins_info": "Trikčių šalinimo informacija",
|
||||
"admin_plugins_info.parts": "Įdiegtos dalys",
|
||||
"admin_plugins_info.plugins": "Įdiegti papildiniai",
|
||||
"admin_plugins_info.page-title": "Papildinio informacija – Etherpad",
|
||||
"admin_plugins_info.version": "Etherpad versija",
|
||||
"admin_plugins_info.version_latest": "Naujausia prieinama versija",
|
||||
"admin_plugins_info.version_number": "Versijos numeris",
|
||||
"admin_settings": "Nustatymai",
|
||||
"admin_settings.current": "Dabartinė konfigūracija",
|
||||
"admin_settings.current_example-devel": "Kūrimo nustatymų šablono pavyzdys",
|
||||
"admin_settings.current_example-prod": "Gamybos nustatymų šablono pavyzdys",
|
||||
"admin_settings.current_restart.value": "Iš naujo paleisti Etherpad",
|
||||
"admin_settings.current_save.value": "Išsaugoti nustatymus",
|
||||
"admin_settings.page-title": "Nustatymai – Etherpad",
|
||||
"index.newPad": "Naujas bloknotas",
|
||||
"index.createOpenPad": "arba sukurkite/atidarykite Bloknotą su pavadinimu:",
|
||||
"index.openPad": "atidaryti egzistuojantį bloknotą su pavadinimu:",
|
||||
"pad.toolbar.bold.title": "Paryškintasis (Ctrl-B)",
|
||||
"pad.toolbar.italic.title": "Pasvirasis (Ctrl-I)",
|
||||
"pad.toolbar.underline.title": "Pabraukimas (Ctrl-U)",
|
||||
|
@ -75,8 +50,6 @@
|
|||
"pad.settings.fontType": "Šrifto tipas:",
|
||||
"pad.settings.fontType.normal": "Normalus",
|
||||
"pad.settings.language": "Kalba:",
|
||||
"pad.settings.about": "Apie",
|
||||
"pad.settings.poweredBy": "Palaiko",
|
||||
"pad.importExport.import_export": "Importuoti/Eksportuoti",
|
||||
"pad.importExport.import": "Įkelkite bet kokį tekstinį failą arba dokumentą",
|
||||
"pad.importExport.importSuccessful": "Pavyko!",
|
||||
|
@ -87,9 +60,9 @@
|
|||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Atvirasis dokumento formatas)",
|
||||
"pad.importExport.abiword.innerHTML": "Galite importuoti tik iš paprasto teksto ar HTML formatų. Dėl išplėstinių importavimo funkcijų prašome <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">įdiegti AbiWord ar LibreOffice</a>.",
|
||||
"pad.importExport.abiword.innerHTML": "Galite importuoti tik iš paprasto teksto ar HTML formato. Dėl išplėstinių importavimo funkcijų prašome <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">įdiegti AbiWord</a>.",
|
||||
"pad.modals.connected": "Prisijungta.",
|
||||
"pad.modals.reconnecting": "Iš naujo prisijungiama prie jūsų bloknoto…",
|
||||
"pad.modals.reconnecting": "Iš naujo prisijungiama prie Jūsų bloknoto",
|
||||
"pad.modals.forcereconnect": "Priversti prisijungti iš naujo",
|
||||
"pad.modals.reconnecttimer": "Bandoma vėl prisijungti",
|
||||
"pad.modals.cancel": "Atšaukti",
|
||||
|
@ -111,9 +84,6 @@
|
|||
"pad.modals.corruptPad.cause": "Tai gali nutikti dėl neteisingos serverio konfigūracijos ar kitos netikėtos elgsenos. Prašome susisiekti su paslaugos administratoriumi.",
|
||||
"pad.modals.deleted": "Ištrintas.",
|
||||
"pad.modals.deleted.explanation": "Bloknotas buvo pašalintas.",
|
||||
"pad.modals.rateLimited.explanation": "Išsiuntėte per daug pranešimų į šį bloknotą, todėl jis atjungė jus.",
|
||||
"pad.modals.rejected.explanation": "Serveris atmetė jūsų naršyklės išsiųstą pranešimą.",
|
||||
"pad.modals.rejected.cause": "Gali būti, kad serveris buvo atnaujintas jums peržiūrint bloknotą, o gal tai Etherpad klaida. Pabandykite iš naujo įkelti puslapį.",
|
||||
"pad.modals.disconnected": "Jūs atsijungėte.",
|
||||
"pad.modals.disconnected.explanation": "Ryšys su serveriu nutrūko",
|
||||
"pad.modals.disconnected.cause": "Gali būti, kad serveris yra nepasiekiamas. Prašome informuoti paslaugos administratorių jei tai tęsiasi.",
|
||||
|
@ -126,7 +96,6 @@
|
|||
"pad.chat.loadmessages": "Įkrauti daugiau pranešimų",
|
||||
"pad.chat.stick.title": "Priklijuoti pokalbį",
|
||||
"pad.chat.writeMessage.placeholder": "Rašykite savo žinutę čia",
|
||||
"timeslider.followContents": "Sekite bloknoto turinio atnaujinimus",
|
||||
"timeslider.pageTitle": "{{appTitle}} Laiko slinkiklis",
|
||||
"timeslider.toolbar.returnbutton": "Grįžti į bloknotą",
|
||||
"timeslider.toolbar.authors": "Autoriai:",
|
||||
|
@ -156,7 +125,7 @@
|
|||
"pad.savedrevs.timeslider": "Galite peržiūrėti išsaugotas peržiūras apsilankydami laiko slinkiklyje",
|
||||
"pad.userlist.entername": "Įveskite savo vardą",
|
||||
"pad.userlist.unnamed": "bevardis",
|
||||
"pad.editbar.clearcolors": "Išvalyti autorystės spalvas visame dokumente? To negalima atšaukti",
|
||||
"pad.editbar.clearcolors": "Išvalyti autorystės spalvas visame dokumente?",
|
||||
"pad.impexp.importbutton": "Importuoti dabar",
|
||||
"pad.impexp.importing": "Importuojama...",
|
||||
"pad.impexp.confirmimport": "Failo importavimas pakeis dabartinį bloknoto tekstą. Ar tikrai norite tęsti?",
|
||||
|
@ -165,6 +134,5 @@
|
|||
"pad.impexp.uploadFailed": "Įkėlimas nepavyko, bandykite dar kartą",
|
||||
"pad.impexp.importfailed": "Importuoti nepavyko",
|
||||
"pad.impexp.copypaste": "Prašome nukopijuoti ir įklijuoti",
|
||||
"pad.impexp.exportdisabled": "Eksportavimas {{type}} formatu yra išjungtas. Prašome susisiekti su savo sistemos administratoriumi dėl informacijos.",
|
||||
"pad.impexp.maxFileSize": "Failas per didelis. Susisiekite su svetainės administratoriumi, kad padidintų leistiną importuoti failo dydį"
|
||||
"pad.impexp.exportdisabled": "Eksportavimas {{type}} formatu yra išjungtas. Prašome susisiekti su savo sistemos administratoriumi dėl informacijos."
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
"@metadata": {
|
||||
"authors": [
|
||||
"AmaryllisGardener",
|
||||
"CiphriusKane",
|
||||
"John Reid",
|
||||
"Nintendofan885"
|
||||
]
|
||||
|
@ -51,7 +50,7 @@
|
|||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Ye can anely import fae plain tex or HTML formats. Fur mair advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">install abiword</a>.",
|
||||
"pad.importExport.abiword.innerHTML": "Ye can yinly import fae plain tex or HTML formats. Fer mair advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">install abiword</a>.",
|
||||
"pad.modals.connected": "Connected.",
|
||||
"pad.modals.reconnecting": "Reconnectin til yer pad..",
|
||||
"pad.modals.forcereconnect": "Force reconnect",
|
||||
|
@ -77,7 +76,7 @@
|
|||
"pad.modals.disconnected.explanation": "The connection til the server wis loast",
|
||||
"pad.modals.disconnected.cause": "The server micht be onavailable. Please notify the service admeenistrater gif this continues tae happen.",
|
||||
"pad.share": "Share this pad",
|
||||
"pad.share.readonly": "Read anely",
|
||||
"pad.share.readonly": "Read yinly",
|
||||
"pad.share.link": "Airtin",
|
||||
"pad.share.emebdcode": "Embed URL",
|
||||
"pad.chat": "Chait",
|
||||
|
|
|
@ -33,7 +33,7 @@ const exportTxt = require('../utils/ExportTxt');
|
|||
const importHtml = require('../utils/ImportHtml');
|
||||
const cleanText = require('./Pad').cleanText;
|
||||
const PadDiff = require('../utils/padDiff');
|
||||
const {checkValidRev, isInt} = require('../utils/checkValidRev');
|
||||
const { checkValidRev, isInt } = require('../utils/checkValidRev');
|
||||
|
||||
/* ********************
|
||||
* GROUP FUNCTIONS ****
|
||||
|
@ -193,13 +193,6 @@ Example returns:
|
|||
{code: 1, message:"padID does not exist", data: null}
|
||||
{code: 1, message:"text too long", data: null}
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* @param {String} padID the id of the pad
|
||||
* @param {String} text the text of the pad
|
||||
* @param {String} authorId the id of the author, defaulting to empty string
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.setText = async (padID, text, authorId = '') => {
|
||||
// text is required
|
||||
if (typeof text !== 'string') {
|
||||
|
@ -221,10 +214,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
{code: 1, message:"text too long", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {String} text the text of the pad
|
||||
@param {String} authorId the id of the author, defaulting to empty string
|
||||
*/
|
||||
*/
|
||||
exports.appendText = async (padID, text, authorId = '') => {
|
||||
// text is required
|
||||
if (typeof text !== 'string') {
|
||||
|
@ -243,9 +233,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {String} rev the revision number, defaulting to the latest revision
|
||||
@return {Promise<{html: string}>} the html of the pad
|
||||
*/
|
||||
exports.getHTML = async (padID, rev) => {
|
||||
if (rev !== undefined) {
|
||||
|
@ -278,10 +265,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
|
||||
@param {String} padID the id of the pad
|
||||
@param {String} html the html of the pad
|
||||
@param {String} authorId the id of the author, defaulting to empty string
|
||||
*/
|
||||
exports.setHTML = async (padID, html, authorId = '') => {
|
||||
// html string is required
|
||||
|
@ -320,9 +303,6 @@ Example returns:
|
|||
{code: 1, message:"start is higher or equal to the current chatHead", data: null}
|
||||
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {Number} start the start point of the chat-history
|
||||
@param {Number} end the end point of the chat-history
|
||||
*/
|
||||
exports.getChatHistory = async (padID, start, end) => {
|
||||
if (start && end) {
|
||||
|
@ -369,10 +349,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {String} text the text of the chat-message
|
||||
@param {String} authorID the id of the author
|
||||
@param {Number} time the timestamp of the chat-message
|
||||
*/
|
||||
exports.appendChatMessage = async (padID, text, authorID, time) => {
|
||||
// text is required
|
||||
|
@ -402,7 +378,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {revisions: 56}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
*/
|
||||
exports.getRevisionsCount = async (padID) => {
|
||||
// get the pad
|
||||
|
@ -417,7 +392,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {savedRevisions: 42}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
*/
|
||||
exports.getSavedRevisionsCount = async (padID) => {
|
||||
// get the pad
|
||||
|
@ -432,7 +406,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
*/
|
||||
exports.listSavedRevisions = async (padID) => {
|
||||
// get the pad
|
||||
|
@ -447,8 +420,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {Number} rev the revision number, defaulting to the latest revision
|
||||
*/
|
||||
exports.saveRevision = async (padID, rev) => {
|
||||
// check if rev is a number
|
||||
|
@ -480,8 +451,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {lastEdited: 1340815946602}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad
|
||||
*/
|
||||
exports.getLastEdited = async (padID) => {
|
||||
// get the pad
|
||||
|
@ -497,9 +466,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"pad does already exist", data: null}
|
||||
@param {String} padName the name of the new pad
|
||||
@param {String} text the initial text of the pad
|
||||
@param {String} authorId the id of the author, defaulting to empty string
|
||||
*/
|
||||
exports.createPad = async (padID, text, authorId = '') => {
|
||||
if (padID) {
|
||||
|
@ -525,7 +491,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
*/
|
||||
exports.deletePad = async (padID) => {
|
||||
const pad = await getPadSafe(padID, true);
|
||||
|
@ -539,9 +504,6 @@ exports.deletePad = async (padID) => {
|
|||
|
||||
{code:0, message:"ok", data:null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {Number} rev the revision number, defaulting to the latest revision
|
||||
@param {String} authorId the id of the author, defaulting to empty string
|
||||
*/
|
||||
exports.restoreRevision = async (padID, rev, authorId = '') => {
|
||||
// check if rev is a number
|
||||
|
@ -606,9 +568,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {padID: destinationID}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} sourceID the id of the source pad
|
||||
@param {String} destinationID the id of the destination pad
|
||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||
*/
|
||||
exports.copyPad = async (sourceID, destinationID, force) => {
|
||||
const pad = await getPadSafe(sourceID, true);
|
||||
|
@ -623,10 +582,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {padID: destinationID}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} sourceID the id of the source pad
|
||||
@param {String} destinationID the id of the destination pad
|
||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||
@param {String} authorId the id of the author, defaulting to empty string
|
||||
*/
|
||||
exports.copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => {
|
||||
const pad = await getPadSafe(sourceID, true);
|
||||
|
@ -641,9 +596,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {padID: destinationID}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} sourceID the id of the source pad
|
||||
@param {String} destinationID the id of the destination pad
|
||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||
*/
|
||||
exports.movePad = async (sourceID, destinationID, force) => {
|
||||
const pad = await getPadSafe(sourceID, true);
|
||||
|
@ -658,7 +610,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
*/
|
||||
exports.getReadOnlyID = async (padID) => {
|
||||
// we don't need the pad object, but this function does all the security stuff for us
|
||||
|
@ -677,7 +628,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {padID: padID}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} roID the readonly id of the pad
|
||||
*/
|
||||
exports.getPadID = async (roID) => {
|
||||
// get the PadId
|
||||
|
@ -696,8 +646,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {Boolean} publicStatus the public status of the pad
|
||||
*/
|
||||
exports.setPublicStatus = async (padID, publicStatus) => {
|
||||
// ensure this is a group pad
|
||||
|
@ -721,7 +669,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {publicStatus: true}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
*/
|
||||
exports.getPublicStatus = async (padID) => {
|
||||
// ensure this is a group pad
|
||||
|
@ -739,7 +686,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
*/
|
||||
exports.listAuthorsOfPad = async (padID) => {
|
||||
// get the pad
|
||||
|
@ -769,8 +715,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok"}
|
||||
{code: 1, message:"padID does not exist"}
|
||||
@param {String} padID the id of the pad
|
||||
@param {String} msg the message to send
|
||||
*/
|
||||
|
||||
exports.sendClientsMessage = async (padID, msg) => {
|
||||
|
@ -796,8 +740,6 @@ Example returns:
|
|||
|
||||
{code: 0, message:"ok", data: {chatHead: 42}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
@param {String} padID the id of the pad
|
||||
@return {Promise<{chatHead: number}>} the chatHead of the pad
|
||||
*/
|
||||
exports.getChatHead = async (padID) => {
|
||||
// get the pad
|
||||
|
@ -821,9 +763,7 @@ Example returns:
|
|||
}
|
||||
}
|
||||
{"code":4,"message":"no or wrong API Key","data":null}
|
||||
@param {String} padID the id of the pad
|
||||
@param {Number} startRev the start revision number
|
||||
@param {Number} endRev the end revision number
|
||||
|
||||
*/
|
||||
exports.createDiffHTML = async (padID, startRev, endRev) => {
|
||||
// check if startRev is a number
|
||||
|
@ -839,9 +779,11 @@ exports.createDiffHTML = async (padID, startRev, endRev) => {
|
|||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
const headRev = pad.getHeadRevisionNumber();
|
||||
if (startRev > headRev) startRev = headRev;
|
||||
if (startRev > headRev)
|
||||
startRev = headRev;
|
||||
|
||||
if (endRev > headRev) endRev = headRev;
|
||||
if (endRev > headRev)
|
||||
endRev = headRev;
|
||||
|
||||
let padDiff;
|
||||
try {
|
||||
|
@ -868,6 +810,7 @@ exports.createDiffHTML = async (padID, startRev, endRev) => {
|
|||
{"code":0,"message":"ok","data":{"totalPads":3,"totalSessions": 2,"totalActivePads": 1}}
|
||||
{"code":4,"message":"no or wrong API Key","data":null}
|
||||
*/
|
||||
|
||||
exports.getStats = async () => {
|
||||
const sessionInfos = padMessageHandler.sessioninfos;
|
||||
|
||||
|
|
|
@ -93,7 +93,6 @@ exports.getColorPalette = () => [
|
|||
|
||||
/**
|
||||
* Checks if the author exists
|
||||
* @param {String} authorID The id of the author
|
||||
*/
|
||||
exports.doesAuthorExist = async (authorID) => {
|
||||
const author = await db.get(`globalAuthor:${authorID}`);
|
||||
|
@ -101,12 +100,50 @@ exports.doesAuthorExist = async (authorID) => {
|
|||
return author != null;
|
||||
};
|
||||
|
||||
/**
|
||||
exported for backwards compatibility
|
||||
@param {String} authorID The id of the author
|
||||
*/
|
||||
/* exported for backwards compatibility */
|
||||
exports.doesAuthorExists = exports.doesAuthorExist;
|
||||
|
||||
const getAuthor4Token = async (token) => {
|
||||
const author = await mapAuthorWithDBKey('token2author', token);
|
||||
|
||||
// return only the sub value authorID
|
||||
return author ? author.authorID : author;
|
||||
};
|
||||
|
||||
exports.getAuthorId = async (token, user) => {
|
||||
const context = {dbKey: token, token, user};
|
||||
let [authorId] = await hooks.aCallFirst('getAuthorId', context);
|
||||
if (!authorId) authorId = await getAuthor4Token(context.dbKey);
|
||||
return authorId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a token.
|
||||
*
|
||||
* @deprecated Use `getAuthorId` instead.
|
||||
* @param {String} token The token
|
||||
*/
|
||||
exports.getAuthor4Token = async (token) => {
|
||||
warnDeprecated(
|
||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
||||
return await getAuthor4Token(token);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a mapper.
|
||||
* @param {String} token The mapper
|
||||
* @param {String} name The name of the author (optional)
|
||||
*/
|
||||
exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
|
||||
const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
|
||||
|
||||
if (name) {
|
||||
// set the name of this author
|
||||
await exports.setAuthorName(author.authorID, name);
|
||||
}
|
||||
|
||||
return author;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a mapper. We can map using a mapperkey,
|
||||
|
@ -137,60 +174,6 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => {
|
|||
return {authorID: author};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a token.
|
||||
* @param {String} token The token of the author
|
||||
* @return {Promise<string|*|{authorID: string}|{authorID: *}>}
|
||||
*/
|
||||
const getAuthor4Token = async (token) => {
|
||||
const author = await mapAuthorWithDBKey('token2author', token);
|
||||
|
||||
// return only the sub value authorID
|
||||
return author ? author.authorID : author;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a token.
|
||||
* @param {String} token
|
||||
* @param {Object} user
|
||||
* @return {Promise<*>}
|
||||
*/
|
||||
exports.getAuthorId = async (token, user) => {
|
||||
const context = {dbKey: token, token, user};
|
||||
let [authorId] = await hooks.aCallFirst('getAuthorId', context);
|
||||
if (!authorId) authorId = await getAuthor4Token(context.dbKey);
|
||||
return authorId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a token.
|
||||
*
|
||||
* @deprecated Use `getAuthorId` instead.
|
||||
* @param {String} token The token
|
||||
*/
|
||||
exports.getAuthor4Token = async (token) => {
|
||||
warnDeprecated(
|
||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
||||
return await getAuthor4Token(token);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a mapper.
|
||||
* @param {String} authorMapper The mapper
|
||||
* @param {String} name The name of the author (optional)
|
||||
*/
|
||||
exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
|
||||
const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
|
||||
|
||||
if (name) {
|
||||
// set the name of this author
|
||||
await exports.setAuthorName(author.authorID, name);
|
||||
}
|
||||
|
||||
return author;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Internal function that creates the database entry for an author
|
||||
* @param {String} name The name of the author
|
||||
|
@ -248,7 +231,7 @@ exports.setAuthorName = async (author, name) => await db.setSub(
|
|||
|
||||
/**
|
||||
* Returns an array of all pads this author contributed to
|
||||
* @param {String} authorID The id of the author
|
||||
* @param {String} author The id of the author
|
||||
*/
|
||||
exports.listPadsOfAuthor = async (authorID) => {
|
||||
/* There are two other places where this array is manipulated:
|
||||
|
@ -272,7 +255,7 @@ exports.listPadsOfAuthor = async (authorID) => {
|
|||
|
||||
/**
|
||||
* Adds a new pad to the list of contributions
|
||||
* @param {String} authorID The id of the author
|
||||
* @param {String} author The id of the author
|
||||
* @param {String} padID The id of the pad the author contributes to
|
||||
*/
|
||||
exports.addPad = async (authorID, padID) => {
|
||||
|
@ -299,7 +282,7 @@ exports.addPad = async (authorID, padID) => {
|
|||
|
||||
/**
|
||||
* Removes a pad from the list of contributions
|
||||
* @param {String} authorID The id of the author
|
||||
* @param {String} author The id of the author
|
||||
* @param {String} padID The id of the pad the author contributes to
|
||||
*/
|
||||
exports.removePad = async (authorID, padID) => {
|
||||
|
|
|
@ -25,10 +25,6 @@ const db = require('./DB');
|
|||
const padManager = require('./PadManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
|
||||
/**
|
||||
* Lists all groups
|
||||
* @return {Promise<{groupIDs: string[]}>} The ids of all groups
|
||||
*/
|
||||
exports.listAllGroups = async () => {
|
||||
let groups = await db.get('groups');
|
||||
groups = groups || {};
|
||||
|
@ -37,11 +33,6 @@ exports.listAllGroups = async () => {
|
|||
return {groupIDs};
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a group and all associated pads
|
||||
* @param {String} groupID The id of the group
|
||||
* @return {Promise<void>} Resolves when the group is deleted
|
||||
*/
|
||||
exports.deleteGroup = async (groupID) => {
|
||||
const group = await db.get(`group:${groupID}`);
|
||||
|
||||
|
@ -77,11 +68,6 @@ exports.deleteGroup = async (groupID) => {
|
|||
await db.remove(`group:${groupID}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a group exists
|
||||
* @param {String} groupID the id of the group to delete
|
||||
* @return {Promise<boolean>} Resolves to true if the group exists
|
||||
*/
|
||||
exports.doesGroupExist = async (groupID) => {
|
||||
// try to get the group entry
|
||||
const group = await db.get(`group:${groupID}`);
|
||||
|
@ -89,10 +75,6 @@ exports.doesGroupExist = async (groupID) => {
|
|||
return (group != null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new group
|
||||
* @return {Promise<{groupID: string}>} the id of the new group
|
||||
*/
|
||||
exports.createGroup = async () => {
|
||||
const groupID = `g.${randomString(16)}`;
|
||||
await db.set(`group:${groupID}`, {pads: {}, mappings: {}});
|
||||
|
@ -103,11 +85,6 @@ exports.createGroup = async () => {
|
|||
return {groupID};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new group if it does not exist already and returns the group ID
|
||||
* @param groupMapper the mapper of the group
|
||||
* @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID
|
||||
*/
|
||||
exports.createGroupIfNotExistsFor = async (groupMapper) => {
|
||||
if (typeof groupMapper !== 'string') {
|
||||
throw new CustomError('groupMapper is not a string', 'apierror');
|
||||
|
@ -126,14 +103,6 @@ exports.createGroupIfNotExistsFor = async (groupMapper) => {
|
|||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a group pad
|
||||
* @param {String} groupID The id of the group
|
||||
* @param {String} padName The name of the pad
|
||||
* @param {String} text The text of the pad
|
||||
* @param {String} authorId The id of the author
|
||||
* @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad
|
||||
*/
|
||||
exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
|
||||
// create the padID
|
||||
const padID = `${groupID}$${padName}`;
|
||||
|
@ -162,11 +131,6 @@ exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
|
|||
return {padID};
|
||||
};
|
||||
|
||||
/**
|
||||
* Lists all pads of a group
|
||||
* @param {String} groupID The id of the group
|
||||
* @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group
|
||||
*/
|
||||
exports.listPads = async (groupID) => {
|
||||
const exists = await exports.doesGroupExist(groupID);
|
||||
|
||||
|
|
|
@ -25,8 +25,7 @@ const promises = require('../utils/promises');
|
|||
/**
|
||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
||||
* line breaks and convert Tabs to spaces
|
||||
* @param {String} txt The text to clean
|
||||
* @returns {String} The cleaned text
|
||||
* @param txt
|
||||
*/
|
||||
exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
|
@ -74,16 +73,9 @@ class Pad {
|
|||
return this.publicStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a new revision
|
||||
* @param {Object} aChangeset The changeset to append to the pad
|
||||
* @param {String} authorId The id of the author
|
||||
* @return {Promise<number|string>}
|
||||
*/
|
||||
async appendRevision(aChangeset, authorId = '') {
|
||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
||||
this.head !== -1) {
|
||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs && this.head !== -1) {
|
||||
return this.head;
|
||||
}
|
||||
Changeset.copyAText(newAText, this.atext);
|
||||
|
@ -166,10 +158,6 @@ class Pad {
|
|||
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'atext']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all authors that worked on this pad
|
||||
* @return {[String]} The id of authors who contributed to this pad
|
||||
*/
|
||||
getAllAuthors() {
|
||||
const authorIds = [];
|
||||
|
||||
|
@ -185,7 +173,8 @@ class Pad {
|
|||
async getInternalRevisionAText(targetRev) {
|
||||
const keyRev = this.getKeyRevisionNumber(targetRev);
|
||||
const headRev = this.getHeadRevisionNumber();
|
||||
if (targetRev > headRev) targetRev = headRev;
|
||||
if (targetRev > headRev)
|
||||
targetRev = headRev;
|
||||
const [keyAText, changesets] = await Promise.all([
|
||||
this._getKeyRevisionAText(keyRev),
|
||||
Promise.all(
|
||||
|
|
|
@ -59,7 +59,6 @@ const padList = new class {
|
|||
|
||||
/**
|
||||
* Returns all pads in alphabetical order as array.
|
||||
* @returns {Promise<string[]>} A promise that resolves to an array of pad IDs.
|
||||
*/
|
||||
async getPads() {
|
||||
if (!this._loaded) {
|
||||
|
|
|
@ -26,15 +26,13 @@ const randomString = require('../utils/randomstring');
|
|||
|
||||
/**
|
||||
* checks if the id pattern matches a read-only pad id
|
||||
* @param {String} id the pad's id
|
||||
* @return {Boolean} true if the id is readonly
|
||||
* @param {String} the pad's id
|
||||
*/
|
||||
exports.isReadOnlyId = (id) => id.startsWith('r.');
|
||||
|
||||
/**
|
||||
* returns a read only id for a pad
|
||||
* @param {String} padId the id of the pad
|
||||
* @return {String} the read only id
|
||||
*/
|
||||
exports.getReadOnlyId = async (padId) => {
|
||||
// check if there is a pad2readonly entry
|
||||
|
@ -55,14 +53,12 @@ exports.getReadOnlyId = async (padId) => {
|
|||
/**
|
||||
* returns the padId for a read only id
|
||||
* @param {String} readOnlyId read only id
|
||||
* @return {String} the padId
|
||||
*/
|
||||
exports.getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`);
|
||||
|
||||
/**
|
||||
* returns the padId and readonlyPadId in an object for any id
|
||||
* @param {String} id read only id or real pad id
|
||||
* @return {Object} an object with the padId and readonlyPadId
|
||||
* @param {String} padIdOrReadonlyPadId read only id or real pad id
|
||||
*/
|
||||
exports.getIds = async (id) => {
|
||||
const readonly = exports.isReadOnlyId(id);
|
||||
|
|
|
@ -49,11 +49,6 @@ const DENY = Object.freeze({accessStatus: 'deny'});
|
|||
*
|
||||
* WARNING: Tokens and session IDs MUST be kept secret, otherwise users will be able to impersonate
|
||||
* each other (which might allow them to gain privileges).
|
||||
* @param {String} padID
|
||||
* @param {String} sessionCookie
|
||||
* @param {String} token
|
||||
* @param {Object} userSettings
|
||||
* @return {DENY|{accessStatus: String, authorID: String}}
|
||||
*/
|
||||
exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
|
||||
if (!padID) {
|
||||
|
|
|
@ -81,11 +81,6 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
|
|||
return sessionInfo.authorID;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a session exists
|
||||
* @param {String} sessionID The id of the session
|
||||
* @return {Promise<boolean>} Resolves to true if the session exists
|
||||
*/
|
||||
exports.doesSessionExist = async (sessionID) => {
|
||||
// check if the database entry of this session exists
|
||||
const session = await db.get(`session:${sessionID}`);
|
||||
|
@ -94,10 +89,6 @@ exports.doesSessionExist = async (sessionID) => {
|
|||
|
||||
/**
|
||||
* Creates a new session between an author and a group
|
||||
* @param {String} groupID The id of the group
|
||||
* @param {String} authorID The id of the author
|
||||
* @param {Number} validUntil The unix timestamp when the session should expire
|
||||
* @return {Promise<{sessionID: string}>} the id of the new session
|
||||
*/
|
||||
exports.createSession = async (groupID, authorID, validUntil) => {
|
||||
// check if the group exists
|
||||
|
@ -155,11 +146,6 @@ exports.createSession = async (groupID, authorID, validUntil) => {
|
|||
return {sessionID};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the sessioninfos for a session
|
||||
* @param {String} sessionID The id of the session
|
||||
* @return {Promise<Object>} the sessioninfos
|
||||
*/
|
||||
exports.getSessionInfo = async (sessionID) => {
|
||||
// check if the database entry of this session exists
|
||||
const session = await db.get(`session:${sessionID}`);
|
||||
|
@ -175,8 +161,6 @@ exports.getSessionInfo = async (sessionID) => {
|
|||
|
||||
/**
|
||||
* Deletes a session
|
||||
* @param {String} sessionID The id of the session
|
||||
* @return {Promise<void>} Resolves when the session is deleted
|
||||
*/
|
||||
exports.deleteSession = async (sessionID) => {
|
||||
// ensure that the session exists
|
||||
|
@ -202,11 +186,6 @@ exports.deleteSession = async (sessionID) => {
|
|||
await db.remove(`session:${sessionID}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of all sessions of a group
|
||||
* @param {String} groupID The id of the group
|
||||
* @return {Promise<Object>} The sessioninfos of all sessions of this group
|
||||
*/
|
||||
exports.listSessionsOfGroup = async (groupID) => {
|
||||
// check that the group exists
|
||||
const exists = await groupManager.doesGroupExist(groupID);
|
||||
|
@ -218,11 +197,6 @@ exports.listSessionsOfGroup = async (groupID) => {
|
|||
return sessions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of all sessions of an author
|
||||
* @param {String} authorID The id of the author
|
||||
* @return {Promise<Object>} The sessioninfos of all sessions of this author
|
||||
*/
|
||||
exports.listSessionsOfAuthor = async (authorID) => {
|
||||
// check that the author exists
|
||||
const exists = await authorManager.doesAuthorExist(authorID);
|
||||
|
@ -230,16 +204,12 @@ exports.listSessionsOfAuthor = async (authorID) => {
|
|||
throw new CustomError('authorID does not exist', 'apierror');
|
||||
}
|
||||
|
||||
return await listSessionsWithDBKey(`author2sessions:${authorID}`);
|
||||
const sessions = await listSessionsWithDBKey(`author2sessions:${authorID}`);
|
||||
return sessions;
|
||||
};
|
||||
|
||||
// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common
|
||||
// required to return null rather than an empty object if there are none
|
||||
/**
|
||||
* Returns an array of all sessions of a group
|
||||
* @param {String} dbkey The db key to use to get the sessions
|
||||
* @return {Promise<*>}
|
||||
*/
|
||||
const listSessionsWithDBKey = async (dbkey) => {
|
||||
// get the group2sessions entry
|
||||
const sessionObject = await db.get(dbkey);
|
||||
|
@ -248,7 +218,8 @@ const listSessionsWithDBKey = async (dbkey) => {
|
|||
// iterate through the sessions and get the sessioninfos
|
||||
for (const sessionID of Object.keys(sessions || {})) {
|
||||
try {
|
||||
sessions[sessionID] = await exports.getSessionInfo(sessionID);
|
||||
const sessionInfo = await exports.getSessionInfo(sessionID);
|
||||
sessions[sessionID] = sessionInfo;
|
||||
} catch (err) {
|
||||
if (err.name === 'apierror') {
|
||||
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
|
||||
|
@ -262,9 +233,5 @@ const listSessionsWithDBKey = async (dbkey) => {
|
|||
return sessions;
|
||||
};
|
||||
|
||||
/**
|
||||
* checks if a number is an int
|
||||
* @param {number|string} value
|
||||
* @return {boolean} If the value is an integer
|
||||
*/
|
||||
// checks if a number is an int
|
||||
const isInt = (value) => (parseFloat(value) === parseInt(value)) && !isNaN(value);
|
||||
|
|
|
@ -165,13 +165,12 @@ exports.version = version;
|
|||
|
||||
/**
|
||||
* Handles a HTTP API call
|
||||
* @param {String} apiVersion the version of the api
|
||||
* @param {String} functionName the name of the called function
|
||||
* @param functionName the name of the called function
|
||||
* @param fields the params of the called function
|
||||
* @req express request object
|
||||
* @res express response object
|
||||
*/
|
||||
exports.handle = async function (apiVersion, functionName, fields) {
|
||||
exports.handle = async function (apiVersion, functionName, fields, req, res) {
|
||||
// say goodbye if this is an unknown API version
|
||||
if (!(apiVersion in version)) {
|
||||
throw new createHTTPError.NotFound('no such api version');
|
||||
|
|
|
@ -38,11 +38,6 @@ const tempDirectory = os.tmpdir();
|
|||
|
||||
/**
|
||||
* do a requested export
|
||||
* @param {Object} req the request object
|
||||
* @param {Object} res the response object
|
||||
* @param {String} padId the pad id to export
|
||||
* @param {String} readOnlyId the read only id of the pad to export
|
||||
* @param {String} type the type to export
|
||||
*/
|
||||
exports.doExport = async (req, res, padId, readOnlyId, type) => {
|
||||
// avoid naming the read-only file as the original pad's id
|
||||
|
|
|
@ -73,10 +73,6 @@ const tmpDirectory = os.tmpdir();
|
|||
|
||||
/**
|
||||
* do a requested import
|
||||
* @param {Object} req the request object
|
||||
* @param {Object} res the response object
|
||||
* @param {String} padId the pad id to export
|
||||
* @param {String} authorId the author id to use for the import
|
||||
*/
|
||||
const doImport = async (req, res, padId, authorId) => {
|
||||
// pipe to a file
|
||||
|
@ -237,14 +233,6 @@ const doImport = async (req, res, padId, authorId) => {
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the request to import a file
|
||||
* @param {Request} req the request object
|
||||
* @param {Response} res the response object
|
||||
* @param {String} padId the pad id to export
|
||||
* @param {String} authorId the author id to use for the import
|
||||
* @return {Promise<void>} a promise
|
||||
*/
|
||||
exports.doImport = async (req, res, padId, authorId = '') => {
|
||||
let httpStatus = 200;
|
||||
let code = 0;
|
||||
|
|
|
@ -35,9 +35,8 @@ const components = {};
|
|||
|
||||
let io;
|
||||
|
||||
/** adds a component
|
||||
* @param {string} moduleName
|
||||
* @param {Module} module
|
||||
/**
|
||||
* adds a component
|
||||
*/
|
||||
exports.addComponent = (moduleName, module) => {
|
||||
if (module == null) return exports.deleteComponent(moduleName);
|
||||
|
@ -45,15 +44,10 @@ exports.addComponent = (moduleName, module) => {
|
|||
module.setSocketIO(io);
|
||||
};
|
||||
|
||||
/**
|
||||
* removes a component
|
||||
* @param {Module} moduleName
|
||||
*/
|
||||
exports.deleteComponent = (moduleName) => { delete components[moduleName]; };
|
||||
|
||||
/**
|
||||
* sets the socket.io and adds event functions for routing
|
||||
* @param {Object} _io the socket.io instance
|
||||
*/
|
||||
exports.setSocketIO = (_io) => {
|
||||
io = _io;
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
'use strict';
|
||||
const eejs = require('../../eejs');
|
||||
|
||||
|
||||
/**
|
||||
* Add the admin navigation link
|
||||
* @param hookName {String} the name of the hook
|
||||
* @param args {Object} the object containing the arguments
|
||||
* @param {Function} cb the callback function
|
||||
* @return {*}
|
||||
*/
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
args.app.get('/admin', (req, res) => {
|
||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
||||
|
|
|
@ -121,13 +121,6 @@ exports.socketio = (hookName, args, cb) => {
|
|||
return cb();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts a list of plugins by a property
|
||||
* @param {Object} plugins The plugins to sort
|
||||
* @param {Object} property The property to sort by
|
||||
* @param {String} dir The directory of the plugin
|
||||
* @return {Object[]}
|
||||
*/
|
||||
const sortPluginList = (plugins, property, /* ASC?*/dir) => plugins.sort((a, b) => {
|
||||
if (a[property] < b[property]) {
|
||||
return dir ? -1 : 1;
|
||||
|
|
|
@ -11,16 +11,13 @@ const securityManager = require('../../db/SecurityManager');
|
|||
const webaccess = require('./webaccess');
|
||||
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
const limiter = rateLimit({
|
||||
...settings.importExportRateLimiting,
|
||||
handler: (request, response, next, options) => {
|
||||
if (request.rateLimit.current === request.rateLimit.limit + 1) {
|
||||
// when the rate limiter triggers, write a warning in the logs
|
||||
console.warn('Import/Export rate limiter triggered on ' +
|
||||
`"${request.originalUrl}" for IP address ${request.ip}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
settings.importExportRateLimiting.onLimitReached = (req, res, options) => {
|
||||
// when the rate limiter triggers, write a warning in the logs
|
||||
console.warn('Import/Export rate limiter triggered on ' +
|
||||
`"${req.originalUrl}" for IP address ${req.ip}`);
|
||||
};
|
||||
// The rate limiter is created in this hook so that restarting the server resets the limiter.
|
||||
const limiter = rateLimit(settings.importExportRateLimiting);
|
||||
|
||||
// handle export requests
|
||||
args.app.use('/p/:pad/:rev?/export/:type', limiter);
|
||||
|
|
|
@ -696,20 +696,10 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get the current root path for an API version
|
||||
* @param {String} version The API version
|
||||
* @param {APIPathStyle} style The style of the API path
|
||||
* @return {String} The root path for the API version
|
||||
*/
|
||||
// helper to get api root
|
||||
const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`;
|
||||
|
||||
/**
|
||||
* Helper to generate an OpenAPI server object when serving definitions
|
||||
* @param {String} apiRoot The root path for the API version
|
||||
* @param {Request} req The express request object
|
||||
* @return {url: String} The server object for the OpenAPI definition location
|
||||
*/
|
||||
// helper to generate an OpenAPI server object when serving definitions
|
||||
const generateServerForApiVersion = (apiRoot, req) => ({
|
||||
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
|
||||
});
|
||||
|
|
|
@ -12,4 +12,4 @@ exports.hkdf = util.promisify(crypto.hkdf);
|
|||
/**
|
||||
* Promisified version of Node.js's crypto.randomBytes
|
||||
*/
|
||||
exports.randomBytes = util.promisify(crypto.randomBytes);
|
||||
exports.randomBytes = util.promisify(crypto.randomBytes);
|
|
@ -25,6 +25,7 @@
|
|||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
log4js.replaceConsole();
|
||||
|
||||
const settings = require('./utils/Settings');
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ for (let i = 0; i < argv.length; i++) {
|
|||
exports.argv.sessionkey = arg;
|
||||
}
|
||||
|
||||
// Override location of APIKEY.txt file
|
||||
// Override location of settings.json file
|
||||
if (prevArg === '--apikey') {
|
||||
exports.argv.apikey = arg;
|
||||
}
|
||||
|
|
|
@ -50,24 +50,15 @@ const nonSettings = [
|
|||
|
||||
// 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. :(
|
||||
const defaultLogConfig = () => ({appenders: {console: {type: 'console'}},
|
||||
categories: {
|
||||
default: {appenders: ['console'], level: 'info'},
|
||||
}});
|
||||
const defaultLogConfig = () => ({appenders: [{type: 'console'}]});
|
||||
const defaultLogLevel = 'INFO';
|
||||
|
||||
const initLogging = (logLevel, config) => {
|
||||
// log4js.configure() modifies exports.logconfig so check for equality first.
|
||||
const logConfigIsDefault = deepEqual(config, defaultLogConfig());
|
||||
log4js.configure(config);
|
||||
log4js.getLogger('console');
|
||||
|
||||
// Overwrites for console output methods
|
||||
console.debug = logger.debug.bind(logger);
|
||||
console.log = logger.info.bind(logger);
|
||||
console.warn = logger.warn.bind(logger);
|
||||
console.error = logger.error.bind(logger);
|
||||
|
||||
log4js.setGlobalLogLevel(logLevel);
|
||||
log4js.replaceConsole();
|
||||
// Log the warning after configuring log4js to increase the chances the user will see it.
|
||||
if (!logConfigIsDefault) logger.warn('The logconfig setting is deprecated.');
|
||||
};
|
||||
|
|
|
@ -2,39 +2,28 @@
|
|||
const semver = require('semver');
|
||||
const settings = require('./Settings');
|
||||
const axios = require('axios');
|
||||
const headers = {
|
||||
'User-Agent': 'Etherpad/' + settings.getEpVersion(),
|
||||
}
|
||||
|
||||
const updateInterval = 60 * 60 * 1000; // 1 hour
|
||||
let infos;
|
||||
let lastLoadingTime = null;
|
||||
|
||||
const loadEtherpadInformations = () => {
|
||||
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
|
||||
return Promise.resolve(infos);
|
||||
}
|
||||
|
||||
return axios.get('https://static.etherpad.org/info.json', {headers: headers})
|
||||
.then(async resp => {
|
||||
infos = await resp.data;
|
||||
if (infos === undefined || infos === null) {
|
||||
await Promise.reject("Could not retrieve current version")
|
||||
return
|
||||
}
|
||||
|
||||
lastLoadingTime = Date.now();
|
||||
return await Promise.resolve(infos);
|
||||
})
|
||||
.catch(async err => {
|
||||
return await Promise.reject(err);
|
||||
});
|
||||
}
|
||||
const loadEtherpadInformations = () =>
|
||||
axios.get('https://static.etherpad.org/info.json')
|
||||
.then(async resp => {
|
||||
try {
|
||||
infos = await resp.data;
|
||||
if (infos === undefined || infos === null) {
|
||||
await Promise.reject("Could not retrieve current version")
|
||||
return
|
||||
}
|
||||
return await Promise.resolve(infos);
|
||||
}
|
||||
catch (err) {
|
||||
return await Promise.reject(err);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
exports.getLatestVersion = () => {
|
||||
exports.needsUpdate().catch();
|
||||
return infos?.latestVersion;
|
||||
exports.needsUpdate();
|
||||
return infos.latestVersion;
|
||||
};
|
||||
|
||||
exports.needsUpdate = async (cb) => {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -31,7 +31,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"async": "^3.2.4",
|
||||
"axios": "^1.6.0",
|
||||
"axios": "^1.5.0",
|
||||
"clean-css": "^5.3.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cross-spawn": "^7.0.3",
|
||||
|
@ -39,7 +39,7 @@
|
|||
"etherpad-require-kernel": "^1.0.15",
|
||||
"etherpad-yajsml": "0.0.12",
|
||||
"express": "4.18.2",
|
||||
"express-rate-limit": "^7.1.3",
|
||||
"express-rate-limit": "^7.0.0",
|
||||
"express-session": "npm:@etherpad/express-session@^1.18.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"find-root": "1.1.0",
|
||||
|
@ -50,24 +50,24 @@
|
|||
"jsonminify": "0.4.2",
|
||||
"languages4translatewiki": "0.1.3",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"log4js": "^6.9.1",
|
||||
"log4js": "0.6.38",
|
||||
"measured-core": "^2.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"npm": "^6.14.18",
|
||||
"openapi-backend": "^5.10.5",
|
||||
"openapi-backend": "^5.9.2",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"rate-limiter-flexible": "^3.0.3",
|
||||
"rate-limiter-flexible": "^3.0.0",
|
||||
"rehype": "^13.0.1",
|
||||
"rehype-minify-whitespace": "^6.0.0",
|
||||
"resolve": "1.22.8",
|
||||
"resolve": "1.22.4",
|
||||
"security": "1.0.0",
|
||||
"semver": "^7.5.4",
|
||||
"socket.io": "^2.5.0",
|
||||
"superagent": "^8.1.2",
|
||||
"terser": "^5.24.0",
|
||||
"terser": "^5.19.4",
|
||||
"threads": "^1.7.0",
|
||||
"tinycon": "0.6.8",
|
||||
"ueberdb2": "^4.2.35",
|
||||
"ueberdb2": "^4.2.1",
|
||||
"underscore": "1.13.6",
|
||||
"unorm": "1.6.0",
|
||||
"wtfnode": "^0.9.1"
|
||||
|
@ -78,16 +78,16 @@
|
|||
"etherpad-lite": "node/server.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.52.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-etherpad": "^3.0.22",
|
||||
"etherpad-cli-client": "^2.0.2",
|
||||
"mocha": "^10.0.0",
|
||||
"mocha-froth": "^0.2.10",
|
||||
"nodeify": "^1.0.1",
|
||||
"openapi-schema-validation": "^0.4.2",
|
||||
"selenium-webdriver": "^4.15.0",
|
||||
"selenium-webdriver": "^4.12.0",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"sinon": "^17.0.1",
|
||||
"sinon": "^15.2.0",
|
||||
"split-grid": "^1.0.11",
|
||||
"supertest": "^6.3.3",
|
||||
"typescript": "^4.9.5"
|
||||
|
@ -103,8 +103,9 @@
|
|||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
||||
"test-container": "mocha --timeout 5000 tests/container/specs/api"
|
||||
"test-container": "mocha --timeout 5000 tests/container/specs/api",
|
||||
"dev": "bash ./bin/run.sh"
|
||||
},
|
||||
"version": "1.9.4",
|
||||
"version": "1.9.2",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
|
@ -3466,7 +3466,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// use that for displaying the side div line number inline with the first line
|
||||
// of content -- This is used in ep_headings, ep_font_size etc. where the line
|
||||
// height is increased.
|
||||
const elementStyle = window.getComputedStyle(docLine.firstChild);
|
||||
const elementStyle = window.getComputedStyle(docLine.firstElementChild);
|
||||
const lineHeight = parseInt(elementStyle.getPropertyValue('line-height'));
|
||||
const marginBottom = parseInt(elementStyle.getPropertyValue('margin-bottom'));
|
||||
lineHeights.push(lineHeight + marginBottom);
|
||||
|
|
|
@ -318,7 +318,7 @@ exports.padeditbar = new class {
|
|||
}
|
||||
} else {
|
||||
// Focus on the editbar :)
|
||||
const firstEditbarElement = $('#editbar button').first();
|
||||
const firstEditbarElement = parent.parent.$('#editbar button').first();
|
||||
|
||||
$(evt.currentTarget).trigger('blur');
|
||||
firstEditbarElement.trigger('focus');
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title><%- padId %></title>
|
||||
<meta name="generator" content="Etherpad"/>
|
||||
<meta name="author" content="Etherpad"/>
|
||||
<meta name="changedby" content="Etherpad"/>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="generator" content="Etherpad">
|
||||
<meta name="author" content="Etherpad">
|
||||
<meta name="changedby" content="Etherpad">
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
ol {
|
||||
counter-reset: item;
|
||||
|
|
|
@ -42,6 +42,8 @@ exports.init = async function () {
|
|||
if (!logLevel.isLessThanOrEqualTo(log4js.levels.DEBUG)) {
|
||||
logger.warn('Disabling non-test logging for the duration of the test. ' +
|
||||
'To enable non-test logging, change the loglevel setting to DEBUG.');
|
||||
log4js.setGlobalLogLevel(log4js.levels.OFF);
|
||||
logger.setLevel(logLevel);
|
||||
}
|
||||
|
||||
// Note: This is only a shallow backup.
|
||||
|
@ -64,6 +66,7 @@ exports.init = async function () {
|
|||
webaccess.authnFailureDelayMs = backups.authnFailureDelayMs;
|
||||
// Note: This does not unset settings that were added.
|
||||
Object.assign(settings, backups.settings);
|
||||
log4js.setGlobalLogLevel(logLevel);
|
||||
await server.exit();
|
||||
});
|
||||
|
||||
|
|
|
@ -36,10 +36,15 @@ const helper = {};
|
|||
await p;
|
||||
};
|
||||
|
||||
if (!win.$) await load('../../static/js/vendors/jquery.js');
|
||||
// sendkeys.js depends on jQuery, so it cannot be loaded until jQuery has finished loading. (In
|
||||
// other words, do not load both jQuery and sendkeys inside a Promise.all() call.)
|
||||
if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js');
|
||||
await Promise.all([
|
||||
!win.$ && load('../../static/js/vendors/jquery.js'),
|
||||
!win.bililiteRange && includeSendkeys && load('../tests/frontend/lib/bililiteRange.js'),
|
||||
]);
|
||||
// jquery.sendkeys.js depends on jQuery, so it cannot be loaded until jQuery has finished
|
||||
// loading. (In other words, do not load sendkeys in the above Promise.all() call.)
|
||||
if (!win.$.fn.sendkeys && includeSendkeys) {
|
||||
await load('../tests/frontend/lib/jquery.sendkeys.js');
|
||||
}
|
||||
|
||||
win.$.window = win;
|
||||
win.$.document = doc;
|
||||
|
|
|
@ -49,7 +49,8 @@ helper.edit = async (message, line) => {
|
|||
*
|
||||
* @returns {Array.<HTMLElement>} array of divs
|
||||
*/
|
||||
helper.linesDiv = () => helper.padInner$('.ace-line').map(function () { return $(this); }).get();
|
||||
helper.linesDiv =
|
||||
() => helper.padInner$('.ace-line').map(function () { return helper.padInner$(this); }).get();
|
||||
|
||||
/**
|
||||
* The pad text as an array of lines
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
|
||||
<script src="../../static/js/require-kernel.js"></script>
|
||||
<script src="../../static/js/vendors/jquery.js"></script>
|
||||
<script src="lib/sendkeys.js"></script>
|
||||
<script src="lib/bililiteRange.js"></script>
|
||||
<script src="lib/jquery.sendkeys.js"></script>
|
||||
<script src="../../static/js/vendors/browser.js"></script>
|
||||
<script src="../../static/plugins/js-cookie/dist/js.cookie.js"></script>
|
||||
<script src="lib/underscore.js"></script>
|
||||
|
|
|
@ -0,0 +1,506 @@
|
|||
// Cross-broswer implementation of text ranges and selections
|
||||
// documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/
|
||||
// Version: 2.0
|
||||
// Copyright (c) 2013 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function(){
|
||||
|
||||
bililiteRange = function(el, debug){
|
||||
var ret;
|
||||
if (debug){
|
||||
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
|
||||
}else if (document.selection && !document.addEventListener){
|
||||
// Internet Explorer 8 and lower
|
||||
ret = new IERange();
|
||||
}else if (window.getSelection && el.setSelectionRange){
|
||||
// Standards. Element is an input or textarea
|
||||
ret = new InputRange();
|
||||
}else if (window.getSelection){
|
||||
// Standards, with any other kind of element
|
||||
ret = new W3CRange()
|
||||
}else{
|
||||
// doesn't support selection
|
||||
ret = new NothingRange();
|
||||
}
|
||||
ret._el = el;
|
||||
// determine parent document, as implemented by John McLear <john@mclear.co.uk>
|
||||
ret._doc = el.ownerDocument;
|
||||
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow;
|
||||
ret._textProp = textProp(el);
|
||||
ret._bounds = [0, ret.length()];
|
||||
if (!('oninput' in el)){
|
||||
// give IE8 a chance
|
||||
var inputhack = function() {ret.dispatch({type: 'input'}) };
|
||||
ret.listen('keyup', inputhack);
|
||||
ret.listen('cut', inputhack);
|
||||
ret.listen('paste', inputhack);
|
||||
ret.listen('drop', inputhack);
|
||||
el.oninput = 'patched';
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function textProp(el){
|
||||
// returns the property that contains the text of the element
|
||||
// note that for <body> elements the text attribute represents the obsolete text color, not the textContent.
|
||||
// we document that these routines do not work for <body> elements so that should not be relevant
|
||||
if (typeof el.value != 'undefined') return 'value';
|
||||
if (typeof el.text != 'undefined') return 'text';
|
||||
if (typeof el.textContent != 'undefined') return 'textContent';
|
||||
return 'innerText';
|
||||
}
|
||||
|
||||
// base class
|
||||
function Range(){}
|
||||
Range.prototype = {
|
||||
length: function() {
|
||||
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
|
||||
},
|
||||
bounds: function(s){
|
||||
if (s === 'all'){
|
||||
this._bounds = [0, this.length()];
|
||||
}else if (s === 'start'){
|
||||
this._bounds = [0, 0];
|
||||
}else if (s === 'end'){
|
||||
this._bounds = [this.length(), this.length()];
|
||||
}else if (s === 'selection'){
|
||||
this.bounds ('all'); // first select the whole thing for constraining
|
||||
this._bounds = this._nativeSelection();
|
||||
}else if (s){
|
||||
this._bounds = s; // don't do error checking now; things may change at a moment's notice
|
||||
}else{
|
||||
var b = [
|
||||
Math.max(0, Math.min (this.length(), this._bounds[0])),
|
||||
Math.max(0, Math.min (this.length(), this._bounds[1]))
|
||||
];
|
||||
b[1] = Math.max(b[0], b[1]);
|
||||
return b; // need to constrain it to fit
|
||||
}
|
||||
return this; // allow for chaining
|
||||
},
|
||||
select: function(){
|
||||
this._nativeSelect(this._nativeRange(this.bounds()));
|
||||
this.dispatch({type: 'select'});
|
||||
return this; // allow for chaining
|
||||
},
|
||||
text: function(text, select){
|
||||
if (arguments.length){
|
||||
var bounds = this.bounds(), el = this._el;
|
||||
// signal the input per DOM 3 input events, http://www.w3.org/TR/DOM-Level-3-Events/#h4_events-inputevents
|
||||
// we add another field, bounds, which are the bounds of the original text before being changed.
|
||||
this.dispatch({type: 'beforeinput', data: text, bounds: bounds});
|
||||
this._nativeSetText(text, this._nativeRange(bounds));
|
||||
if (select == 'start'){
|
||||
this.bounds ([bounds[0], bounds[0]]);
|
||||
}else if (select == 'end'){
|
||||
this.bounds ([bounds[0]+text.length, bounds[0]+text.length]);
|
||||
}else if (select == 'all'){
|
||||
this.bounds ([bounds[0], bounds[0]+text.length]);
|
||||
}
|
||||
this.dispatch({type: 'input', data: text, bounds: bounds});
|
||||
return this; // allow for chaining
|
||||
}else{
|
||||
return this._nativeGetText(this._nativeRange(this.bounds()));
|
||||
}
|
||||
},
|
||||
insertEOL: function (){
|
||||
this._nativeEOL();
|
||||
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
|
||||
return this;
|
||||
},
|
||||
scrollIntoView: function(){
|
||||
this._nativeScrollIntoView(this._nativeRange(this.bounds()));
|
||||
return this;
|
||||
},
|
||||
wrap: function (n){
|
||||
this._nativeWrap(n, this._nativeRange(this.bounds()));
|
||||
return this;
|
||||
},
|
||||
selection: function(text){
|
||||
if (arguments.length){
|
||||
return this.bounds('selection').text(text, 'end').select();
|
||||
}else{
|
||||
return this.bounds('selection').text();
|
||||
}
|
||||
},
|
||||
clone: function(){
|
||||
return bililiteRange(this._el).bounds(this.bounds());
|
||||
},
|
||||
all: function(text){
|
||||
if (arguments.length){
|
||||
this.dispatch ({type: 'beforeinput', data: text});
|
||||
this._el[this._textProp] = text;
|
||||
this.dispatch ({type: 'input', data: text});
|
||||
return this;
|
||||
}else{
|
||||
return this._el[this._textProp].replace(/\r/g, ''); // need to correct for IE's CrLf weirdness;
|
||||
}
|
||||
},
|
||||
element: function() { return this._el },
|
||||
// includes a quickie polyfill for CustomEvent for IE that isn't perfect but works for me
|
||||
// IE10 allows custom events but not "new CustomEvent"; have to do it the old-fashioned way
|
||||
dispatch: function(opts){
|
||||
opts = opts || {};
|
||||
var event = document.createEvent ? document.createEvent('CustomEvent') : this._doc.createEventObject();
|
||||
event.initCustomEvent && event.initCustomEvent(opts.type, !!opts.bubbles, !!opts.cancelable, opts.detail);
|
||||
|
||||
for (var key in opts) event[key] = opts[key];
|
||||
// dispatch event asynchronously (in the sense of on the next turn of the event loop; still should be fired in order of dispatch
|
||||
var el = this._el;
|
||||
setTimeout(function(){
|
||||
try {
|
||||
el.dispatchEvent ? el.dispatchEvent(event) : el.fireEvent("on" + opts.type, document.createEventObject());
|
||||
}catch(e){
|
||||
// IE8 will not let me fire custom events at all. Call them directly
|
||||
if (jQuery) {
|
||||
jQuery(el).trigger(event);
|
||||
}else{
|
||||
var listeners = el['listen'+opts.type];
|
||||
if (listeners) for (var i = 0; i < listeners.length; ++i){
|
||||
listeners[i].call(el, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
return this;
|
||||
},
|
||||
listen: function (type, func){
|
||||
var el = this._el;
|
||||
if (el.addEventListener){
|
||||
el.addEventListener(type, func);
|
||||
}else if (jQuery){
|
||||
jQuery(el).on(type, func);
|
||||
}else{
|
||||
el.attachEvent("on" + type, func);
|
||||
// IE8 can't even handle custom events created with createEventObject (though it permits attachEvent), so we have to make our own
|
||||
var listeners = el['listen'+type] = el['listen'+type] || [];
|
||||
listeners.push(func);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
dontlisten: function (type, func){
|
||||
var el = this._el;
|
||||
if (el.removeEventListener){
|
||||
el.removeEventListener(type, func);
|
||||
}else if (jQuery){
|
||||
jQuery(el).off(type, func);
|
||||
}else try{
|
||||
el.detachEvent("on" + type, func);
|
||||
}catch(e){
|
||||
var listeners = el['listen'+type];
|
||||
if (listeners) for (var i = 0; i < listeners.length; ++i){
|
||||
if (listeners[i] === func) listeners[i] = function(){}; // replace with a noop
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// allow extensions ala jQuery
|
||||
bililiteRange.fn = Range.prototype; // to allow monkey patching
|
||||
bililiteRange.extend = function(fns){
|
||||
for (fn in fns) Range.prototype[fn] = fns[fn];
|
||||
};
|
||||
|
||||
function IERange(){}
|
||||
IERange.prototype = new Range();
|
||||
IERange.prototype._nativeRange = function (bounds){
|
||||
var rng;
|
||||
if (this._el.tagName == 'INPUT'){
|
||||
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
|
||||
rng = this._el.createTextRange();
|
||||
}else{
|
||||
rng = this._doc.body.createTextRange ();
|
||||
rng.moveToElementText(this._el);
|
||||
}
|
||||
if (bounds){
|
||||
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
|
||||
if (bounds[0] > this.length()) bounds[0] = this.length();
|
||||
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf wierdness
|
||||
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
|
||||
rng.moveEnd ('character', -1);
|
||||
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length);
|
||||
}
|
||||
if (bounds[0] > 0) rng.moveStart('character', bounds[0]);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
IERange.prototype._nativeSelect = function (rng){
|
||||
rng.select();
|
||||
};
|
||||
IERange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
// this fails for an empty selection! selection.createRange() if in a text area does not create a text selection, so I can't compare it.
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
var len = this.length();
|
||||
// this._el.focus(); This solves the problem of text areas not having a real selection , but sucks the focus from everything else, so I can't use it
|
||||
var sel = this._doc.selection.createRange();
|
||||
try{
|
||||
return [
|
||||
iestart(sel, rng),
|
||||
ieend (sel, rng)
|
||||
];
|
||||
}catch (e){
|
||||
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
|
||||
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len];
|
||||
}
|
||||
};
|
||||
IERange.prototype._nativeGetText = function (rng){
|
||||
return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness
|
||||
};
|
||||
IERange.prototype._nativeSetText = function (text, rng){
|
||||
rng.text = text;
|
||||
};
|
||||
IERange.prototype._nativeEOL = function(){
|
||||
if ('value' in this._el){
|
||||
this.text('\n'); // for input and textarea, insert it straight
|
||||
}else{
|
||||
this._nativeRange(this.bounds()).pasteHTML('\n<br/>');
|
||||
}
|
||||
};
|
||||
IERange.prototype._nativeScrollIntoView = function(rng){
|
||||
rng.scrollIntoView();
|
||||
}
|
||||
IERange.prototype._nativeWrap = function(n, rng) {
|
||||
// hacky to use string manipulation but I don't see another way to do it.
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(n);
|
||||
// insert the existing range HTML after the first tag
|
||||
var html = div.innerHTML.replace('><', '>'+rng.htmlText+'<');
|
||||
rng.pasteHTML(html);
|
||||
};
|
||||
|
||||
// IE internals
|
||||
function iestart(rng, constraint){
|
||||
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len;
|
||||
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1));
|
||||
return i;
|
||||
}
|
||||
function ieend (rng, constraint){
|
||||
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
|
||||
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0;
|
||||
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1));
|
||||
return i;
|
||||
}
|
||||
|
||||
// an input element in a standards document. "Native Range" is just the bounds array
|
||||
function InputRange(){}
|
||||
InputRange.prototype = new Range();
|
||||
InputRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0, this.length()];
|
||||
};
|
||||
InputRange.prototype._nativeSelect = function (rng){
|
||||
this._el.setSelectionRange(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSelection = function(){
|
||||
return [this._el.selectionStart, this._el.selectionEnd];
|
||||
};
|
||||
InputRange.prototype._nativeGetText = function(rng){
|
||||
return this._el.value.substring(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSetText = function(text, rng){
|
||||
var val = this._el.value;
|
||||
this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
InputRange.prototype._nativeScrollIntoView = function(rng){
|
||||
// I can't remember where I found this clever hack to find the location of text in a text area
|
||||
var clone = this._el.cloneNode(true);
|
||||
clone.style.visibility = 'hidden';
|
||||
clone.style.position = 'absolute';
|
||||
this._el.parentNode.insertBefore(clone, this._el);
|
||||
clone.style.height = '1px';
|
||||
clone.value = this._el.value.slice(0, rng[0]);
|
||||
var top = clone.scrollHeight;
|
||||
// this gives the bottom of the text, so we have to subtract the height of a single line
|
||||
clone.value = 'X';
|
||||
top -= 2*clone.scrollHeight; // show at least a line above
|
||||
clone.parentNode.removeChild(clone);
|
||||
// scroll into position if necessary
|
||||
if (this._el.scrollTop > top || this._el.scrollTop+this._el.clientHeight < top){
|
||||
this._el.scrollTop = top;
|
||||
}
|
||||
// now scroll the element into view; get its position as in jQuery.offset
|
||||
var rect = this._el.getBoundingClientRect();
|
||||
rect.top += this._win.pageYOffset - this._doc.documentElement.clientTop;
|
||||
rect.left += this._win.pageXOffset - this._doc.documentElement.clientLeft;
|
||||
// create an element to scroll to (can't just use the clone above, since scrollIntoView wants a visible element)
|
||||
var div = this._doc.createElement('div');
|
||||
div.style.position = 'absolute';
|
||||
div.style.top = (rect.top+top-this._el.scrollTop)+'px'; // adjust for how far in the range is; it may not have scrolled all the way to the top
|
||||
div.style.left = rect.left+'px';
|
||||
div.innerHTML = ' ';
|
||||
this._doc.body.appendChild(div);
|
||||
div.scrollIntoViewIfNeeded ? div.scrollIntoViewIfNeeded() : div.scrollIntoView();
|
||||
div.parentNode.removeChild(div);
|
||||
}
|
||||
InputRange.prototype._nativeWrap = function() {throw new Error("Cannot wrap in a text element")};
|
||||
|
||||
function W3CRange(){}
|
||||
W3CRange.prototype = new Range();
|
||||
W3CRange.prototype._nativeRange = function (bounds){
|
||||
var rng = this._doc.createRange();
|
||||
rng.selectNodeContents(this._el);
|
||||
if (bounds){
|
||||
w3cmoveBoundary (rng, bounds[0], true, this._el);
|
||||
rng.collapse (true);
|
||||
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
W3CRange.prototype._nativeSelect = function (rng){
|
||||
this._win.getSelection().removeAllRanges();
|
||||
this._win.getSelection().addRange (rng);
|
||||
};
|
||||
W3CRange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
|
||||
var sel = this._win.getSelection().getRangeAt(0);
|
||||
return [
|
||||
w3cstart(sel, rng),
|
||||
w3cend (sel, rng)
|
||||
];
|
||||
}
|
||||
W3CRange.prototype._nativeGetText = function (rng){
|
||||
return rng.toString();
|
||||
};
|
||||
W3CRange.prototype._nativeSetText = function (text, rng){
|
||||
rng.deleteContents();
|
||||
rng.insertNode (this._doc.createTextNode(text));
|
||||
this._el.normalize(); // merge the text with the surrounding text
|
||||
};
|
||||
W3CRange.prototype._nativeEOL = function(){
|
||||
var rng = this._nativeRange(this.bounds());
|
||||
rng.deleteContents();
|
||||
var br = this._doc.createElement('br');
|
||||
br.setAttribute ('_moz_dirty', ''); // for Firefox
|
||||
rng.insertNode (br);
|
||||
rng.insertNode (this._doc.createTextNode('\n'));
|
||||
rng.collapse (false);
|
||||
};
|
||||
W3CRange.prototype._nativeScrollIntoView = function(rng){
|
||||
// can't scroll to a range; have to scroll to an element instead
|
||||
var span = this._doc.createElement('span');
|
||||
rng.insertNode(span);
|
||||
span.scrollIntoViewIfNeeded ? span.scrollIntoViewIfNeeded() : span.scrollIntoView();
|
||||
span.parentNode.removeChild(span);
|
||||
}
|
||||
W3CRange.prototype._nativeWrap = function(n, rng) {
|
||||
rng.surroundContents(n);
|
||||
};
|
||||
|
||||
// W3C internals
|
||||
function nextnode (node, root){
|
||||
// in-order traversal
|
||||
// we've already visited node, so get kids then siblings
|
||||
if (node.firstChild) return node.firstChild;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
if (node===root) return null;
|
||||
while (node.parentNode){
|
||||
// get uncles
|
||||
node = node.parentNode;
|
||||
if (node == root) return null;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function w3cmoveBoundary (rng, n, bStart, el){
|
||||
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
|
||||
// if the start is moved after the end, then an exception is raised
|
||||
if (n <= 0) return;
|
||||
var node = rng[bStart ? 'startContainer' : 'endContainer'];
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
// we may be starting somewhere into the text
|
||||
n += rng[bStart ? 'startOffset' : 'endOffset'];
|
||||
}
|
||||
for (; node; node = nextnode(node, el)) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
if (n < node.nodeValue.length) {
|
||||
rng[bStart ? 'setStart' : 'setEnd'](node, n);
|
||||
return;
|
||||
}
|
||||
n -= node.nodeValue.length;
|
||||
}
|
||||
if (!node.firstChild) rng[bStart ? 'setStartAfter' : 'setEndAfter'](node);
|
||||
}
|
||||
}
|
||||
var START_TO_START = 0; // from the w3c definitions
|
||||
var START_TO_END = 1;
|
||||
var END_TO_END = 2;
|
||||
var END_TO_START = 3;
|
||||
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
|
||||
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
|
||||
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
|
||||
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
|
||||
function w3cstart(rng, constraint){
|
||||
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
|
||||
return constraint.toString().length - rng.toString().length;
|
||||
}
|
||||
function w3cend (rng, constraint){
|
||||
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
|
||||
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
|
||||
return rng.toString().length;
|
||||
}
|
||||
|
||||
function NothingRange(){}
|
||||
NothingRange.prototype = new Range();
|
||||
NothingRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0,this.length()];
|
||||
};
|
||||
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
|
||||
};
|
||||
NothingRange.prototype._nativeSelection = function(){
|
||||
return [0,0];
|
||||
};
|
||||
NothingRange.prototype._nativeGetText = function (rng){
|
||||
return this._el[this._textProp].substring(rng[0], rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeSetText = function (text, rng){
|
||||
var val = this._el[this._textProp];
|
||||
this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
NothingRange.prototype._nativeScrollIntoView = function(){
|
||||
this._el.scrollIntoView();
|
||||
};
|
||||
NothingRange.prototype._nativeWrap = function() {throw new Error("Wrapping not implemented")};
|
||||
|
||||
})();
|
|
@ -0,0 +1,164 @@
|
|||
// insert characters in a textarea or text input field
|
||||
// special characters are enclosed in {}; use {{} for the { character itself
|
||||
// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/
|
||||
// Version: 2.2
|
||||
// Copyright (c) 2013 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function($){
|
||||
|
||||
$.fn.sendkeys = function (x, opts){
|
||||
return this.each( function(){
|
||||
var localkeys = $.extend({}, opts, $(this).data('sendkeys')); // allow for element-specific key functions
|
||||
// most elements to not keep track of their selection when they lose focus, so we have to do it for them
|
||||
var rng = $(this).selectionTracker();
|
||||
$(this).trigger({type: 'beforesendkeys', which: x});
|
||||
this.focus();
|
||||
$.data(this, 'sendkeys.originalText', rng.text());
|
||||
// turn line feeds into explicit break insertions, but not if escaped
|
||||
x.replace(/{[^}]*}|[^{]+/g, (s) => s.startsWith('{') ? s : s.replace(/\n/g, '{enter}')).
|
||||
replace(/{[^}]*}|[^{]+/g, function(s){
|
||||
(localkeys[s] || $.fn.sendkeys.defaults[s] || $.fn.sendkeys.defaults.simplechar)(rng, s);
|
||||
rng.select();
|
||||
});
|
||||
$(this).trigger({type: 'sendkeys', which: x});
|
||||
});
|
||||
}; // sendkeys
|
||||
|
||||
// add the functions publicly so they can be overridden
|
||||
$.fn.sendkeys.defaults = {
|
||||
simplechar: function (rng, s){
|
||||
// deal with unknown {key}s
|
||||
if (/^{[^}]*}$/.test(s)) s = s.slice(1,-1);
|
||||
for (var i =0; i < s.length; ++i){
|
||||
var x = s.charCodeAt(i);
|
||||
$(rng.element()).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
|
||||
}
|
||||
rng.text(s, 'end');
|
||||
},
|
||||
'{enter}': function (rng){
|
||||
$(rng._el).trigger({type: 'keypress', keyCode: 13, which: 13, charCode: 13, code: 'Enter', key: 'Enter'});
|
||||
rng.insertEOL();
|
||||
},
|
||||
'{backspace}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{del}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{rightarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
|
||||
rng.bounds([b[1], b[1]]);
|
||||
},
|
||||
'{leftarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
|
||||
rng.bounds([b[0], b[0]]);
|
||||
},
|
||||
'{selectall}' : function (rng){
|
||||
rng.bounds('all');
|
||||
},
|
||||
'{selection}': function (rng){
|
||||
// insert the characters without the sendkeys processing
|
||||
var s = $.data(rng.element(), 'sendkeys.originalText');
|
||||
for (var i =0; i < s.length; ++i){
|
||||
var x = s.charCodeAt(i);
|
||||
$(rng.element()).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
|
||||
}
|
||||
rng.selection(s);
|
||||
},
|
||||
'{mark}' : function (rng){
|
||||
var bounds = rng.bounds();
|
||||
$(rng.element()).one('sendkeys', function(){
|
||||
// set up the event listener to change the selection after the sendkeys is done
|
||||
rng.bounds(bounds).select();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Most ranges do not keep track of what was selected when they lose focus.
|
||||
// We have to do that for them
|
||||
$.fn.selectionTracker = function(bounds){
|
||||
var rng = this.data('selectionTracker');
|
||||
if (!rng){
|
||||
rng = bililiteRange(this[0]).bounds('selection');
|
||||
this.data('selectionTracker', rng);
|
||||
$(this).on('mouseup.selectionTracker', function(evt){
|
||||
// we have to update the saved range.
|
||||
rng.bounds('selection');
|
||||
}).on('keyup.selectionTracker', function(evt){
|
||||
// restore the selection if we got here with a tab (a click should select what was clicked on)
|
||||
if (evt.which == 9){
|
||||
// there's a flash of selection when we restore the focus, but I don't know how to avoid that.
|
||||
rng.select();
|
||||
}else{
|
||||
rng.bounds('selection');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (arguments.length > 0) rng.bounds(bounds); // change the saved selection without actually selecting
|
||||
if (document.activeElement == this[0]) rng.select(); // explicitly select it if already active
|
||||
return rng;
|
||||
}
|
||||
|
||||
// monkey patch bililiteRange to reflect the saved range
|
||||
var oldselect = bililiteRange.fn.select;
|
||||
bililiteRange.fn.select = function(){
|
||||
var $el = $(this.element());
|
||||
if (
|
||||
$el.data('selectionTracker') &&
|
||||
document.activeElement != $el[0]
|
||||
){
|
||||
$el.selectionTracker(this.bounds());
|
||||
}
|
||||
return oldselect.apply(this, arguments);
|
||||
};
|
||||
var oldbounds = bililiteRange.fn.bounds;
|
||||
bililiteRange.fn.bounds = function(bounds){
|
||||
var $el = $(this.element());
|
||||
if (
|
||||
$el.data('selectionTracker') && // if we are tracking the selection
|
||||
document.activeElement != $el[0] && // and the real selection isn't here
|
||||
bounds == 'selection' // and we want the selection anyway
|
||||
){
|
||||
bounds = $el.selectionTracker().bounds(); // use the saved selection
|
||||
}
|
||||
return oldbounds.call(this, bounds);
|
||||
}
|
||||
|
||||
// monkey patch focus to actually focus the element, on the saved range
|
||||
var focus = $.fn.focus;
|
||||
$.fn.focus = function(){
|
||||
if (this.length > 0){
|
||||
this[0].focus();
|
||||
this.selectionTracker();
|
||||
}
|
||||
return focus.apply(this, arguments);
|
||||
}
|
||||
|
||||
})(jQuery)
|
|
@ -1,467 +0,0 @@
|
|||
// Cross-broswer implementation of text ranges and selections
|
||||
// documentation: http://bililite.com/blog/2011/01/11/cross-browser-.and-selections/
|
||||
// Version: 1.1
|
||||
// Copyright (c) 2010 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function($){
|
||||
|
||||
bililiteRange = function(el, debug){
|
||||
var ret;
|
||||
if (debug){
|
||||
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
|
||||
}else if (document.selection && !document.addEventListener){
|
||||
// Internet Explorer 8 and lower
|
||||
ret = new IERange();
|
||||
}else if (window.getSelection && el.setSelectionRange){
|
||||
// Standards. Element is an input or textarea
|
||||
ret = new InputRange();
|
||||
}else if (window.getSelection){
|
||||
// Standards, with any other kind of element
|
||||
ret = new W3CRange()
|
||||
}else{
|
||||
// doesn't support selection
|
||||
ret = new NothingRange();
|
||||
}
|
||||
ret._el = el;
|
||||
ret._doc = el.ownerDocument;
|
||||
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow;
|
||||
ret._textProp = textProp(el);
|
||||
ret._bounds = [0, ret.length()];
|
||||
return ret;
|
||||
}
|
||||
|
||||
function textProp(el){
|
||||
// returns the property that contains the text of the element
|
||||
if (typeof el.value != 'undefined') return 'value';
|
||||
if (typeof el.text != 'undefined') return 'text';
|
||||
if (typeof el.textContent != 'undefined') return 'textContent';
|
||||
return 'innerText';
|
||||
}
|
||||
|
||||
// base class
|
||||
function Range(){}
|
||||
Range.prototype = {
|
||||
length: function() {
|
||||
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
|
||||
},
|
||||
bounds: function(s){
|
||||
if (s === 'all'){
|
||||
this._bounds = [0, this.length()];
|
||||
}else if (s === 'start'){
|
||||
this._bounds = [0, 0];
|
||||
}else if (s === 'end'){
|
||||
this._bounds = [this.length(), this.length()];
|
||||
}else if (s === 'selection'){
|
||||
this.bounds ('all'); // first select the whole thing for constraining
|
||||
this._bounds = this._nativeSelection();
|
||||
}else if (s){
|
||||
this._bounds = s; // don't error check now; the element may change at any moment, so constrain it when we need it.
|
||||
}else{
|
||||
var b = [
|
||||
Math.max(0, Math.min (this.length(), this._bounds[0])),
|
||||
Math.max(0, Math.min (this.length(), this._bounds[1]))
|
||||
];
|
||||
return b; // need to constrain it to fit
|
||||
}
|
||||
return this; // allow for chaining
|
||||
},
|
||||
select: function(){
|
||||
this._nativeSelect(this._nativeRange(this.bounds()));
|
||||
return this; // allow for chaining
|
||||
},
|
||||
text: function(text, select){
|
||||
if (arguments.length){
|
||||
this._nativeSetText(text, this._nativeRange(this.bounds()));
|
||||
if (select == 'start'){
|
||||
this.bounds ([this._bounds[0], this._bounds[0]]);
|
||||
this.select();
|
||||
}else if (select == 'end'){
|
||||
this.bounds ([this._bounds[0]+text.length, this._bounds[0]+text.length]);
|
||||
this.select();
|
||||
}else if (select == 'all'){
|
||||
this.bounds ([this._bounds[0], this._bounds[0]+text.length]);
|
||||
this.select();
|
||||
}
|
||||
return this; // allow for chaining
|
||||
}else{
|
||||
return this._nativeGetText(this._nativeRange(this.bounds()));
|
||||
}
|
||||
},
|
||||
insertEOL: function (){
|
||||
this._nativeEOL();
|
||||
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function IERange(){}
|
||||
IERange.prototype = new Range();
|
||||
IERange.prototype._nativeRange = function (bounds){
|
||||
var rng;
|
||||
if (this._el.tagName == 'INPUT'){
|
||||
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
|
||||
rng = this._el.createTextRange();
|
||||
}else{
|
||||
rng = this._doc.body.createTextRange ();
|
||||
rng.moveToElementText(this._el);
|
||||
}
|
||||
if (bounds){
|
||||
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
|
||||
if (bounds[0] > this.length()) bounds[0] = this.length();
|
||||
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf wierdness
|
||||
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
|
||||
rng.moveEnd ('character', -1);
|
||||
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length);
|
||||
}
|
||||
if (bounds[0] > 0) rng.moveStart('character', bounds[0]);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
IERange.prototype._nativeSelect = function (rng){
|
||||
rng.select();
|
||||
};
|
||||
IERange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
var len = this.length();
|
||||
if (this._doc.selection.type != 'Text') return [0,0]; // append to the end
|
||||
var sel = this._doc.selection.createRange();
|
||||
try{
|
||||
return [
|
||||
iestart(sel, rng),
|
||||
ieend (sel, rng)
|
||||
];
|
||||
}catch (e){
|
||||
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
|
||||
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len];
|
||||
}
|
||||
};
|
||||
IERange.prototype._nativeGetText = function (rng){
|
||||
return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness
|
||||
};
|
||||
IERange.prototype._nativeSetText = function (text, rng){
|
||||
rng.text = text;
|
||||
};
|
||||
IERange.prototype._nativeEOL = function(){
|
||||
if (typeof this._el.value != 'undefined'){
|
||||
this.text('\n'); // for input and textarea, insert it straight
|
||||
}else{
|
||||
this._nativeRange(this.bounds()).pasteHTML('<br/>');
|
||||
}
|
||||
};
|
||||
// IE internals
|
||||
function iestart(rng, constraint){
|
||||
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len;
|
||||
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1));
|
||||
return i;
|
||||
}
|
||||
function ieend (rng, constraint){
|
||||
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
|
||||
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0;
|
||||
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1));
|
||||
return i;
|
||||
}
|
||||
|
||||
// an input element in a standards document. "Native Range" is just the bounds array
|
||||
function InputRange(){}
|
||||
InputRange.prototype = new Range();
|
||||
InputRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0, this.length()];
|
||||
};
|
||||
InputRange.prototype._nativeSelect = function (rng){
|
||||
this._el.setSelectionRange(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSelection = function(){
|
||||
return [this._el.selectionStart, this._el.selectionEnd];
|
||||
};
|
||||
InputRange.prototype._nativeGetText = function(rng){
|
||||
return this._el.value.substring(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSetText = function(text, rng){
|
||||
var val = this._el.value;
|
||||
this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
|
||||
function W3CRange(){}
|
||||
W3CRange.prototype = new Range();
|
||||
W3CRange.prototype._nativeRange = function (bounds){
|
||||
var rng = this._doc.createRange();
|
||||
rng.selectNodeContents(this._el);
|
||||
if (bounds){
|
||||
w3cmoveBoundary (rng, bounds[0], true, this._el);
|
||||
rng.collapse (true);
|
||||
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
W3CRange.prototype._nativeSelect = function (rng){
|
||||
this._win.getSelection().removeAllRanges();
|
||||
this._win.getSelection().addRange (rng);
|
||||
};
|
||||
W3CRange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
|
||||
var sel = this._win.getSelection().getRangeAt(0);
|
||||
return [
|
||||
w3cstart(sel, rng),
|
||||
w3cend (sel, rng)
|
||||
];
|
||||
}
|
||||
W3CRange.prototype._nativeGetText = function (rng){
|
||||
return rng.toString();
|
||||
};
|
||||
W3CRange.prototype._nativeSetText = function (text, rng){
|
||||
rng.deleteContents();
|
||||
rng.insertNode (this._doc.createTextNode(text));
|
||||
this._el.normalize(); // merge the text with the surrounding text
|
||||
};
|
||||
W3CRange.prototype._nativeEOL = function(){
|
||||
var rng = this._nativeRange(this.bounds());
|
||||
rng.deleteContents();
|
||||
var br = this._doc.createElement('br');
|
||||
br.setAttribute ('_moz_dirty', ''); // for Firefox
|
||||
rng.insertNode (br);
|
||||
rng.insertNode (this._doc.createTextNode('\n'));
|
||||
rng.collapse (false);
|
||||
};
|
||||
// W3C internals
|
||||
function nextnode (node, root){
|
||||
// in-order traversal
|
||||
// we've already visited node, so get kids then siblings
|
||||
if (node.firstChild) return node.firstChild;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
if (node===root) return null;
|
||||
while (node.parentNode){
|
||||
// get uncles
|
||||
node = node.parentNode;
|
||||
if (node == root) return null;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function w3cmoveBoundary (rng, n, bStart, el){
|
||||
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
|
||||
// if the start is moved after the end, then an exception is raised
|
||||
if (n <= 0) return;
|
||||
var node = rng[bStart ? 'startContainer' : 'endContainer'];
|
||||
if (node.nodeType == 3){
|
||||
// we may be starting somewhere into the text
|
||||
n += rng[bStart ? 'startOffset' : 'endOffset'];
|
||||
}
|
||||
while (node){
|
||||
if (node.nodeType == 3){
|
||||
if (n <= node.nodeValue.length){
|
||||
rng[bStart ? 'setStart' : 'setEnd'](node, n);
|
||||
// special case: if we end next to a <br>, include that node.
|
||||
if (n == node.nodeValue.length){
|
||||
// skip past zero-length text nodes
|
||||
for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){
|
||||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
|
||||
}
|
||||
if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
|
||||
}
|
||||
return;
|
||||
}else{
|
||||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
|
||||
n -= node.nodeValue.length; // and eat these characters
|
||||
}
|
||||
}
|
||||
node = nextnode (node, el);
|
||||
}
|
||||
}
|
||||
var START_TO_START = 0; // from the w3c definitions
|
||||
var START_TO_END = 1;
|
||||
var END_TO_END = 2;
|
||||
var END_TO_START = 3;
|
||||
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
|
||||
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
|
||||
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
|
||||
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
|
||||
function w3cstart(rng, constraint){
|
||||
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
|
||||
return constraint.toString().length - rng.toString().length;
|
||||
}
|
||||
function w3cend (rng, constraint){
|
||||
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
|
||||
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
|
||||
return rng.toString().length;
|
||||
}
|
||||
|
||||
function NothingRange(){}
|
||||
NothingRange.prototype = new Range();
|
||||
NothingRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0,this.length()];
|
||||
};
|
||||
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
|
||||
};
|
||||
NothingRange.prototype._nativeSelection = function(){
|
||||
return [0,0];
|
||||
};
|
||||
NothingRange.prototype._nativeGetText = function (rng){
|
||||
return this._el[this._textProp].substring(rng[0], rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeSetText = function (text, rng){
|
||||
var val = this._el[this._textProp];
|
||||
this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
// insert characters in a textarea or text input field
|
||||
// special characters are enclosed in {}; use {{} for the { character itself
|
||||
// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/
|
||||
// Version: 2.0
|
||||
// Copyright (c) 2010 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function($){
|
||||
|
||||
$.fn.sendkeys = function (x, opts){
|
||||
return this.each( function(){
|
||||
var localkeys = $.extend({}, opts, $(this).data('sendkeys')); // allow for element-specific key functions
|
||||
// most elements to not keep track of their selection when they lose focus, so we have to do it for them
|
||||
var rng = $.data (this, 'sendkeys.selection');
|
||||
if (!rng){
|
||||
rng = bililiteRange(this).bounds('selection');
|
||||
$.data(this, 'sendkeys.selection', rng);
|
||||
$(this).bind('mouseup.sendkeys', function(){
|
||||
// we have to update the saved range. The routines here update the bounds with each press, but actual keypresses and mouseclicks do not
|
||||
$.data(this, 'sendkeys.selection').bounds('selection');
|
||||
}).bind('keyup.sendkeys', function(evt){
|
||||
// restore the selection if we got here with a tab (a click should select what was clicked on)
|
||||
if (evt.which == 9){
|
||||
// there's a flash of selection when we restore the focus, but I don't know how to avoid that
|
||||
$.data(this, 'sendkeys.selection').select();
|
||||
}else{
|
||||
$.data(this, 'sendkeys.selection').bounds('selection');
|
||||
}
|
||||
});
|
||||
}
|
||||
this.focus();
|
||||
if (typeof x === 'undefined') return; // no string, so we just set up the event handlers
|
||||
$.data(this, 'sendkeys.originalText', rng.text());
|
||||
x.replace(/\n/g, '{enter}'). // turn line feeds into explicit break insertions
|
||||
replace(/{[^}]*}|[^{]+/g, function(s){
|
||||
(localkeys[s] || $.fn.sendkeys.defaults[s] || $.fn.sendkeys.defaults.simplechar)(rng, s);
|
||||
});
|
||||
$(this).trigger({type: 'sendkeys', which: x});
|
||||
});
|
||||
}; // sendkeys
|
||||
|
||||
|
||||
// add the functions publicly so they can be overridden
|
||||
$.fn.sendkeys.defaults = {
|
||||
simplechar: function (rng, s){
|
||||
rng.text(s, 'end');
|
||||
for (var i =0; i < s.length; ++i){
|
||||
var x = s.charCodeAt(i);
|
||||
// a bit of cheating: rng._el is the element associated with rng.
|
||||
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
|
||||
}
|
||||
},
|
||||
'{{}': function (rng){
|
||||
$.fn.sendkeys.defaults.simplechar (rng, '{')
|
||||
},
|
||||
'{enter}': function (rng){
|
||||
rng.insertEOL();
|
||||
rng.select();
|
||||
$(rng._el).trigger(
|
||||
{type: 'keypress', keyCode: 13, which: 13, charCode: 13, code: 'Enter', key: 'Enter'});
|
||||
},
|
||||
'{backspace}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{del}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{rightarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
|
||||
rng.bounds([b[1], b[1]]).select();
|
||||
},
|
||||
'{leftarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
|
||||
rng.bounds([b[0], b[0]]).select();
|
||||
},
|
||||
'{selectall}' : function (rng){
|
||||
rng.bounds('all').select();
|
||||
},
|
||||
'{selection}': function (rng){
|
||||
$.fn.sendkeys.defaults.simplechar(rng, $.data(rng._el, 'sendkeys.originalText'));
|
||||
},
|
||||
'{mark}' : function (rng){
|
||||
var bounds = rng.bounds();
|
||||
$(rng._el).one('sendkeys', function(){
|
||||
// set up the event listener to change the selection after the sendkeys is done
|
||||
rng.bounds(bounds).select();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery)
|
|
@ -25,9 +25,7 @@ describe('Chat messages and UI', function () {
|
|||
const username = helper.chatTextParagraphs().children('b').text();
|
||||
const time = helper.chatTextParagraphs().children('.time').text();
|
||||
|
||||
// TODO: The '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of it
|
||||
// without breaking the other tests that use $.sendkeys().
|
||||
expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}\n`);
|
||||
expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}`);
|
||||
|
||||
await helper.hideChat();
|
||||
});
|
||||
|
@ -48,9 +46,7 @@ describe('Chat messages and UI', function () {
|
|||
const username = chat.children('b').text();
|
||||
const time = chat.children('.time').text();
|
||||
|
||||
// TODO: Each '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of them
|
||||
// without breaking the other tests that use $.sendkeys().
|
||||
expect(chat.text()).to.be(`${username}${time} \n${chatValue}\n`);
|
||||
expect(chat.text()).to.be(`${username}${time} \n${chatValue}`);
|
||||
});
|
||||
|
||||
it('makes chat stick to right side of the screen via settings, ' +
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('timeslider follow', function () {
|
|||
// send 6 revisions
|
||||
const revs = 6;
|
||||
const message = 'a\n\n\n\n\n\n\n\n\n\n';
|
||||
const newLines = message.split('\n').length;
|
||||
const newLines = message.split('\n').length - 1;
|
||||
for (let i = 0; i < revs; i++) {
|
||||
await helper.edit(message, newLines * i + 1);
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@ try() { "$@" || fatal "'$@' failed"; }
|
|||
MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1
|
||||
try cd "${MY_DIR}/../../../.."
|
||||
|
||||
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json.template
|
||||
|
||||
try sed -e '
|
||||
s!"loadTest":[^,]*!"loadTest": true!
|
||||
# Reduce rate limit aggressiveness
|
||||
|
|
Loading…
Reference in New Issue