Merge branch 'master' into rotation

# Conflicts:
#	package.json
This commit is contained in:
raphael 2020-05-22 17:35:22 +02:00
commit 4ff618be8d
43 changed files with 7692 additions and 1282 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{*.js,*.css,*.html}]
indent_size = 4

18
.github/workflows/build-docker.yml vendored Normal file
View File

@ -0,0 +1,18 @@
# This workflow will do clean build of the docker image
name: Docker Image CI (also tests build)
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: docker build . --file Dockerfile --tag rofl256/whiteboard:$(date +%s)

View File

@ -0,0 +1,22 @@
# This workflow will do a clean install of node dependencies and check the code style
name: Linting and testing code CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- run: npm ci
- run: npm run style
- run: npm run test

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
/config.run.yml
# Logs # Logs
logs logs
*.log *.log

3
.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 100
}

View File

@ -1,4 +1,4 @@
FROM node:11 as base FROM node:12 as base
# Create app directory # Create app directory
RUN mkdir -p /opt/app RUN mkdir -p /opt/app
@ -19,7 +19,7 @@ RUN npm run build
# Final image # Final image
##################### #####################
FROM node:11-alpine FROM node:12-alpine
ENV NODE_ENV=prod ENV NODE_ENV=prod
MAINTAINER cracker0dks MAINTAINER cracker0dks
@ -28,7 +28,7 @@ MAINTAINER cracker0dks
RUN mkdir -p /opt/app RUN mkdir -p /opt/app
WORKDIR /opt/app WORKDIR /opt/app
COPY ./package.json ./package-lock.json ./ COPY ./package.json ./package-lock.json config.default.yml ./
RUN npm ci --only=prod RUN npm ci --only=prod
COPY scripts ./scripts COPY scripts ./scripts

189
README.md
View File

@ -1,34 +1,51 @@
# whiteboard # whiteboard
This is a lightweight NodeJS collaborative Whiteboard/Sketchboard witch can easily be customized... This is a lightweight NodeJS collaborative Whiteboard/Sketchboard witch can easily be customized...
![start](./doc/start.png) ![start](./doc/start.png)
## Demowhiteboard ## ## Demowhiteboard
[HERE](https://cloud13.de/testwhiteboard/) (Reset every night) [HERE](https://cloud13.de/testwhiteboard/) (Reset every night)
## Updating
Information related to updating this app can be found [here](./doc/updating_guide.md).
## Some Features ## Some Features
* Shows remote user cursors while drawing
* Undo / Redo function for each user - Shows remote user cursors while drawing
* Drag+Drop / Copy+Paste Images or PDFs from PC and Browsers - Undo / Redo function for each user
* Resize, Move & Draw Images to Canvas or Background - Drag+Drop / Copy+Paste Images or PDFs from PC and Browsers
* Write text - Resize, Move & Draw Images to Canvas or Background
* Save Whiteboard to Image and JSON - Write text
* Draw angle lines by pressing "shift" while drawing (with line tool) - Save Whiteboard to Image and JSON
* Draw square by pressing "shift" while drawing (with rectangle tool) - Draw angle lines by pressing "shift" while drawing (with line tool)
* Indicator that shows the smallest screen participating - Draw square by pressing "shift" while drawing (with rectangle tool)
* Keybindings for ALL the functions - Indicator that shows the smallest screen participating
* Working on PC, Tablet & Mobile - Keybindings for ALL the functions
- Working on PC, Tablet & Mobile
## Projects using this Whiteboard
- [meetzi](https://meetzi.de/) - WebRtc Conference tool
- [Accelerator](https://github.com/cracker0dks/Accelerator) - WebRtc Conference tool
- Your Project here...
## Install the App ## Install the App
You can run this app with and without docker You can run this app with and without docker
### Without Docker ### Without Docker
1. install the latest NodeJs
1. install the latest NodeJs (version >= 12)
2. Clone the app 2. Clone the app
3. Run `npm ci` inside the folder 3. Run `npm ci` inside the folder
4. Run `npm run start:prod` 4. Run `npm run start:prod`
5. Surf to http://YOURIP:8080 5. Surf to http://YOURIP:8080
### With Docker ### With Docker
1. `docker run -d -p 8080:8080 rofl256/whiteboard` 1. `docker run -d -p 8080:8080 rofl256/whiteboard`
2. Surf to http://YOURIP:8080 2. Surf to http://YOURIP:8080
@ -37,67 +54,84 @@ You can run this app with and without docker
After you have installed the app, run `npm run start:dev` to start the backend and a frontend development server. The website will be accessible on http://locahost:8080. After you have installed the app, run `npm run start:dev` to start the backend and a frontend development server. The website will be accessible on http://locahost:8080.
## Default keyboard shortcuts ## Default keyboard shortcuts
Use keyboard shortcuts to become more productive while using Whiteboard. Use keyboard shortcuts to become more productive while using Whiteboard.
They are especially useful if you work with interactive displays such as XP-Pen Artist, Huion Kamvas and Wacom Cintiq. These devices have quick buttons (6-8 buttons and scrolling). By default, the buttons on these displays are mapped to standard Photoshop keyboard shortcuts. Keys can be configured to function effectively in other software. They are especially useful if you work with interactive displays such as XP-Pen Artist, Huion Kamvas and Wacom Cintiq. These devices have quick buttons (6-8 buttons and scrolling). By default, the buttons on these displays are mapped to standard Photoshop keyboard shortcuts. Keys can be configured to function effectively in other software.
The following are predefined shortcuts that you can override in the file [./src/js/keybinds.js](./src/js/keybinds.js) The following are predefined shortcuts that you can override in the file [./src/js/keybinds.js](./src/js/keybinds.js)
Result | Windows and Linux | macOS | Result | Windows and Linux | macOS |
------ | -------------------- | ------- | ---------------------------------------------------------------- | -------------------- | ----------------------- |
Clear the whiteboard | Ctrl + Shift + Z | Command + Shift + Z | Clear the whiteboard | Ctrl + Shift + Z | Command + Shift + Z |
Undo your last step | Ctrl + Z | Command + Z | Undo your last step | Ctrl + Z | Command + Z |
Redo your last undo | Ctrl + Y | Command + Y | Redo your last undo | Ctrl + Y | Command + Y |
Select an area | Ctrl + X | Command + X | Select an area | Ctrl + X | Command + X |
Take the mouse | Ctrl + M | Command + M | Take the mouse | Ctrl + M | Command + M |
Take the pen | Ctrl + P | Command + P | Take the pen | Ctrl + P | Command + P |
Draw a line | Ctrl + L | Command + L | Draw a line | Ctrl + L | Command + L |
Draw a rectangle | Ctrl + R | Command + R | Draw a rectangle | Ctrl + R | Command + R |
Draw a circle | Ctrl + C | Command + C | Draw a circle | Ctrl + C | Command + C |
Toggle between line, rectangle and circle | Ctrl + Shift + F | Command + Shift + F | Toggle between line, rectangle and circle | Ctrl + Shift + F | Command + Shift + F |
Toggle between pen and eraser | Ctrl + Shift + X | Command + Shift + X | Toggle between pen and eraser | Ctrl + Shift + X | Command + Shift + X |
Toggle between main clolors (black, blue, green, yellow and red) | Ctrl + Shift + R | Command + Shift + R | Toggle between main clolors (black, blue, green, yellow and red) | Ctrl + Shift + R | Command + Shift + R |
Write text | Ctrl + A | Command + A | Write text | Ctrl + A | Command + A |
Take the eraser | Ctrl + E | Command + E | Take the eraser | Ctrl + E | Command + E |
Increase thickness | Ctrl + Up Arrow | Command + Up Arrow | Increase thickness | Ctrl + Up Arrow | Command + Up Arrow |
Decrease thickness | Ctrl + Down Arrow | Command + Down Arrow | Decrease thickness | Ctrl + Down Arrow | Command + Down Arrow |
Colorpicker | Ctrl + Shift + C | Command + Shift + C | Colorpicker | Ctrl + Shift + C | Command + Shift + C |
Set black color | Ctrl + Shift + 1 | Command + Shift + 1 | Set black color | Ctrl + Shift + 1 | Command + Shift + 1 |
Set blue color | Ctrl + Shift + 2 | Command + Shift + 2 | Set blue color | Ctrl + Shift + 2 | Command + Shift + 2 |
Set green color | Ctrl + Shift + 3 | Command + Shift + 3 | Set green color | Ctrl + Shift + 3 | Command + Shift + 3 |
Set yellow color | Ctrl + Shift + 4 | Command + Shift + 4 | Set yellow color | Ctrl + Shift + 4 | Command + Shift + 4 |
Set red color | Ctrl + Shift + 5 | Command + Shift + 5 | Set red color | Ctrl + Shift + 5 | Command + Shift + 5 |
Save whiteboard as image | Ctrl + S | Command + S | Save whiteboard as image | Ctrl + S | Command + S |
Save whiteboard as JSON | Ctrl + Shift + K | Command + Shift + K | Save whiteboard as JSON | Ctrl + Shift + K | Command + Shift + K |
Save whiteboard to WebDav | Ctrl + Shift + I (i) | Command + Shift + I (i) | Save whiteboard to WebDav | Ctrl + Shift + I (i) | Command + Shift + I (i) |
Load saved JSON to whiteboard | Ctrl + Shift + J | Command + Shift + J | Load saved JSON to whiteboard | Ctrl + Shift + J | Command + Shift + J |
Share whiteboard | Ctrl + Shift + S | Command + Shift + S | Share whiteboard | Ctrl + Shift + S | Command + Shift + S |
Hide or show toolbar | Tab | Tab | Hide or show toolbar | Tab | Tab |
Move selected object up | Up Arrow | Up Arrow | Move selected object up | Up Arrow | Up Arrow |
Move selected object down | Down Arrow | Down Arrow | Move selected object down | Down Arrow | Down Arrow |
Move selected object left | Left Arrow | Left Arrow | Move selected object left | Left Arrow | Left Arrow |
Move selected object right | Right Arrow | Right Arrow | Move selected object right | Right Arrow | Right Arrow |
Drop object | Ctrl + Enter | Command + Enter | Drop object | Ctrl + Enter | Command + Enter |
Add Image to backgroud | Shift + Enter | Shift + Enter | Add Image to backgroud | Shift + Enter | Shift + Enter |
Cancel all actions | Escape | Escape | Cancel all actions | Escape | Escape |
Delete selected object | Delete | Delete | Delete selected object | Delete | Delete |
| Use Line tool when pen is active (Not changeable) | Shift (Hold) | Shift (Hold) |
## URL Parameters ## URL Parameters
Call your site with GET parameters to change the WhiteboardID or the Username Call your site with GET parameters to change the WhiteboardID or the Username
`http://YOURIP:8080?whiteboardid=MYID&username=MYNAME` `http://YOURIP:8080?whiteboardid=MYID&username=MYNAME`
* whiteboardid => All people with the same ID are drawing on the same board - whiteboardid => All people with the same ID are drawing on the same board
* username => The name witch is showing to others while drawing - username => The name witch is showing to others while drawing
* title => Change the name of the Browser Tab - title => Change the name of the Browser Tab
* randomid => if set to true, a random whiteboardId will be generated if not given aswell - randomid => if set to true, a random whiteboardId will be generated if not given aswell
## Security - AccessToken (Optional) ## Configuration
To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start.
<b>Server (Without docker):</b> `node scripts/server.js --accesstoken="mySecToken"` Many settings of this project can be set using a simple `yaml` file, to change some behaviors or tweak performances.
<b>Server (With docker):</b> `docker run -d -p 8080:8080 rofl256/whiteboard --accesstoken="mySecToken"` ### Config. file
To run the project with custom settings:
1. Create a `config.run.yml` file based on the content of [`config.default.yml`](./config.default.yml),
2. Change the settings,
3. Run the project with your custom configuration (it will be merged into the default one):
- locally: `node scripts/server.js --config=./config.run.yml`
- docker: `docker run -d -p 8080:8080 -v $(pwd)/config.run.yml:/config.run.yml:ro rofl256/whiteboard --config=/config.run.yml`
### Highlights
#### Security - AccessToken (Optional)
To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start (see [here](./config.default.yml)).
Then set the same token on the client side as well: Then set the same token on the client side as well:
@ -105,14 +139,11 @@ Then set the same token on the client side as well:
Done! Done!
## WebDAV (Optional) #### WebDAV (Optional)
This function allows your users to save the whiteboard directly to a webdav server (Nextcloud) as image without downloading it. This function allows your users to save the whiteboard directly to a webdav server (Nextcloud) as image without downloading it.
To enable it: To enable set `enableWebdav` to `true` in the [configuration](./config.default.yml).
<b>Server (Without docker):</b> `node scripts/server.js --webdav=true`
<b>Server (With docker):</b> `docker run -d -p 8080:8080 rofl256/whiteboard --webdav=true`
Then set the same parameter on the client side as well: Then set the same parameter on the client side as well:
@ -124,21 +155,23 @@ Note: For the most owncloud/nextcloud setups you have to set the WebDav-Server U
Done! Done!
### And many more (performance, etc.)
Many more settings can be tweaked. All of them are described in the [default config file](./config.default.yml).
## Things you may want to know ## Things you may want to know
* Whiteboards are gone if you restart the Server, so keep that in mind (or save your whiteboard)
* You should be able to customize the layout without ever touching the whiteboard.js (take a look at index.html & main.js)
## All server start parameters (also docker) - Whiteboards are gone if you restart the Server, so keep that in mind (or save your whiteboard)
* accesstoken => take a look at "Security - AccessToken" for a full explanation - You should be able to customize the layout without ever touching the whiteboard.js (take a look at index.html & main.js)
* disablesmallestscreen => set this to "true" if you don't want show the "smallest screen" indicator (A dotted gray line) to the users
* webdav => Enable the function to save to a webdav-server (Must also be enabled on the client; Take a look at the webdav section)
## ToDo ## ToDo
* Make undo function more reliable on texts
- Make undo function more reliable on texts
## Nginx Reverse Proxy configuration ## Nginx Reverse Proxy configuration
Add this to your server part: Add this to your server part:
``` ```
location /whiteboard/ { location /whiteboard/ {
proxy_set_header HOST $host; proxy_set_header HOST $host;
@ -148,22 +181,22 @@ Add this to your server part:
proxy_pass http://YOURIP:8080/; proxy_pass http://YOURIP:8080/;
} }
``` ```
To run it at /whiteboard. Don't forget to change -> YOURIP! To run it at /whiteboard. Don't forget to change -> YOURIP!
## Nextcloud integration ## Nextcloud integration
1. Install this app on your server 1. Install this app on your server
2. Enable and go to "external sites" (app) on your Nextcloud 2. Enable and go to "external sites" (app) on your Nextcloud
2. Add a link to your server: `https://YOURIP/whiteboard/?whiteboardid=WHITEBOARDNAME&username={uid}` 3. Add a link to your server: `https://YOURIP/whiteboard/?whiteboardid=WHITEBOARDNAME&username={uid}`
You can give each group its own whiteboard by changeing the WHITEBOARDNAME in the URL if you want. You can give each group its own whiteboard by changeing the WHITEBOARDNAME in the URL if you want.
Note: You might have to serve the app with https (If your nextcloud server runs https). To do so, its recommend to run this app behind a reverse proxy. (as shown above) Note: You might have to serve the app with https (If your nextcloud server runs https). To do so, its recommend to run this app behind a reverse proxy. (as shown above)
#### (Optional) Set whiteboard icon in nextcloud #### (Optional) Set whiteboard icon in nextcloud
![start](https://raw.githubusercontent.com/cracker0dks/whiteboard/master/doc/iconPrev.jpg) ![start](https://raw.githubusercontent.com/cracker0dks/whiteboard/master/doc/iconPrev.jpg)
Upload both icons present at /doc/nextcloud_icons/ to your nextcloud at the "external sites" admin section. Then set it as symbol on your link. Upload both icons present at /doc/nextcloud_icons/ to your nextcloud at the "external sites" admin section. Then set it as symbol on your link.
**_ MIT License _**
___ MIT License ___

BIN
assets/images/bg_dots.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

54
config.default.yml Normal file
View File

@ -0,0 +1,54 @@
# Backend configuration
backend:
# Access token required for interacting with the server -- string (empty string for no restrictions)
accessToken: ""
# Enable the function to save to a webdav-server (check README for more info) -- boolean
enableWebdav: false
# Backend performance tweaks
performance:
# Whiteboard information broadcasting frequency (in Hz i.e. /s) -- number
# => diminishing this will result in more latency
whiteboardInfoBroadcastFreq: 1
# Frontend configuration
frontend:
# When a whiteboard is loaded on a client
onWhiteboardLoad:
# should an (editable) whiteboard be started in read-only mode by default -- boolean
setReadOnly: false
# should the whiteboard info be displayed by default -- boolean
displayInfo: false
# Show the smallest screen indicator ? (with dotted lines) -- boolean
showSmallestScreenIndicator: true
# Image download format, can be "png", "jpeg" (or "webp" -> only working on chrome) -- string
imageDownloadFormat: "png"
# draw the background grid to images on download ? (If True, even PNGs are also not transparent anymore) -- boolean
drawBackgroundGrid: false
# Background Image; Can be "bg_grid.png" or "bg_dots.png" -- string
backgroundGridImage: "bg_grid.png"
# Frontend performance tweaks
performance:
# Refresh frequency of the debug / info div (in Hz i.e. /s) -- number
refreshInfoFreq: 5
# Throttling of pointer events (except drawing related) -- array of object (one must have fromUserCount == 0)
# Throttling of events can be defined for different user count levels
# Throttling consist of skipping certain events (i.e. not broadcasting them to others)
pointerEventsThrottling:
- # User count from which the specific throttling is applied -- number
fromUserCount: 0
# Min screen distance (in pixels) below which throttling is applied
minDistDelta: 1
# Maximum frequency above which throttling is applied
maxFreq: 30
- fromUserCount: 10
minDistDelta: 5
maxFreq: 10

View File

@ -1,61 +1,59 @@
const webpack = require("webpack"); const webpack = require("webpack");
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyPlugin = require('copy-webpack-plugin'); const CopyPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin') const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path"); const path = require("path");
const config = { const config = {
entry: { entry: {
main: ["./src/js/index.js"], main: ["./src/js/index.js"],
}, },
output: { output: {
path: path.join(__dirname, "..", "dist"), path: path.join(__dirname, "..", "dist"),
filename: "[name]-[hash].js" filename: "[name]-[hash].js",
}, },
resolve: { resolve: {
extensions: ["*", ".json", ".js"] extensions: ["*", ".json", ".js"],
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.(js)$/, test: /\.(js)$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: "babel-loader", loader: "babel-loader",
options: { options: {
compact: true compact: true,
} },
}, },
{ {
test: /\.css$/, test: /\.css$/,
use: ['style-loader', 'css-loader'] use: ["style-loader", "css-loader"],
}, },
{ {
test: /\.(png|jpe?g|gif)$/i, test: /\.(png|jpe?g|gif)$/i,
use: [ use: [
{ {
loader: 'file-loader', loader: "file-loader",
}, },
],
},
], ],
} },
] plugins: [
}, new CleanWebpackPlugin(),
plugins: [ new webpack.ProvidePlugin({
new CleanWebpackPlugin(), $: "jquery",
new webpack.ProvidePlugin({ jQuery: "jquery",
$: 'jquery', "window.jQuery": "jquery",
jQuery: 'jquery', "window.$": "jquery",
"window.jQuery": "jquery", }),
"window.$": "jquery", new CopyPlugin([{ from: "assets", to: "" }]),
}), new HtmlWebpackPlugin({
new CopyPlugin([ template: "src/index.html",
{ from: 'assets', to: '' }, minify: false,
]), inject: true,
new HtmlWebpackPlugin({ }),
template: 'src/index.html', ],
minify: false,
inject: true
})
]
}; };
module.exports = config; module.exports = config;

View File

@ -2,10 +2,10 @@ const merge = require("webpack-merge");
const baseConfig = require("./webpack.base"); const baseConfig = require("./webpack.base");
module.exports = merge(baseConfig, { module.exports = merge(baseConfig, {
mode: "production", mode: "production",
optimization: { optimization: {
minimize: true, minimize: true,
nodeEnv: "production", nodeEnv: "production",
}, },
devtool: false devtool: false,
}); });

View File

@ -3,16 +3,16 @@ const baseConfig = require("./webpack.base");
const webpack = require("webpack"); const webpack = require("webpack");
const devConfig = merge(baseConfig, { const devConfig = merge(baseConfig, {
mode: "development", mode: "development",
devtool: "eval-source-map", devtool: "eval-source-map",
optimization: { optimization: {
minimize: false, minimize: false,
}, },
plugins: [ plugins: [
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), new webpack.NamedModulesPlugin(),
new webpack.NoEmitOnErrorsPlugin(), new webpack.NoEmitOnErrorsPlugin(),
].concat(baseConfig.plugins), ].concat(baseConfig.plugins),
}); });
module.exports = devConfig; module.exports = devConfig;

13
doc/updating_guide.md Normal file
View File

@ -0,0 +1,13 @@
# Updating guide
## From v1.x to 2.x (or latest)
Configuration handling has been updated: the ability to change settings from the CLI or the environment has been removed.
**Configuration is now handled with a yml config file**, which can be overridden with the `--config` CLI argument.
Here is the mapping from old cli argument / env variables to the new config file object:
- accesstoken => `backend.accessToken`
- webdav => `backend.enableWebdav`
- disablesmallestscreen => `frontend.showSmallestScreenIndicator`

View File

@ -1,9 +1,8 @@
version: '3.1' version: "3.1"
services: services:
whiteboard: whiteboard:
image: rofl256/whiteboard image: rofl256/whiteboard
restart: always restart: always
ports: ports:
- "8080:8080/tcp" - "8080:8080/tcp"
environment: command: --config=./config.default.yml
- ACCESSTOKEN=mysecrettoken

4577
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,10 @@
"build": "webpack --config config/webpack.build.js", "build": "webpack --config config/webpack.build.js",
"start:dev": "node scripts/server.js --mode=development", "start:dev": "node scripts/server.js --mode=development",
"start:prod": "npm run build && node scripts/server.js --mode=production", "start:prod": "npm run build && node scripts/server.js --mode=production",
"test": "echo \"No tests needed!\" && exit 1" "test": "jest",
"pretty-quick": "pretty-quick",
"format": "prettier --write .",
"style": "prettier --check ."
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -19,12 +22,20 @@
"Sketchboard", "Sketchboard",
"lightweight" "lightweight"
], ],
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
},
"dependencies": { "dependencies": {
"ajv": "6.12.2",
"dompurify": "^2.0.7", "dompurify": "^2.0.7",
"express": "4.*", "express": "4.*",
"formidable": "1.*", "formidable": "1.*",
"fs-extra": "7.*", "fs-extra": "7.*",
"jquery-ui-rotatable": "^1.1.0", "jquery-ui-rotatable": "^1.1.0",
"html2canvas": "^1.0.0-rc.5",
"js-yaml": "3.13.1",
"jsdom": "^14.0.0", "jsdom": "^14.0.0",
"pdfjs-dist": "^2.3.200", "pdfjs-dist": "^2.3.200",
"socket.io": "2.*", "socket.io": "2.*",
@ -46,9 +57,13 @@
"copy-webpack-plugin": "^5.1.1", "copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.5.2", "css-loader": "^3.5.2",
"html-webpack-plugin": "^4.2.0", "html-webpack-plugin": "^4.2.0",
"husky": "^4.2.5",
"jest": "26.0.1",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"jquery-ui": "^1.12.1", "jquery-ui": "^1.12.1",
"keymage": "^1.1.3", "keymage": "^1.1.3",
"prettier": "^2.0.5",
"pretty-quick": "^2.0.1",
"style-loader": "^1.1.4", "style-loader": "^1.1.4",
"vanilla-picker": "^2.10.1", "vanilla-picker": "^2.10.1",
"webpack": "^4.42.1", "webpack": "^4.42.1",

View File

@ -0,0 +1,99 @@
const config = require("./config/config");
/**
* Class to hold information related to a whiteboard
*/
class WhiteboardServerSideInfo {
static defaultScreenResolution = { w: 1000, h: 1000 };
constructor() {
/**
* @type {number}
* @private
*/
this._nbConnectedUsers = 0;
/**
* @type {Map<int, {w: number, h: number}>}
* @private
*/
this._screenResolutionByClients = new Map();
/**
* Variable to tell if these info have been sent or not
*
* @private
* @type {boolean}
*/
this._hasNonSentUpdates = false;
}
incrementNbConnectedUsers() {
this._nbConnectedUsers++;
this._hasNonSentUpdates = true;
}
decrementNbConnectedUsers() {
this._nbConnectedUsers--;
this._hasNonSentUpdates = true;
}
hasConnectedUser() {
return this._nbConnectedUsers > 0;
}
/**
* Store information about the client's screen resolution
*
* @param {number} clientId
* @param {number} w client's width
* @param {number} h client's hight
*/
setScreenResolutionForClient(clientId, { w, h }) {
this._screenResolutionByClients.set(clientId, { w, h });
this._hasNonSentUpdates = true;
}
/**
* Delete the stored information about the client's screen resoltion
* @param clientId
*/
deleteScreenResolutionOfClient(clientId) {
this._screenResolutionByClients.delete(clientId);
this._hasNonSentUpdates = true;
}
/**
* Get the smallest client's screen size on a whiteboard
* @return {{w: number, h: number}}
*/
getSmallestScreenResolution() {
const { _screenResolutionByClients: resolutions } = this;
return {
w: Math.min(...Array.from(resolutions.values()).map((res) => res.w)),
h: Math.min(...Array.from(resolutions.values()).map((res) => res.h)),
};
}
infoWasSent() {
this._hasNonSentUpdates = false;
}
shouldSendInfo() {
return this._hasNonSentUpdates;
}
asObject() {
const out = {
nbConnectedUsers: this._nbConnectedUsers,
};
if (config.frontend.showSmallestScreenIndicator) {
out.smallestScreenResolution = this.getSmallestScreenResolution();
}
return out;
}
}
module.exports = WhiteboardServerSideInfo;

View File

@ -0,0 +1,99 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Whiteboard config",
"type": "object",
"properties": {
"backend": {
"type": "object",
"required": ["accessToken", "performance", "enableWebdav"],
"additionalProperties": false,
"properties": {
"accessToken": {
"type": "string"
},
"enableWebdav": {
"type": "boolean"
},
"performance": {
"additionalProperties": false,
"type": "object",
"required": ["whiteboardInfoBroadcastFreq"],
"properties": {
"whiteboardInfoBroadcastFreq": {
"type": "number",
"minimum": 0
}
}
}
}
},
"frontend": {
"type": "object",
"additionalProperties": false,
"required": ["onWhiteboardLoad", "showSmallestScreenIndicator", "performance"],
"properties": {
"onWhiteboardLoad": {
"type": "object",
"additionalProperties": false,
"required": ["displayInfo", "setReadOnly"],
"properties": {
"setReadOnly": {
"type": "boolean"
},
"displayInfo": {
"type": "boolean"
}
}
},
"showSmallestScreenIndicator": {
"type": "boolean"
},
"imageDownloadFormat": {
"type": "string"
},
"drawBackgroundGrid": {
"type": "boolean"
},
"backgroundGridImage": {
"type": "string"
},
"performance": {
"type": "object",
"additionalProperties": false,
"required": ["pointerEventsThrottling", "refreshInfoFreq"],
"properties": {
"pointerEventsThrottling": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["fromUserCount", "minDistDelta", "maxFreq"],
"properties": {
"fromUserCount": {
"type": "number",
"minimum": 0
},
"minDistDelta": {
"type": "number",
"minimum": 0
},
"maxFreq": {
"type": "number",
"minimum": 0
}
}
}
},
"refreshInfoFreq": {
"type": "number",
"minimum": 0
}
}
}
}
}
},
"required": ["backend", "frontend"],
"additionalProperties": false
}

80
scripts/config/config.js Normal file
View File

@ -0,0 +1,80 @@
const util = require("util");
const { getDefaultConfig, getConfig, deepMergeConfigs, isConfigValid } = require("./utils");
const { getArgs } = require("./../utils");
const defaultConfig = getDefaultConfig();
const cliArgs = getArgs();
let userConfig = {};
if (cliArgs["config"]) {
userConfig = getConfig(cliArgs["config"]);
}
const config = deepMergeConfigs(defaultConfig, userConfig);
/**
* Update the config based on the CLI args
* @param {object} startArgs
*/
function updateConfigFromStartArgs(startArgs) {
function deprecateCliArg(key, callback) {
const val = startArgs[key];
if (val) {
console.warn(
"\x1b[33m\x1b[1m",
`Setting config values (${key}) from the CLI is deprecated. ` +
"This ability will be removed in the next major version. " +
"You should use the config file. "
);
callback(val);
}
}
deprecateCliArg("accesstoken", (val) => (config.backend.accessToken = val));
deprecateCliArg(
"disablesmallestscreen",
() => (config.backend.showSmallestScreenIndicator = false)
);
deprecateCliArg("webdav", () => (config.backend.enableWebdav = true));
}
/**
* Update the config based on the env variables
*/
function updateConfigFromEnv() {
function deprecateEnv(key, callback) {
const val = process.env[key];
if (val) {
console.warn(
"\x1b[33m\x1b[1m",
`Setting config values (${key}) from the environment is deprecated. ` +
"This ability will be removed in the next major version. " +
"You should use the config file. "
);
callback(val);
}
}
deprecateEnv("accesstoken", (val) => (config.backend.accessToken = val));
deprecateEnv(
"disablesmallestscreen",
() => (config.backend.showSmallestScreenIndicator = false)
);
deprecateEnv("webdav", () => (config.backend.enableWebdav = true));
}
// compatibility layer
// FIXME: remove this in next major
updateConfigFromEnv();
// FIXME: remove this in next major
updateConfigFromStartArgs(cliArgs);
if (!isConfigValid(config, true)) {
throw new Error("Config is not valid. Check logs for details");
}
console.info(util.inspect(config, { showHidden: false, depth: null, colors: true }));
module.exports = config;

92
scripts/config/utils.js Normal file
View File

@ -0,0 +1,92 @@
const path = require("path");
const fs = require("fs");
const yaml = require("js-yaml");
const Ajv = require("ajv");
const ajv = new Ajv({ allErrors: true });
const configSchema = require("./config-schema.json");
/**
* Load a yaml config file from a given path.
*
* @param path
* @return {Object}
*/
function getConfig(path) {
return yaml.safeLoad(fs.readFileSync(path, "utf8"));
}
/**
* Check that a config object is valid.
*
* @param {Object} config Config object
* @param {boolean} warn Should we warn in console for errors
* @return {boolean}
*/
function isConfigValid(config, warn = true) {
const validate = ajv.compile(configSchema);
const isValidAgainstSchema = validate(config);
if (!isValidAgainstSchema && warn) console.warn(validate.errors);
let structureIsValid = false;
try {
structureIsValid = config.frontend.performance.pointerEventsThrottling.some(
(item) => item.fromUserCount === 0
);
} catch (e) {
if (!e instanceof TypeError) {
throw e;
}
}
if (!structureIsValid && warn)
console.warn(
"At least one item under frontend.performance.pointerEventsThrottling" +
"must have fromUserCount set to 0"
);
return isValidAgainstSchema && structureIsValid;
}
/**
* Load the default project config
* @return {Object}
*/
function getDefaultConfig() {
const defaultConfigPath = path.join(__dirname, "..", "..", "config.default.yml");
return getConfig(defaultConfigPath);
}
/**
* Deep merge of project config
*
* Objects are merged, not arrays
*
* @param baseConfig
* @param overrideConfig
* @return {Object}
*/
function deepMergeConfigs(baseConfig, overrideConfig) {
const out = {};
Object.entries(baseConfig).forEach(([key, val]) => {
out[key] = val;
if (overrideConfig.hasOwnProperty(key)) {
const overrideVal = overrideConfig[key];
if (typeof val === "object" && !Array.isArray(val) && val !== null) {
out[key] = deepMergeConfigs(val, overrideVal);
} else {
out[key] = overrideVal;
}
}
});
return out;
}
module.exports.getConfig = getConfig;
module.exports.getDefaultConfig = getDefaultConfig;
module.exports.deepMergeConfigs = deepMergeConfigs;
module.exports.isConfigValid = isConfigValid;

View File

@ -0,0 +1,48 @@
const { getDefaultConfig, deepMergeConfigs, isConfigValid } = require("./utils");
test("Load default config", () => {
const defaultConfig = getDefaultConfig();
expect(typeof defaultConfig).toBe("object");
});
test("Full config override", () => {
const defaultConfig = getDefaultConfig();
expect(deepMergeConfigs(defaultConfig, defaultConfig)).toEqual(defaultConfig);
});
test("Simple partial config override", () => {
expect(deepMergeConfigs({ test: true }, { test: false }).test).toBe(false);
expect(deepMergeConfigs({ test: false }, { test: true }).test).toBe(true);
});
test("Simple deep config override", () => {
expect(deepMergeConfigs({ stage1: { stage2: true } }, { stage1: { stage2: false } })).toEqual({
stage1: { stage2: false },
});
});
test("Complex object config override", () => {
expect(
deepMergeConfigs({ stage1: { stage2: true, stage2b: true } }, { stage1: { stage2: false } })
).toEqual({
stage1: { stage2: false, stage2b: true },
});
});
test("Override default config", () => {
const defaultConfig = getDefaultConfig();
const overrideConfig1 = { frontend: { onWhiteboardLoad: { setReadOnly: true } } };
expect(
deepMergeConfigs(defaultConfig, overrideConfig1).frontend.onWhiteboardLoad.setReadOnly
).toBe(true);
});
test("Dumb config is not valid", () => {
expect(isConfigValid({}, false)).toBe(false);
});
test("Default config is valid", () => {
const defaultConfig = getDefaultConfig();
expect(isConfigValid(defaultConfig)).toBe(true);
});

View File

@ -7,10 +7,12 @@ module.exports = {
var tool = content["t"]; //Tool witch is used var tool = content["t"]; //Tool witch is used
var wid = content["wid"]; //whiteboard ID var wid = content["wid"]; //whiteboard ID
var username = content["username"]; var username = content["username"];
if (tool === "clear") { //Clear the whiteboard if (tool === "clear") {
//Clear the whiteboard
delete savedBoards[wid]; delete savedBoards[wid];
delete savedUndos[wid]; delete savedUndos[wid];
} else if (tool === "undo") { //Undo an action } else if (tool === "undo") {
//Undo an action
if (!savedUndos[wid]) { if (!savedUndos[wid]) {
savedUndos[wid] = []; savedUndos[wid] = [];
} }
@ -19,7 +21,10 @@ module.exports = {
if (savedBoards[wid][i]["username"] == username) { if (savedBoards[wid][i]["username"] == username) {
var drawId = savedBoards[wid][i]["drawId"]; var drawId = savedBoards[wid][i]["drawId"];
for (var i = savedBoards[wid].length - 1; i >= 0; i--) { for (var i = savedBoards[wid].length - 1; i >= 0; i--) {
if (savedBoards[wid][i]["drawId"] == drawId && savedBoards[wid][i]["username"] == username) { if (
savedBoards[wid][i]["drawId"] == drawId &&
savedBoards[wid][i]["username"] == username
) {
savedUndos[wid].push(savedBoards[wid][i]); savedUndos[wid].push(savedBoards[wid][i]);
savedBoards[wid].splice(i, 1); savedBoards[wid].splice(i, 1);
} }
@ -27,7 +32,7 @@ module.exports = {
break; break;
} }
} }
if(savedUndos[wid].length > 1000) { if (savedUndos[wid].length > 1000) {
savedUndos[wid].splice(0, savedUndos[wid].length - 1000); savedUndos[wid].splice(0, savedUndos[wid].length - 1000);
} }
} }
@ -42,7 +47,10 @@ module.exports = {
if (savedUndos[wid][i]["username"] == username) { if (savedUndos[wid][i]["username"] == username) {
var drawId = savedUndos[wid][i]["drawId"]; var drawId = savedUndos[wid][i]["drawId"];
for (var i = savedUndos[wid].length - 1; i >= 0; i--) { for (var i = savedUndos[wid].length - 1; i >= 0; i--) {
if (savedUndos[wid][i]["drawId"] == drawId && savedUndos[wid][i]["username"] == username) { if (
savedUndos[wid][i]["drawId"] == drawId &&
savedUndos[wid][i]["username"] == username
) {
savedBoards[wid].push(savedUndos[wid][i]); savedBoards[wid].push(savedUndos[wid][i]);
savedUndos[wid].splice(i, 1); savedUndos[wid].splice(i, 1);
} }
@ -50,14 +58,36 @@ module.exports = {
break; break;
} }
} }
} else if (["line", "pen", "rect", "circle", "eraser", "addImgBG", "recSelect", "eraseRec", "addTextBox", "setTextboxText", "removeTextbox", "setTextboxPosition", "setTextboxFontSize", "setTextboxFontColor"].includes(tool)) { //Save all this actions } else if (
[
"line",
"pen",
"rect",
"circle",
"eraser",
"addImgBG",
"recSelect",
"eraseRec",
"addTextBox",
"setTextboxText",
"removeTextbox",
"setTextboxPosition",
"setTextboxFontSize",
"setTextboxFontColor",
].includes(tool)
) {
//Save all this actions
if (!savedBoards[wid]) { if (!savedBoards[wid]) {
savedBoards[wid] = []; savedBoards[wid] = [];
} }
delete content["wid"]; //Delete id from content so we don't store it twice delete content["wid"]; //Delete id from content so we don't store it twice
if (tool === "setTextboxText") { if (tool === "setTextboxText") {
for (var i = savedBoards[wid].length - 1; i >= 0; i--) { //Remove old textbox tex -> dont store it twice for (var i = savedBoards[wid].length - 1; i >= 0; i--) {
if (savedBoards[wid][i]["t"] === "setTextboxText" && savedBoards[wid][i]["d"][0] === content["d"][0]) { //Remove old textbox tex -> dont store it twice
if (
savedBoards[wid][i]["t"] === "setTextboxText" &&
savedBoards[wid][i]["d"][0] === content["d"][0]
) {
savedBoards[wid].splice(i, 1); savedBoards[wid].splice(i, 1);
} }
} }
@ -65,7 +95,8 @@ module.exports = {
savedBoards[wid].push(content); savedBoards[wid].push(content);
} }
}, },
loadStoredData: function (wid) { //Load saved whiteboard loadStoredData: function (wid) {
//Load saved whiteboard
return savedBoards[wid] ? savedBoards[wid] : []; return savedBoards[wid] ? savedBoards[wid] : [];
} },
} };

View File

@ -1,63 +1,33 @@
const { getArgs } = require("./utils");
const path = require("path"); const path = require("path");
const config = require("./config/config");
const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo");
function startBackendServer(port) { function startBackendServer(port) {
var accessToken = ""; //Can be set here or as start parameter (node server.js --accesstoken=MYTOKEN)
var disableSmallestScreen = false; //Can be set to true if you dont want to show (node server.js --disablesmallestscreen=true)
var webdav = false; //Can be set to true if you want to allow webdav save (node server.js --webdav=true)
var fs = require("fs-extra"); var fs = require("fs-extra");
var express = require('express'); var express = require("express");
var formidable = require('formidable'); //form upload processing var formidable = require("formidable"); //form upload processing
const createDOMPurify = require('dompurify'); //Prevent xss const createDOMPurify = require("dompurify"); //Prevent xss
const { JSDOM } = require('jsdom'); const { JSDOM } = require("jsdom");
const window = (new JSDOM('')).window; const window = new JSDOM("").window;
const DOMPurify = createDOMPurify(window); const DOMPurify = createDOMPurify(window);
const { createClient } = require("webdav"); const { createClient } = require("webdav");
var s_whiteboard = require("./s_whiteboard.js"); var s_whiteboard = require("./s_whiteboard.js");
var app = express(); var app = express();
app.use(express.static(path.join(__dirname, '..', 'dist'))); app.use(express.static(path.join(__dirname, "..", "dist")));
app.use("/uploads", express.static(path.join(__dirname, '..', 'public', 'uploads'))); app.use("/uploads", express.static(path.join(__dirname, "..", "public", "uploads")));
var server = require('http').Server(app); var server = require("http").Server(app);
server.listen(port); server.listen(port);
var io = require('socket.io')(server, {path: "/ws-api", }); var io = require("socket.io")(server, { path: "/ws-api" });
console.log("Webserver & socketserver running on port:" + port); console.log("Webserver & socketserver running on port:" + port);
if (process.env.accesstoken) {
accessToken = process.env.accesstoken; const { accessToken, enableWebdav } = config.backend;
}
if (process.env.disablesmallestscreen) { app.get("/api/loadwhiteboard", function (req, res) {
disablesmallestscreen = true;
}
if (process.env.webdav) {
webdav = true;
}
var startArgs = getArgs();
if (startArgs["accesstoken"]) {
accessToken = startArgs["accesstoken"];
}
if (startArgs["disablesmallestscreen"]) {
disableSmallestScreen = true;
}
if (startArgs["webdav"]) {
webdav = true;
}
if (accessToken !== "") {
console.log("AccessToken set to: " + accessToken);
}
if (disableSmallestScreen) {
console.log("Disabled showing smallest screen resolution!");
}
if (webdav) {
console.log("Webdav save is enabled!");
}
app.get('/api/loadwhiteboard', function (req, res) {
var wid = req["query"]["wid"]; var wid = req["query"]["wid"];
var at = req["query"]["at"]; //accesstoken var at = req["query"]["at"]; //accesstoken
if (accessToken === "" || accessToken == at) { if (accessToken === "" || accessToken == at) {
@ -65,31 +35,32 @@ function startBackendServer(port) {
res.send(ret); res.send(ret);
res.end(); res.end();
} else { } else {
res.status(401); //Unauthorized res.status(401); //Unauthorized
res.end(); res.end();
} }
}); });
app.post('/api/upload', function (req, res) { //File upload app.post("/api/upload", function (req, res) {
//File upload
var form = new formidable.IncomingForm(); //Receive form var form = new formidable.IncomingForm(); //Receive form
var formData = { var formData = {
files: {}, files: {},
fields: {} fields: {},
} };
form.on('file', function (name, file) { form.on("file", function (name, file) {
formData["files"][file.name] = file; formData["files"][file.name] = file;
}); });
form.on('field', function (name, value) { form.on("field", function (name, value) {
formData["fields"][name] = value; formData["fields"][name] = value;
}); });
form.on('error', function (err) { form.on("error", function (err) {
console.log('File uplaod Error!'); console.log("File uplaod Error!");
}); });
form.on('end', function () { form.on("end", function () {
if (accessToken === "" || accessToken == formData["fields"]["at"]) { if (accessToken === "" || accessToken == formData["fields"]["at"]) {
progressUploadFormData(formData, function (err) { progressUploadFormData(formData, function (err) {
if (err) { if (err) {
@ -104,22 +75,22 @@ function startBackendServer(port) {
} }
}); });
} else { } else {
res.status(401); //Unauthorized res.status(401); //Unauthorized
res.end(); res.end();
} }
//End file upload //End file upload
}); });
form.parse(req); form.parse(req);
}); });
function progressUploadFormData(formData, callback) { function progressUploadFormData(formData, callback) {
console.log("Progress new Form Data"); console.log("Progress new Form Data");
var fields = escapeAllContentStrings(formData.fields); var fields = escapeAllContentStrings(formData.fields);
var files = formData.files; var files = formData.files;
var whiteboardId = fields["whiteboardId"]; var whiteboardId = fields["whiteboardId"];
var name = fields["name"] || ""; var name = fields["name"] || "";
var date = fields["date"] || (+new Date()); var date = fields["date"] || +new Date();
var filename = whiteboardId + "_" + date + ".png"; var filename = whiteboardId + "_" + date + ".png";
var webdavaccess = fields["webdavaccess"] || false; var webdavaccess = fields["webdavaccess"] || false;
try { try {
@ -133,24 +104,33 @@ function startBackendServer(port) {
return; return;
} }
var imagedata = fields["imagedata"]; var imagedata = fields["imagedata"];
if (imagedata && imagedata != "") { //Save from base64 data if (imagedata && imagedata != "") {
imagedata = imagedata.replace(/^data:image\/png;base64,/, "").replace(/^data:image\/jpeg;base64,/, ""); //Save from base64 data
imagedata = imagedata
.replace(/^data:image\/png;base64,/, "")
.replace(/^data:image\/jpeg;base64,/, "");
console.log(filename, "uploaded"); console.log(filename, "uploaded");
fs.writeFile('./public/uploads/' + filename, imagedata, 'base64', function (err) { fs.writeFile("./public/uploads/" + filename, imagedata, "base64", function (err) {
if (err) { if (err) {
console.log("error", err); console.log("error", err);
callback(err); callback(err);
} else { } else {
if (webdavaccess) { //Save image to webdav if (webdavaccess) {
if (webdav) { //Save image to webdav
saveImageToWebdav('./public/uploads/' + filename, filename, webdavaccess, function (err) { if (enableWebdav) {
if (err) { saveImageToWebdav(
console.log("error", err); "./public/uploads/" + filename,
callback(err); filename,
} else { webdavaccess,
callback(); function (err) {
if (err) {
console.log("error", err);
callback(err);
} else {
callback();
}
} }
}) );
} else { } else {
callback("Webdav is not enabled on the server!"); callback("Webdav is not enabled on the server!");
} }
@ -165,114 +145,138 @@ function startBackendServer(port) {
} }
}); });
} }
function saveImageToWebdav(imagepath, filename, webdavaccess, callback) { function saveImageToWebdav(imagepath, filename, webdavaccess, callback) {
if (webdavaccess) { if (webdavaccess) {
var webdavserver = webdavaccess["webdavserver"] || ""; var webdavserver = webdavaccess["webdavserver"] || "";
var webdavpath = webdavaccess["webdavpath"] || "/"; var webdavpath = webdavaccess["webdavpath"] || "/";
var webdavusername = webdavaccess["webdavusername"] || ""; var webdavusername = webdavaccess["webdavusername"] || "";
var webdavpassword = webdavaccess["webdavpassword"] || ""; var webdavpassword = webdavaccess["webdavpassword"] || "";
const client = createClient( const client = createClient(webdavserver, {
webdavserver, username: webdavusername,
{ password: webdavpassword,
username: webdavusername,
password: webdavpassword
}
)
client.getDirectoryContents(webdavpath).then((items) => {
var cloudpath = webdavpath+ '' + filename;
console.log("webdav saving to:", cloudpath);
fs.createReadStream(imagepath).pipe(client.createWriteStream(cloudpath));
callback();
}).catch((error) => {
callback("403");
console.log("Could not connect to webdav!")
}); });
client
.getDirectoryContents(webdavpath)
.then((items) => {
var cloudpath = webdavpath + "" + filename;
console.log("webdav saving to:", cloudpath);
fs.createReadStream(imagepath).pipe(client.createWriteStream(cloudpath));
callback();
})
.catch((error) => {
callback("403");
console.log("Could not connect to webdav!");
});
} else { } else {
callback("Error: no access data!") callback("Error: no access data!");
} }
} }
var smallestScreenResolutions = {}; /**
io.on('connection', function (socket) { * @type {Map<string, WhiteboardServerSideInfo>}
var whiteboardId = null; */
const infoByWhiteboard = new Map();
socket.on('disconnect', function () {
if (smallestScreenResolutions && smallestScreenResolutions[whiteboardId] && socket && socket.id) { setInterval(() => {
delete smallestScreenResolutions[whiteboardId][socket.id]; infoByWhiteboard.forEach((info, whiteboardId) => {
if (info.shouldSendInfo()) {
io.sockets
.in(whiteboardId)
.compress(false)
.emit("whiteboardInfoUpdate", info.asObject());
info.infoWasSent();
} }
socket.broadcast.emit('refreshUserBadges', null); //Removes old user Badges
sendSmallestScreenResolution();
}); });
}, (1 / config.backend.performance.whiteboardInfoBroadcastFreq) * 1000);
socket.on('drawToWhiteboard', function (content) {
io.on("connection", function (socket) {
var whiteboardId = null;
socket.on("disconnect", function () {
if (infoByWhiteboard.has(whiteboardId)) {
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
if (socket && socket.id) {
whiteboardServerSideInfo.deleteScreenResolutionOfClient(socket.id);
}
whiteboardServerSideInfo.decrementNbConnectedUsers();
if (whiteboardServerSideInfo.hasConnectedUser()) {
socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges
} else {
infoByWhiteboard.delete(whiteboardId);
}
}
});
socket.on("drawToWhiteboard", function (content) {
content = escapeAllContentStrings(content); content = escapeAllContentStrings(content);
if (accessToken === "" || accessToken == content["at"]) { if (accessToken === "" || accessToken == content["at"]) {
socket.broadcast.to(whiteboardId).emit('drawToWhiteboard', content); //Send to all users in the room (not own socket) socket.compress(false).broadcast.to(whiteboardId).emit("drawToWhiteboard", content); //Send to all users in the room (not own socket)
s_whiteboard.handleEventsAndData(content); //save whiteboardchanges on the server s_whiteboard.handleEventsAndData(content); //save whiteboardchanges on the server
} else { } else {
socket.emit('wrongAccessToken', true); socket.emit("wrongAccessToken", true);
} }
}); });
socket.on('joinWhiteboard', function (content) { socket.on("joinWhiteboard", function (content) {
content = escapeAllContentStrings(content); content = escapeAllContentStrings(content);
if (accessToken === "" || accessToken == content["at"]) { if (accessToken === "" || accessToken == content["at"]) {
socket.emit("whiteboardConfig", { common: config.frontend });
whiteboardId = content["wid"]; whiteboardId = content["wid"];
socket.join(whiteboardId); //Joins room name=wid socket.join(whiteboardId); //Joins room name=wid
smallestScreenResolutions[whiteboardId] = smallestScreenResolutions[whiteboardId] ? smallestScreenResolutions[whiteboardId] : {}; if (!infoByWhiteboard.has(whiteboardId)) {
smallestScreenResolutions[whiteboardId][socket.id] = content["windowWidthHeight"] || { w: 10000, h: 10000 }; infoByWhiteboard.set(whiteboardId, new WhiteboardServerSideInfo());
sendSmallestScreenResolution(); }
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
whiteboardServerSideInfo.incrementNbConnectedUsers();
whiteboardServerSideInfo.setScreenResolutionForClient(
socket.id,
content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution
);
} else { } else {
socket.emit('wrongAccessToken', true); socket.emit("wrongAccessToken", true);
} }
}); });
socket.on('updateScreenResolution', function (content) { socket.on("updateScreenResolution", function (content) {
content = escapeAllContentStrings(content); content = escapeAllContentStrings(content);
if (smallestScreenResolutions[whiteboardId] && (accessToken === "" || accessToken == content["at"])) { if (accessToken === "" || accessToken == content["at"]) {
smallestScreenResolutions[whiteboardId][socket.id] = content["windowWidthHeight"] || { w: 10000, h: 10000 }; const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
sendSmallestScreenResolution(); whiteboardServerSideInfo.setScreenResolutionForClient(
socket.id,
content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution
);
} }
}); });
function sendSmallestScreenResolution() {
if (disableSmallestScreen) {
return;
}
var smallestWidth = 10000;
var smallestHeight = 10000;
for (var i in smallestScreenResolutions[whiteboardId]) {
smallestWidth = smallestWidth > smallestScreenResolutions[whiteboardId][i]["w"] ? smallestScreenResolutions[whiteboardId][i]["w"] : smallestWidth;
smallestHeight = smallestHeight > smallestScreenResolutions[whiteboardId][i]["h"] ? smallestScreenResolutions[whiteboardId][i]["h"] : smallestHeight;
}
io.to(whiteboardId).emit('updateSmallestScreenResolution', { w: smallestWidth, h: smallestHeight });
}
}); });
//Prevent cross site scripting (xss) //Prevent cross site scripting (xss)
function escapeAllContentStrings(content, cnt) { function escapeAllContentStrings(content, cnt) {
if (!cnt) if (!cnt) cnt = 0;
cnt = 0;
if (typeof content === "string") {
if (typeof (content) === "string") {
return DOMPurify.sanitize(content); return DOMPurify.sanitize(content);
} }
for (var i in content) { for (var i in content) {
if (typeof (content[i]) === "string") { if (typeof content[i] === "string") {
content[i] = DOMPurify.sanitize(content[i]); content[i] = DOMPurify.sanitize(content[i]);
} if (typeof (content[i]) === "object" && cnt < 10) { }
if (typeof content[i] === "object" && cnt < 10) {
content[i] = escapeAllContentStrings(content[i], ++cnt); content[i] = escapeAllContentStrings(content[i], ++cnt);
} }
} }
return content; return content;
} }
process.on('unhandledRejection', error => { process.on("unhandledRejection", (error) => {
// Will print "unhandledRejection err is not defined" // Will print "unhandledRejection err is not defined"
console.log('unhandledRejection', error.message); console.log("unhandledRejection", error.message);
}) });
} }
module.exports = startBackendServer; module.exports = startBackendServer;

View File

@ -1,34 +1,33 @@
const devServerConfig = { const devServerConfig = {
hot: true, hot: true,
inline: true, inline: true,
stats: { stats: {
children: false, children: false,
maxModules: 0 maxModules: 0,
}, },
proxy: { proxy: {
// proxies for the backend // proxies for the backend
'/api': 'http://localhost:3000', "/api": "http://localhost:3000",
'/uploads': 'http://localhost:3000', "/uploads": "http://localhost:3000",
'/ws-api': { "/ws-api": {
target: 'ws://localhost:3000', target: "ws://localhost:3000",
ws: true, ws: true,
} },
} },
} };
function startFrontendDevServer(port) { function startFrontendDevServer(port) {
// require here to prevent prod dependency to webpack // require here to prevent prod dependency to webpack
const webpack = require("webpack"); const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server"); const WebpackDevServer = require("webpack-dev-server");
const config = require("../config/webpack.dev"); const config = require("../config/webpack.dev");
new WebpackDevServer(webpack(config), devServerConfig) new WebpackDevServer(webpack(config), devServerConfig).listen(port, (err) => {
.listen(port, (err) => { if (err) {
if (err) { console.log(err);
console.log(err); }
}
console.log("Listening on port " + port); console.log("Listening on port " + port);
}); });
} }

View File

@ -4,23 +4,23 @@ const startBackendServer = require("./server-backend");
const SERVER_MODES = { const SERVER_MODES = {
PRODUCTION: 1, PRODUCTION: 1,
DEVELOPMENT: 2 DEVELOPMENT: 2,
} };
const args = getArgs(); const args = getArgs();
if ( typeof args.mode === "undefined") { if (typeof args.mode === "undefined") {
// default to production mode // default to production mode
args.mode = "production"; args.mode = "production";
} }
if (args.mode !== "production" && args.mode !== "development") { if (args.mode !== "production" && args.mode !== "development") {
throw new Error("--mode can only be 'development' or 'production'") throw new Error("--mode can only be 'development' or 'production'");
} }
const server_mode = args.mode === "production" ? SERVER_MODES.PRODUCTION : SERVER_MODES.DEVELOPMENT; const server_mode = args.mode === "production" ? SERVER_MODES.PRODUCTION : SERVER_MODES.DEVELOPMENT;
if (server_mode === SERVER_MODES.DEVELOPMENT){ if (server_mode === SERVER_MODES.DEVELOPMENT) {
console.info("Starting server in development mode."); console.info("Starting server in development mode.");
startFrontendDevServer(8080); startFrontendDevServer(8080);
// this time, it's the frontend server that is on port 8080 // this time, it's the frontend server that is on port 8080

View File

@ -1,22 +1,20 @@
function getArgs() { function getArgs() {
const args = {} const args = {};
process.argv process.argv.slice(2, process.argv.length).forEach((arg) => {
.slice(2, process.argv.length) // long arg
.forEach(arg => { if (arg.slice(0, 2) === "--") {
// long arg const longArg = arg.split("=");
if (arg.slice(0, 2) === '--') { args[longArg[0].slice(2, longArg[0].length)] = longArg[1];
const longArg = arg.split('=') }
args[longArg[0].slice(2, longArg[0].length)] = longArg[1] // flags
} else if (arg[0] === "-") {
// flags const flags = arg.slice(1, arg.length).split("");
else if (arg[0] === '-') { flags.forEach((flag) => {
const flags = arg.slice(1, arg.length).split('') args[flag] = true;
flags.forEach(flag => { });
args[flag] = true }
}) });
} return args;
})
return args
} }
module.exports.getArgs = getArgs; module.exports.getArgs = getArgs;

View File

@ -1,3 +1,7 @@
:root {
--selected-icon-bg-color: #dfdfdf;
}
body { body {
position: relative; position: relative;
margin: 0px; margin: 0px;
@ -13,7 +17,8 @@ body {
.btn-group button { .btn-group button {
background: transparent; background: transparent;
border: 1px solid #636060; border: 2px solid #636060;
margin: -1px;
/* Green border */ /* Green border */
color: black; color: black;
/* White text */ /* White text */
@ -25,15 +30,29 @@ body {
/* Float the buttons side by side */ /* Float the buttons side by side */
font-size: 1.2em; font-size: 1.2em;
height: 45px; height: 45px;
width: 50px;
} }
button::-moz-focus-inner { button::-moz-focus-inner {
border: 0; border: 0;
} }
.btn-group button:not(:last-child) { .whiteboard-edit-group.group-disabled {
border-right: none; background: repeating-linear-gradient(
/* Prevent double borders */ 45deg,
rgba(255, 166, 0, 0.366),
rgba(255, 166, 0, 0.366) 10px,
rgba(255, 166, 0, 0.666) 10px,
rgba(255, 166, 0, 0.666) 20px
);
}
/*
* Deactivate all pointer events on all the children
* of a group when it's disabled.
*/
.whiteboard-edit-group.group-disabled > * {
pointer-events: none;
} }
/* Clear floats (clearfix hack) */ /* Clear floats (clearfix hack) */
@ -57,12 +76,13 @@ button {
.btn-group { .btn-group {
background-color: #808080ab; background-color: #808080ab;
margin-left: 5px; margin-left: 5px;
margin-bottom: 5px;
float: left; float: left;
position: relative; position: relative;
} }
.whiteboardTool.active { .whiteboard-tool.active:not(:disabled) {
background: #bfbfbf; background: var(--selected-icon-bg-color);
} }
#whiteboardThicknessSlider { #whiteboardThicknessSlider {
@ -73,19 +93,21 @@ button {
background: transparent; background: transparent;
outline: none; outline: none;
opacity: 1; opacity: 1;
-webkit-transition: opacity .15s ease-in-out; -webkit-transition: opacity 0.15s ease-in-out;
transition: opacity .15s ease-in-out; transition: opacity 0.15s ease-in-out;
} }
.textBox.active { .textBox.active {
border: 1px dashed gray; border: 1px dashed gray;
} }
.textBox>.removeIcon, .textBox>.moveIcon { .textBox > .removeIcon,
.textBox > .moveIcon {
display: none; display: none;
} }
.textBox.active>.removeIcon, .textBox.active>.moveIcon { .textBox.active > .removeIcon,
.textBox.active > .moveIcon {
display: block; display: block;
} }
@ -95,4 +117,18 @@ button {
border: 0px; border: 0px;
min-width: 50px; min-width: 50px;
cursor: pointer; cursor: pointer;
} }
#displayWhiteboardInfoBtn.active {
background: var(--selected-icon-bg-color);
}
#whiteboardInfoContainer {
position: absolute;
bottom: 10px;
right: 10px;
}
.displayNone {
display: none;
}

View File

@ -1,129 +1,262 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<title>Whiteboard</title>
<meta charset="utf-8" />
<link rel="icon" type="image/vnd.microsoft.icon" href="favicon.ico" />
</head>
<head> <body>
<title>Whiteboard</title> <!---Whiteboard container -!-->
<meta charset="utf-8" /> <div id="whiteboardContainer"></div>
<link rel="icon" type="image/vnd.microsoft.icon" href="favicon.ico">
</head>
<body> <!---Toolbar -!-->
<!---Whiteboard container -!--> <div id="toolbar" style="position: absolute; top: 10px; left: 10px;">
<div id="whiteboardContainer"></div> <div class="btn-group">
<button
id="whiteboardLockBtn"
style="background-color: orange;"
title="View and Write"
type="button"
>
<i class="fa fa-lock"></i>
</button>
<button id="whiteboardUnlockBtn" title="View Only" type="button">
<i class="fa fa-lock-open"></i>
</button>
</div>
<!---Toolbar -!--> <div class="btn-group whiteboard-edit-group">
<div id="toolbar" style="position: absolute; top: 10px; left: 10px;"> <button id="whiteboardTrashBtn" title="Clear the whiteboard" type="button">
<div class="btn-group"> <i class="fa fa-trash"></i>
<button id="whiteboardTrashBtn" title="Clear the whiteboard" type="button" class="whiteboardBtn"> </button>
<i class="fa fa-trash"></i> <button
</button> style="position: absolute; left: 0px; top: 0px; width: 46px; display: none;"
<button style="position:absolute; left:0px; top:0px; width: 46px; display:none;" id="whiteboardTrashBtnConfirm"
id="whiteboardTrashBtnConfirm" title="Confirm clear..." type="button" class="whiteboardBtn"> title="Confirm clear..."
<i class="fa fa-check"></i> type="button"
</button> >
<button id="whiteboardUndoBtn" title="Undo your last step" type="button" class="whiteboardBtn"> <i class="fa fa-check"></i>
<i class="fa fa-undo"></i> </button>
</button> <button id="whiteboardUndoBtn" title="Undo your last step" type="button">
<button id="whiteboardRedoBtn" title="Redo your last undo" type="button" class="whiteboardBtn"> <i class="fa fa-undo"></i>
<i class="fa fa-redo"></i> </button>
</button> <button id="whiteboardRedoBtn" title="Redo your last undo" type="button">
</div> <i class="fa fa-redo"></i>
</button>
</div>
<div class="btn-group"> <div class="btn-group whiteboard-edit-group">
<button tool="mouse" title="Take the mouse" type="button" class="whiteboardTool"> <button tool="mouse" title="Take the mouse" type="button" class="whiteboard-tool">
<i class="fa fa-mouse-pointer"></i> <i class="fa fa-mouse-pointer"></i>
</button> </button>
<button style="padding-bottom: 11px;" tool="recSelect" title="Select an area" type="button" <button
class="whiteboardTool"> style="padding-bottom: 11px;"
<img src="./images/dottedRec.png"> tool="recSelect"
</button> title="Select an area"
<button tool="pen" title="Take the pen" type="button" class="whiteboardTool active"> type="button"
<i class="fa fa-pencil-alt"></i> class="whiteboard-tool"
</button> >
<button style="padding-bottom: 8px; padding-top: 6px;" tool="line" title="draw a line" type="button" <img src="./images/dottedRec.png" />
class="whiteboardTool"> </button>
<button
</button> tool="pen"
<button tool="rect" title="draw a rectangle" type="button" class="whiteboardTool"> title="Take the pen"
<i class="far fa-square"></i> type="button"
</button> class="whiteboard-tool active"
<button tool="circle" title="draw a circle" type="button" class="whiteboardTool"> >
<i class="far fa-circle"></i> <i class="fa fa-pencil-alt"></i>
</button> </button>
<button tool="text" title="write text" type="button" class="whiteboardTool"> <button
<i class="fas fa-font"></i> style="padding-bottom: 8px; padding-top: 6px;"
</button> tool="line"
<button tool="eraser" title="take the eraser" type="button" class="whiteboardTool"> title="draw a line"
<i class="fa fa-eraser"></i> type="button"
</button> class="whiteboard-tool"
</div> >
</button>
<button tool="rect" title="draw a rectangle" type="button" class="whiteboard-tool">
<i class="far fa-square"></i>
</button>
<button tool="circle" title="draw a circle" type="button" class="whiteboard-tool">
<i class="far fa-circle"></i>
</button>
<button tool="text" title="write text" type="button" class="whiteboard-tool">
<i class="fas fa-font"></i>
</button>
<button tool="eraser" title="take the eraser" type="button" class="whiteboard-tool">
<i class="fa fa-eraser"></i>
</button>
</div>
<div class="btn-group"> <div class="btn-group whiteboard-edit-group">
<button style="width: 190px; cursor:default;"> <button style="width: 190px; cursor: default;">
<div class="activeToolIcon" style="position:absolute; top:2px; left:2px; font-size: 0.6em;"><i <div
class="fa fa-pencil-alt"></i></div> class="activeToolIcon"
<img style="position: absolute; left: 11px; top: 16px; height:14px; width:130px;" style="position: absolute; top: 2px; left: 2px; font-size: 0.6em;"
src="./images/slider-background.svg"> >
<input title="Thickness" id="whiteboardThicknessSlider" <i class="fa fa-pencil-alt"></i>
style="position: absolute; left:9px; width: 130px; top: 15px;" type="range" min="1" max="50" </div>
value="3"> <img
<div id="whiteboardColorpicker" style="
style="position: absolute; left: 155px; top: 10px; width: 26px; height: 23px; border-radius: 3px; border: 1px solid darkgrey;" position: absolute;
data-color="#000000"> left: 11px;
</div> top: 16px;
</button> height: 14px;
</div> width: 130px;
"
src="./images/slider-background.svg"
/>
<input
title="Thickness"
id="whiteboardThicknessSlider"
style="position: absolute; left: 9px; width: 130px; top: 15px;"
type="range"
min="1"
max="50"
value="3"
/>
<div
id="whiteboardColorpicker"
style="
position: absolute;
left: 155px;
top: 10px;
width: 26px;
height: 23px;
border-radius: 3px;
border: 1px solid darkgrey;
"
data-color="#000000"
></div>
</button>
</div>
<div class="btn-group"> <div class="btn-group whiteboard-edit-group">
<button id="saveAsImageBtn" title="Save whiteboard as image" type="button" class="whiteboardBtn"> <button id="addImgToCanvasBtn" title="Upload Image to whiteboard" type="button">
<i class="fas fa-image"></i> <i class="fas fa-image"></i>
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; " <i
class="fas fa-save"></i> style="
</button> position: absolute;
<button style="position: relative; display: none;" id="uploadWebDavBtn" title="Save whiteboard to webdav" top: 3px;
type="button" class="whiteboardBtn"> left: 2px;
color: #000000;
font-size: 0.5em;
"
class="fas fa-upload"
></i>
</button>
<i class="fas fa-globe"></i> <button
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; " style="position: relative;"
class="fas fa-save"></i> id="uploadJsonBtn"
</button> title="Load saved JSON to whiteboard"
<button style="position: relative;" id="saveAsJSONBtn" title="Save whiteboard as JSON" type="button" type="button"
class="whiteboardBtn"> >
<i class="far fa-file-alt"></i> <i class="far fa-file-alt"></i>
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; " <i
class="fas fa-save"></i> style="
</button> position: absolute;
</div> top: 3px;
left: 2px;
color: #000000;
font-size: 0.5em;
"
class="fas fa-upload"
></i>
</button>
<div class="btn-group"> <input style="display: none;" id="myFile" type="file" />
<button id="addImgToCanvasBtn" title="Upload Image to whiteboard" type="button" class="whiteboardBtn"> </div>
<i class="fas fa-image"></i>
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; "
class="fas fa-upload"></i>
</button>
<button style="position: relative;" id="uploadJsonBtn" title="Load saved JSON to whiteboard" type="button" <div class="btn-group">
class="whiteboardBtn"> <button id="saveAsImageBtn" title="Save whiteboard as image" type="button">
<i class="fas fa-image"></i>
<i
style="
position: absolute;
top: 3px;
left: 2px;
color: #000000;
font-size: 0.5em;
"
class="fas fa-save"
></i>
</button>
<button
style="position: relative; display: none;"
id="uploadWebDavBtn"
title="Save whiteboard to webdav"
type="button"
>
<i class="fas fa-globe"></i>
<i
style="
position: absolute;
top: 3px;
left: 2px;
color: #000000;
font-size: 0.5em;
"
class="fas fa-save"
></i>
</button>
<button
style="position: relative;"
id="saveAsJSONBtn"
title="Save whiteboard as JSON"
type="button"
>
<i class="far fa-file-alt"></i>
<i
style="
position: absolute;
top: 3px;
left: 2px;
color: #000000;
font-size: 0.5em;
"
class="fas fa-save"
></i>
</button>
<i class="far fa-file-alt"></i> <button id="shareWhiteboardBtn" title="share whiteboard" type="button">
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; " <i class="fas fa-share-square"></i>
class="fas fa-upload"></i> </button>
</button>
<input style="display:none;" id="myFile" type="file" />
<button id="shareWhiteboardBtn" title="share whiteboard" type="button"> <button id="displayWhiteboardInfoBtn" title="Show whiteboard info" type="button">
<i class="fas fa-share-square"></i> <i class="fas fa-info-circle"></i>
</button> </button>
</div> </div>
<div class="btn-group minGroup"> <div class="btn-group minGroup">
<button style="width: 25px; padding: 11px 11px;" id="minMaxBtn" title="hide buttons" type="button"> <button
<i id="minBtn" style="position:relative; left:-5px;" class="fas fa-angle-left"></i> style="width: 25px; padding: 11px 11px;"
<i id="maxBtn" style="position:relative; left:-5px; display: none;" class="fas fa-angle-right"></i> id="minMaxBtn"
</button> title="hide buttons"
</div> type="button"
</div> >
<i
id="minBtn"
style="position: relative; left: -5px;"
class="fas fa-angle-left"
></i>
<i
id="maxBtn"
style="position: relative; left: -5px; display: none;"
class="fas fa-angle-right"
></i>
</button>
</div>
</div>
</body> <div id="whiteboardInfoContainer">
<p><b>Whiteboard information:</b></p>
</html> <p># connected users: <i id="connectedUsersCount">0</i></p>
<p>Smallest screen resolution: <i id="smallestScreenResolution">Unknown.</i></p>
<p># msg. sent to server: <i id="messageSentCount">0</i></p>
<p># msg. received from server: <i id="messageReceivedCount">0</i></p>
</div>
</body>
</html>

79
src/js/classes/Point.js Normal file
View File

@ -0,0 +1,79 @@
import { computeDist } from "../utils";
class Point {
/**
* @type {number}
*/
#x;
get x() {
return this.#x;
}
/**
* @type {number}
*/
#y;
get y() {
return this.#y;
}
/**
* @type {Point}
*/
static #lastKnownPos = new Point(0, 0);
static get lastKnownPos() {
return Point.#lastKnownPos;
}
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
this.#x = x;
this.#y = y;
}
get isZeroZero() {
return this.#x === 0 && this.#y === 0;
}
/**
* Get a Point object from an event
* @param {event} e
* @returns {Point}
*/
static fromEvent(e) {
// the epsilon hack is required to detect touches
const epsilon = 0.0001;
let x = (e.offsetX || e.pageX - $(e.target).offset().left) + epsilon;
let y = (e.offsetY || e.pageY - $(e.target).offset().top) + epsilon;
if (Number.isNaN(x) || Number.isNaN(y) || (x === epsilon && y === epsilon)) {
// if it's a touch actually
if (e.touches && e.touches.length && e.touches.length > 0) {
const touch = e.touches[0];
x = touch.clientX - $("#mouseOverlay").offset().left;
y = touch.clientY - $("#mouseOverlay").offset().top;
} else {
// if it's a touchend event
return Point.#lastKnownPos;
}
}
Point.#lastKnownPos = new Point(x - epsilon, y - epsilon);
return Point.#lastKnownPos;
}
/**
* Compute euclidean distance between points
*
* @param {Point} otherPoint
* @returns {number}
*/
distTo(otherPoint) {
return computeDist(this, otherPoint);
}
}
export default Point;

View File

@ -16,6 +16,9 @@ import {
faAngleRight, faAngleRight,
faSortDown, faSortDown,
faExpandArrowsAlt, faExpandArrowsAlt,
faLock,
faLockOpen,
faInfoCircle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { import {
faSquare, faSquare,
@ -46,7 +49,10 @@ library.add(
faCircle, faCircle,
faFile, faFile,
faFileAlt, faFileAlt,
faPlusSquare faPlusSquare,
faLock,
faLockOpen,
faInfoCircle
); );
dom.i2svg() dom.i2svg();

View File

@ -1,7 +1,7 @@
import "jquery-ui/ui/core"; import "jquery-ui/ui/core";
import "jquery-ui/ui/widgets/draggable"; import "jquery-ui/ui/widgets/draggable";
import "jquery-ui/ui/widgets/resizable"; import "jquery-ui/ui/widgets/resizable";
import "jquery-ui-rotatable/jquery.ui.rotatable" import "jquery-ui-rotatable/jquery.ui.rotatable";
import "jquery-ui/themes/base/resizable.css"; import "jquery-ui/themes/base/resizable.css";
import "../css/main.css"; import "../css/main.css";
@ -9,19 +9,19 @@ import "./icons";
import main from "./main"; import main from "./main";
$(document).ready(function () { $(document).ready(function () {
// Set correct width height on mobile browsers // Set correct width height on mobile browsers
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
if (isChrome) { if (isChrome) {
$('head').append('<meta name="viewport" content="width=device-width, initial-scale=0.52, maximum-scale=1" />'); $("head").append(
'<meta name="viewport" content="width=device-width, initial-scale=0.52, maximum-scale=1" />'
);
} else { } else {
$('head').append('<meta name="viewport" content="width=1400" />'); $("head").append('<meta name="viewport" content="width=1400" />');
} }
main(); main();
}); });
if (module.hot) { if (module.hot) {
module.hot.accept(); module.hot.accept();
} }

View File

@ -1,48 +1,48 @@
/* ----------- /* -----------
KEYBINDINGS KEYBINDINGS
----------- */ ----------- */
//> defmod is "command" on OS X and "ctrl" elsewhere //> defmod is "command" on OS X and "ctrl" elsewhere
//Advanced Example: 'defmod-k j' -> For this to fire you have to first press both ctrl and k, and then j. //Advanced Example: 'defmod-k j' -> For this to fire you have to first press both ctrl and k, and then j.
const keybinds = { const keybinds = {
// 'key(s)' : 'function', // 'key(s)' : 'function',
'defmod-shift-z' : 'clearWhiteboard', "defmod-shift-z": "clearWhiteboard",
'defmod-z' : 'undoStep', "defmod-z": "undoStep",
'defmod-y' : 'redoStep', "defmod-y": "redoStep",
'defmod-x' : 'setTool_recSelect', "defmod-x": "setTool_recSelect",
'defmod-m' : 'setTool_mouse', "defmod-m": "setTool_mouse",
'defmod-p' : 'setTool_pen', "defmod-p": "setTool_pen",
'defmod-l' : 'setTool_line', "defmod-l": "setTool_line",
'defmod-r' : 'setTool_rect', "defmod-r": "setTool_rect",
'defmod-c' : 'setTool_circle', "defmod-c": "setTool_circle",
'defmod-shift-f' : 'toggleLineRecCircle', "defmod-shift-f": "toggleLineRecCircle",
'defmod-shift-x' : 'togglePenEraser', "defmod-shift-x": "togglePenEraser",
'defmod-shift-r' : 'toggleMainColors', "defmod-shift-r": "toggleMainColors",
'defmod-a' : 'setTool_text', "defmod-a": "setTool_text",
'defmod-e' : 'setTool_eraser', "defmod-e": "setTool_eraser",
'defmod-up' : 'thickness_bigger', "defmod-up": "thickness_bigger",
'defmod-down' : 'thickness_smaller', "defmod-down": "thickness_smaller",
'defmod-shift-c' : 'openColorPicker', "defmod-shift-c": "openColorPicker",
'defmod-shift-1' : 'setDrawColorBlack', "defmod-shift-1": "setDrawColorBlack",
'defmod-shift-2' : 'setDrawColorBlue', "defmod-shift-2": "setDrawColorBlue",
'defmod-shift-3' : 'setDrawColorGreen', "defmod-shift-3": "setDrawColorGreen",
'defmod-shift-4' : 'setDrawColorYellow', "defmod-shift-4": "setDrawColorYellow",
'defmod-shift-5' : 'setDrawColorRed', "defmod-shift-5": "setDrawColorRed",
'defmod-s' : 'saveWhiteboardAsImage', "defmod-s": "saveWhiteboardAsImage",
'defmod-shift-k' : 'saveWhiteboardAsJson', "defmod-shift-k": "saveWhiteboardAsJson",
'defmod-shift-i' : 'uploadWhiteboardToWebDav', "defmod-shift-i": "uploadWhiteboardToWebDav",
'defmod-shift-j' : 'uploadJsonToWhiteboard', "defmod-shift-j": "uploadJsonToWhiteboard",
'defmod-shift-s' : 'shareWhiteboard', "defmod-shift-s": "shareWhiteboard",
'tab' : 'hideShowControls', tab: "hideShowControls",
'up' : 'moveDraggableUp', up: "moveDraggableUp",
'down' : 'moveDraggableDown', down: "moveDraggableDown",
'left' : 'moveDraggableLeft', left: "moveDraggableLeft",
'right' : 'moveDraggableRight', right: "moveDraggableRight",
'defmod-enter' : 'dropDraggable', "defmod-enter": "dropDraggable",
'shift-enter' : 'addToBackground', "shift-enter": "addToBackground",
'escape' : 'cancelAllActions', escape: "cancelAllActions",
'del' : 'deleteSelection' del: "deleteSelection",
} };
export default keybinds; export default keybinds;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,118 @@
import { getThrottling } from "./ConfigService.utils";
/**
* Class to hold the configuration sent by the backend
*/
class ConfigService {
/**
* @type {object}
*/
#configFromServer = {};
get configFromServer() {
return this.#configFromServer;
}
/**
* @type {{displayInfo: boolean, setReadOnly: boolean}}
* @readonly
*/
#onWhiteboardLoad = { setReadOnly: false, displayInfo: false };
get readOnlyOnWhiteboardLoad() {
return this.#onWhiteboardLoad.setReadOnly;
}
get displayInfoOnWhiteboardLoad() {
return this.#onWhiteboardLoad.displayInfo;
}
/**
* @type {boolean}
*/
#showSmallestScreenIndicator = true;
get showSmallestScreenIndicator() {
return this.#showSmallestScreenIndicator;
}
/**
* @type {string}
*/
#imageDownloadFormat = "png";
get imageDownloadFormat() {
return this.#imageDownloadFormat;
}
/**
* @type {boolean}
*/
#drawBackgroundGrid = false;
get drawBackgroundGrid() {
return this.#drawBackgroundGrid;
}
/**
* @type {string}
*/
#backgroundGridImage = "bg_grid.png";
get backgroundGridImage() {
return this.#backgroundGridImage;
}
/**
* @type {{minDistDelta: number, minTimeDelta: number}}
*/
#pointerEventsThrottling = { minDistDelta: 0, minTimeDelta: 0 };
get pointerEventsThrottling() {
return this.#pointerEventsThrottling;
}
/**
* @type {number}
*/
#refreshInfoInterval = 1000;
get refreshInfoInterval() {
return this.#refreshInfoInterval;
}
/**
* Init the service from the config sent by the server
*
* @param {object} configFromServer
*/
initFromServer(configFromServer) {
this.#configFromServer = configFromServer;
const { common } = configFromServer;
const {
onWhiteboardLoad,
showSmallestScreenIndicator,
imageDownloadFormat,
drawBackgroundGrid,
backgroundGridImage,
performance,
} = common;
this.#onWhiteboardLoad = onWhiteboardLoad;
this.#showSmallestScreenIndicator = showSmallestScreenIndicator;
this.#imageDownloadFormat = imageDownloadFormat;
this.#drawBackgroundGrid = drawBackgroundGrid;
this.#backgroundGridImage = backgroundGridImage;
this.#refreshInfoInterval = 1000 / performance.refreshInfoFreq;
console.log("Whiteboard config from server:", configFromServer, "parsed:", this);
}
/**
* Refresh config that depends on the number of user connected to whiteboard
*
* @param {number} userCount
*/
refreshUserCountDependant(userCount) {
const { configFromServer } = this;
const { common } = configFromServer;
const { performance } = common;
const { pointerEventsThrottling } = performance;
this.#pointerEventsThrottling = getThrottling(pointerEventsThrottling, userCount);
}
}
export default new ConfigService();

View File

@ -0,0 +1,21 @@
/**
* Helper to extract the correct throttling values based on the config and the number of user
*
* @param {Array.<{fromUserCount: number, minDistDelta: number, maxFreq: number}>} pointerEventsThrottling
* @param {number} userCount
* @return {{minDistDelta: number, minTimeDelta: number}}
*/
export function getThrottling(pointerEventsThrottling, userCount) {
let tmpOut = pointerEventsThrottling[0];
let lastDistToUserCount = userCount - tmpOut.fromUserCount;
if (lastDistToUserCount < 0) lastDistToUserCount = Number.MAX_VALUE;
for (const el of pointerEventsThrottling) {
const distToUserCount = userCount - el.fromUserCount;
if (el.fromUserCount <= userCount && distToUserCount <= lastDistToUserCount) {
tmpOut = el;
lastDistToUserCount = distToUserCount;
}
}
return { minDistDelta: tmpOut.minDistDelta, minTimeDelta: 1000 * (1 / tmpOut.maxFreq) };
}

View File

@ -0,0 +1,29 @@
import { getThrottling } from "./ConfigService.utils";
test("Simple throttling config", () => {
const throttling = [{ fromUserCount: 0, minDistDelta: 1, maxFreq: 1 }];
const target0 = { minDistDelta: 1, minTimeDelta: 1000 };
expect(getThrottling(throttling, 0)).toEqual(target0);
const target100 = { minDistDelta: 1, minTimeDelta: 1000 };
expect(getThrottling(throttling, 100)).toEqual(target100);
});
test("Complex throttling config", () => {
// mix ordering
const throttling = [
{ fromUserCount: 100, minDistDelta: 100, maxFreq: 1 },
{ fromUserCount: 0, minDistDelta: 1, maxFreq: 1 },
{ fromUserCount: 50, minDistDelta: 50, maxFreq: 1 },
];
const target0 = { minDistDelta: 1, minTimeDelta: 1000 };
expect(getThrottling(throttling, 0)).toEqual(target0);
const target50 = { minDistDelta: 50, minTimeDelta: 1000 };
expect(getThrottling(throttling, 50)).toEqual(target50);
const target100 = { minDistDelta: 100, minTimeDelta: 1000 };
expect(getThrottling(throttling, 100)).toEqual(target100);
});

View File

@ -0,0 +1,137 @@
import ConfigService from "./ConfigService";
/**
* Class the handle the information about the whiteboard
*/
class InfoService {
/**
* @type {boolean}
*/
#infoAreDisplayed = false;
get infoAreDisplayed() {
return this.#infoAreDisplayed;
}
/**
* Holds the number of user connected to the server
*
* @type {number}
*/
#nbConnectedUsers = -1;
get nbConnectedUsers() {
return this.#nbConnectedUsers;
}
/**
* @type {{w: number, h: number}}
*/
#smallestScreenResolution = undefined;
get smallestScreenResolution() {
return this.#smallestScreenResolution;
}
/**
* @type {number}
*/
#nbMessagesSent = 0;
get nbMessagesSent() {
return this.#nbMessagesSent;
}
/**
* @type {number}
*/
#nbMessagesReceived = 0;
get nbMessagesReceived() {
return this.#nbMessagesReceived;
}
/**
* Holds the interval Id
* @type {number}
*/
#refreshInfoIntervalId = undefined;
get refreshInfoIntervalId() {
return this.#refreshInfoIntervalId;
}
/**
* @param {number} nbConnectedUsers
* @param {{w: number, h: number}} smallestScreenResolution
*/
updateInfoFromServer({ nbConnectedUsers, smallestScreenResolution = undefined }) {
if (this.#nbConnectedUsers !== nbConnectedUsers) {
// Refresh config service parameters on nb connected user change
ConfigService.refreshUserCountDependant(nbConnectedUsers);
}
this.#nbConnectedUsers = nbConnectedUsers;
if (smallestScreenResolution) {
this.#smallestScreenResolution = smallestScreenResolution;
}
}
incrementNbMessagesReceived() {
this.#nbMessagesReceived++;
}
incrementNbMessagesSent() {
this.#nbMessagesSent++;
}
refreshDisplayedInfo() {
const {
nbMessagesReceived,
nbMessagesSent,
nbConnectedUsers,
smallestScreenResolution: ssr,
} = this;
$("#messageReceivedCount")[0].innerText = String(nbMessagesReceived);
$("#messageSentCount")[0].innerText = String(nbMessagesSent);
$("#connectedUsersCount")[0].innerText = String(nbConnectedUsers);
$("#smallestScreenResolution")[0].innerText = ssr ? `(${ssr.w}, ${ssr.h})` : "Unknown";
}
/**
* Show the info div
*/
displayInfo() {
$("#whiteboardInfoContainer").toggleClass("displayNone", false);
$("#displayWhiteboardInfoBtn").toggleClass("active", true);
this.#infoAreDisplayed = true;
this.refreshDisplayedInfo();
this.#refreshInfoIntervalId = setInterval(() => {
// refresh only on a specific interval to reduce
// refreshing cost
this.refreshDisplayedInfo();
}, ConfigService.refreshInfoInterval);
}
/**
* Hide the info div
*/
hideInfo() {
$("#whiteboardInfoContainer").toggleClass("displayNone", true);
$("#displayWhiteboardInfoBtn").toggleClass("active", false);
this.#infoAreDisplayed = false;
const { refreshInfoIntervalId } = this;
if (refreshInfoIntervalId) {
clearInterval(refreshInfoIntervalId);
this.#refreshInfoIntervalId = undefined;
}
}
/**
* Switch between hiding and showing the info div
*/
toggleDisplayInfo() {
const { infoAreDisplayed } = this;
if (infoAreDisplayed) {
this.hideInfo();
} else {
this.displayInfo();
}
}
}
export default new InfoService();

View File

@ -0,0 +1,57 @@
/**
* Class the handle the read-only logic
*/
class ReadOnlyService {
/**
* @type {boolean}
*/
#readOnlyActive = true;
get readOnlyActive() {
return this.#readOnlyActive;
}
/**
* @type {object}
*/
#previousToolHtmlElem = null;
get previousToolHtmlElem() {
return this.#previousToolHtmlElem;
}
/**
* Activate read-only mode
*/
activateReadOnlyMode() {
this.#readOnlyActive = true;
this.#previousToolHtmlElem = $(".whiteboard-tool.active");
// switch to mouse tool to prevent the use of the
// other tools
$(".whiteboard-tool[tool=mouse]").click();
$(".whiteboard-tool").prop("disabled", true);
$(".whiteboard-edit-group > button").prop("disabled", true);
$(".whiteboard-edit-group").addClass("group-disabled");
$("#whiteboardUnlockBtn").hide();
$("#whiteboardLockBtn").show();
}
/**
* Deactivate read-only mode
*/
deactivateReadOnlyMode() {
this.#readOnlyActive = false;
$(".whiteboard-tool").prop("disabled", false);
$(".whiteboard-edit-group > button").prop("disabled", false);
$(".whiteboard-edit-group").removeClass("group-disabled");
$("#whiteboardUnlockBtn").show();
$("#whiteboardLockBtn").hide();
// restore previously selected tool
const { previousToolHtmlElem } = this;
if (previousToolHtmlElem) previousToolHtmlElem.click();
}
}
export default new ReadOnlyService();

View File

@ -0,0 +1,48 @@
import Point from "../classes/Point";
import { getCurrentTimeMs } from "../utils";
import ConfigService from "./ConfigService";
/**
* Class to handle all the throttling logic
*/
class ThrottlingService {
/**
* @type {number}
*/
#lastSuccessTime = 0;
get lastSuccessTime() {
return this.#lastSuccessTime;
}
/**
* @type {Point}
*/
#lastPointPosition = new Point(0, 0);
get lastPointPosition() {
return this.#lastPointPosition;
}
/**
* Helper to throttle events based on the configuration.
* Only if checks are ok, the onSuccess callback will be called.
*
* @param {Point} newPosition New point position to base the throttling on
* @param {function()} onSuccess Callback called when the throttling is successful
*/
throttle(newPosition, onSuccess) {
const newTime = getCurrentTimeMs();
const { lastPointPosition, lastSuccessTime } = this;
if (newTime - lastSuccessTime > ConfigService.pointerEventsThrottling.minTimeDelta) {
if (
lastPointPosition.distTo(newPosition) >
ConfigService.pointerEventsThrottling.minDistDelta
) {
onSuccess();
this.#lastPointPosition = newPosition;
this.#lastSuccessTime = newTime;
}
}
}
}
export default new ThrottlingService();

147
src/js/shortcutFunctions.js Normal file
View File

@ -0,0 +1,147 @@
import whiteboard from "./whiteboard";
import ReadOnlyService from "./services/ReadOnlyService";
/**
* @param {function} callback
* @param {boolean} readOnlySensitive should the shortcut function be active event when the whiteboard is in read-only mode
*/
function defineShortcut(callback, readOnlySensitive = true) {
return () => {
if (readOnlySensitive && ReadOnlyService.readOnlyActive) return;
callback();
};
}
const shortcutFunctions = {
clearWhiteboard: defineShortcut(() => whiteboard.clearWhiteboard()),
undoStep: defineShortcut(() => whiteboard.undoWhiteboardClick()),
redoStep: defineShortcut(() => whiteboard.redoWhiteboardClick()),
setTool_mouse: defineShortcut(() => $(".whiteboard-tool[tool=mouse]").click()),
setTool_recSelect: defineShortcut(() => $(".whiteboard-tool[tool=recSelect]").click()),
setTool_pen: defineShortcut(() => {
$(".whiteboard-tool[tool=pen]").click();
whiteboard.redrawMouseCursor();
}),
setTool_line: defineShortcut(() => $(".whiteboard-tool[tool=line]").click()),
setTool_rect: defineShortcut(() => $(".whiteboard-tool[tool=rect]").click()),
setTool_circle: defineShortcut(() => $(".whiteboard-tool[tool=circle]").click()),
setTool_text: defineShortcut(() => $(".whiteboard-tool[tool=text]").click()),
setTool_eraser: defineShortcut(() => {
$(".whiteboard-tool[tool=eraser]").click();
whiteboard.redrawMouseCursor();
}),
thickness_bigger: defineShortcut(() => {
const thickness = parseInt($("#whiteboardThicknessSlider").val()) + 1;
$("#whiteboardThicknessSlider").val(thickness);
whiteboard.setStrokeThickness(thickness);
whiteboard.redrawMouseCursor();
}),
thickness_smaller: defineShortcut(() => {
const thickness = parseInt($("#whiteboardThicknessSlider").val()) - 1;
$("#whiteboardThicknessSlider").val(thickness);
whiteboard.setStrokeThickness(thickness);
whiteboard.redrawMouseCursor();
}),
openColorPicker: defineShortcut(() => $("#whiteboardColorpicker").click()),
saveWhiteboardAsImage: defineShortcut(() => $("#saveAsImageBtn").click(), false),
saveWhiteboardAsJson: defineShortcut(() => $("#saveAsJSONBtn").click(), false),
uploadWhiteboardToWebDav: defineShortcut(() => $("#uploadWebDavBtn").click()),
uploadJsonToWhiteboard: defineShortcut(() => $("#uploadJsonBtn").click()),
shareWhiteboard: defineShortcut(() => $("#shareWhiteboardBtn").click(), false),
hideShowControls: defineShortcut(() => $("#minMaxBtn").click(), false),
setDrawColorBlack: defineShortcut(() => {
whiteboard.setDrawColor("black");
whiteboard.redrawMouseCursor();
}),
setDrawColorRed: defineShortcut(() => {
whiteboard.setDrawColor("red");
whiteboard.redrawMouseCursor();
}),
setDrawColorGreen: defineShortcut(() => {
whiteboard.setDrawColor("green");
whiteboard.redrawMouseCursor();
}),
setDrawColorBlue: defineShortcut(() => {
whiteboard.setDrawColor("blue");
whiteboard.redrawMouseCursor();
}),
setDrawColorYellow: defineShortcut(() => {
whiteboard.setDrawColor("yellow");
whiteboard.redrawMouseCursor();
}),
toggleLineRecCircle: defineShortcut(() => {
const activeTool = $(".whiteboard-tool.active").attr("tool");
if (activeTool === "line") {
$(".whiteboard-tool[tool=rect]").click();
} else if (activeTool === "rect") {
$(".whiteboard-tool[tool=circle]").click();
} else {
$(".whiteboard-tool[tool=line]").click();
}
}),
togglePenEraser: defineShortcut(() => {
const activeTool = $(".whiteboard-tool.active").attr("tool");
if (activeTool === "pen") {
$(".whiteboard-tool[tool=eraser]").click();
} else {
$(".whiteboard-tool[tool=pen]").click();
}
}),
toggleMainColors: defineShortcut(() => {
const bgColor = $("#whiteboardColorpicker")[0].style.backgroundColor;
if (bgColor === "blue") {
shortcutFunctions.setDrawColorGreen();
} else if (bgColor === "green") {
shortcutFunctions.setDrawColorYellow();
} else if (bgColor === "yellow") {
shortcutFunctions.setDrawColorRed();
} else if (bgColor === "red") {
shortcutFunctions.setDrawColorBlack();
} else {
shortcutFunctions.setDrawColorBlue();
}
}),
moveDraggableUp: defineShortcut(() => {
const elm =
whiteboard.tool === "text"
? $("#" + whiteboard.latestActiveTextBoxId)
: $(".dragMe")[0];
const p = $(elm).position();
if (p) $(elm).css({ top: p.top - 5, left: p.left });
}),
moveDraggableDown: defineShortcut(() => {
const elm =
whiteboard.tool === "text"
? $("#" + whiteboard.latestActiveTextBoxId)
: $(".dragMe")[0];
const p = $(elm).position();
if (p) $(elm).css({ top: p.top + 5, left: p.left });
}),
moveDraggableLeft: defineShortcut(() => {
const elm =
whiteboard.tool === "text"
? $("#" + whiteboard.latestActiveTextBoxId)
: $(".dragMe")[0];
const p = $(elm).position();
if (p) $(elm).css({ top: p.top, left: p.left - 5 });
}),
moveDraggableRight: defineShortcut(() => {
const elm =
whiteboard.tool === "text"
? $("#" + whiteboard.latestActiveTextBoxId)
: $(".dragMe")[0];
const p = $(elm).position();
if (p) $(elm).css({ top: p.top, left: p.left + 5 });
}),
dropDraggable: defineShortcut(() => {
$($(".dragMe")[0]).find(".addToCanvasBtn").click();
}),
addToBackground: defineShortcut(() => {
$($(".dragMe")[0]).find(".addToBackgroundBtn").click();
}),
cancelAllActions: defineShortcut(() => whiteboard.escKeyAction()),
deleteSelection: defineShortcut(() => whiteboard.delKeyAction()),
};
export default shortcutFunctions;

44
src/js/utils.js Normal file
View File

@ -0,0 +1,44 @@
/**
* Compute the euclidean distance between two points
* @param {Point} p1
* @param {Point} p2
*/
export function computeDist(p1, p2) {
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
/**
* Return the current time in ms since 1970
* @returns {number}
*/
export function getCurrentTimeMs() {
return new Date().getTime();
}
/**
* get 'GET' parameter by variable name
* @param variable
* @return {boolean|*}
*/
export function getQueryVariable(variable) {
const query = window.location.search.substring(1);
const vars = query.split("&");
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split("=");
if (pair[0] === variable) {
return pair[1];
}
}
return false;
}
export function getSubDir() {
const url = document.URL.substr(0, document.URL.lastIndexOf("/"));
const urlSplit = url.split("/");
let subdir = "";
for (let i = 3; i < urlSplit.length; i++) {
subdir = subdir + "/" + urlSplit[i];
}
return subdir;
}

File diff suppressed because it is too large Load Diff