Merge branch 'master' into rotation
# Conflicts: # package.json
This commit is contained in:
commit
4ff618be8d
12
.editorconfig
Normal file
12
.editorconfig
Normal 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
18
.github/workflows/build-docker.yml
vendored
Normal 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)
|
22
.github/workflows/linting-testing-code.yml
vendored
Normal file
22
.github/workflows/linting-testing-code.yml
vendored
Normal 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
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
/config.run.yml
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 100
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
FROM node:11 as base
|
||||
FROM node:12 as base
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /opt/app
|
||||
@ -19,7 +19,7 @@ RUN npm run build
|
||||
# Final image
|
||||
#####################
|
||||
|
||||
FROM node:11-alpine
|
||||
FROM node:12-alpine
|
||||
ENV NODE_ENV=prod
|
||||
|
||||
MAINTAINER cracker0dks
|
||||
@ -28,7 +28,7 @@ MAINTAINER cracker0dks
|
||||
RUN mkdir -p /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
|
||||
|
||||
COPY scripts ./scripts
|
||||
|
189
README.md
189
README.md
@ -1,34 +1,51 @@
|
||||
# whiteboard
|
||||
|
||||
This is a lightweight NodeJS collaborative Whiteboard/Sketchboard witch can easily be customized...
|
||||
|
||||
![start](./doc/start.png)
|
||||
|
||||
## Demowhiteboard ##
|
||||
## Demowhiteboard
|
||||
|
||||
[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
|
||||
* Shows remote user cursors while drawing
|
||||
* Undo / Redo function for each user
|
||||
* Drag+Drop / Copy+Paste Images or PDFs from PC and Browsers
|
||||
* Resize, Move & Draw Images to Canvas or Background
|
||||
* Write text
|
||||
* Save Whiteboard to Image and JSON
|
||||
* Draw angle lines by pressing "shift" while drawing (with line tool)
|
||||
* Draw square by pressing "shift" while drawing (with rectangle tool)
|
||||
* Indicator that shows the smallest screen participating
|
||||
* Keybindings for ALL the functions
|
||||
* Working on PC, Tablet & Mobile
|
||||
|
||||
- Shows remote user cursors while drawing
|
||||
- Undo / Redo function for each user
|
||||
- Drag+Drop / Copy+Paste Images or PDFs from PC and Browsers
|
||||
- Resize, Move & Draw Images to Canvas or Background
|
||||
- Write text
|
||||
- Save Whiteboard to Image and JSON
|
||||
- Draw angle lines by pressing "shift" while drawing (with line tool)
|
||||
- Draw square by pressing "shift" while drawing (with rectangle tool)
|
||||
- Indicator that shows the smallest screen participating
|
||||
- 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
|
||||
|
||||
You can run this app with and without docker
|
||||
|
||||
### Without Docker
|
||||
1. install the latest NodeJs
|
||||
|
||||
1. install the latest NodeJs (version >= 12)
|
||||
2. Clone the app
|
||||
3. Run `npm ci` inside the folder
|
||||
4. Run `npm run start:prod`
|
||||
5. Surf to http://YOURIP:8080
|
||||
|
||||
### With Docker
|
||||
|
||||
1. `docker run -d -p 8080:8080 rofl256/whiteboard`
|
||||
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.
|
||||
|
||||
## Default keyboard shortcuts
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
------ | -------------------- | -------
|
||||
Clear the whiteboard | Ctrl + Shift + Z | Command + Shift + Z
|
||||
Undo your last step | Ctrl + Z | Command + Z
|
||||
Redo your last undo | Ctrl + Y | Command + Y
|
||||
Select an area | Ctrl + X | Command + X
|
||||
Take the mouse | Ctrl + M | Command + M
|
||||
Take the pen | Ctrl + P | Command + P
|
||||
Draw a line | Ctrl + L | Command + L
|
||||
Draw a rectangle | Ctrl + R | Command + R
|
||||
Draw a circle | Ctrl + C | Command + C
|
||||
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 main clolors (black, blue, green, yellow and red) | Ctrl + Shift + R | Command + Shift + R
|
||||
Write text | Ctrl + A | Command + A
|
||||
Take the eraser | Ctrl + E | Command + E
|
||||
Increase thickness | Ctrl + Up Arrow | Command + Up Arrow
|
||||
Decrease thickness | Ctrl + Down Arrow | Command + Down Arrow
|
||||
Colorpicker | Ctrl + Shift + C | Command + Shift + C
|
||||
Set black color | Ctrl + Shift + 1 | Command + Shift + 1
|
||||
Set blue color | Ctrl + Shift + 2 | Command + Shift + 2
|
||||
Set green color | Ctrl + Shift + 3 | Command + Shift + 3
|
||||
Set yellow color | Ctrl + Shift + 4 | Command + Shift + 4
|
||||
Set red color | Ctrl + Shift + 5 | Command + Shift + 5
|
||||
Save whiteboard as image | Ctrl + S | Command + S
|
||||
Save whiteboard as JSON | Ctrl + Shift + K | Command + Shift + K
|
||||
Save whiteboard to WebDav | Ctrl + Shift + I (i) | Command + Shift + I (i)
|
||||
Load saved JSON to whiteboard | Ctrl + Shift + J | Command + Shift + J
|
||||
Share whiteboard | Ctrl + Shift + S | Command + Shift + S
|
||||
Hide or show toolbar | Tab | Tab
|
||||
Move selected object up | Up Arrow | Up Arrow
|
||||
Move selected object down | Down Arrow | Down Arrow
|
||||
Move selected object left | Left Arrow | Left Arrow
|
||||
Move selected object right | Right Arrow | Right Arrow
|
||||
Drop object | Ctrl + Enter | Command + Enter
|
||||
Add Image to backgroud | Shift + Enter | Shift + Enter
|
||||
Cancel all actions | Escape | Escape
|
||||
Delete selected object | Delete | Delete
|
||||
| Result | Windows and Linux | macOS |
|
||||
| ---------------------------------------------------------------- | -------------------- | ----------------------- |
|
||||
| Clear the whiteboard | Ctrl + Shift + Z | Command + Shift + Z |
|
||||
| Undo your last step | Ctrl + Z | Command + Z |
|
||||
| Redo your last undo | Ctrl + Y | Command + Y |
|
||||
| Select an area | Ctrl + X | Command + X |
|
||||
| Take the mouse | Ctrl + M | Command + M |
|
||||
| Take the pen | Ctrl + P | Command + P |
|
||||
| Draw a line | Ctrl + L | Command + L |
|
||||
| Draw a rectangle | Ctrl + R | Command + R |
|
||||
| Draw a circle | Ctrl + C | Command + C |
|
||||
| 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 main clolors (black, blue, green, yellow and red) | Ctrl + Shift + R | Command + Shift + R |
|
||||
| Write text | Ctrl + A | Command + A |
|
||||
| Take the eraser | Ctrl + E | Command + E |
|
||||
| Increase thickness | Ctrl + Up Arrow | Command + Up Arrow |
|
||||
| Decrease thickness | Ctrl + Down Arrow | Command + Down Arrow |
|
||||
| Colorpicker | Ctrl + Shift + C | Command + Shift + C |
|
||||
| Set black color | Ctrl + Shift + 1 | Command + Shift + 1 |
|
||||
| Set blue color | Ctrl + Shift + 2 | Command + Shift + 2 |
|
||||
| Set green color | Ctrl + Shift + 3 | Command + Shift + 3 |
|
||||
| Set yellow color | Ctrl + Shift + 4 | Command + Shift + 4 |
|
||||
| Set red color | Ctrl + Shift + 5 | Command + Shift + 5 |
|
||||
| Save whiteboard as image | Ctrl + S | Command + S |
|
||||
| Save whiteboard as JSON | Ctrl + Shift + K | Command + Shift + K |
|
||||
| Save whiteboard to WebDav | Ctrl + Shift + I (i) | Command + Shift + I (i) |
|
||||
| Load saved JSON to whiteboard | Ctrl + Shift + J | Command + Shift + J |
|
||||
| Share whiteboard | Ctrl + Shift + S | Command + Shift + S |
|
||||
| Hide or show toolbar | Tab | Tab |
|
||||
| Move selected object up | Up Arrow | Up Arrow |
|
||||
| Move selected object down | Down Arrow | Down Arrow |
|
||||
| Move selected object left | Left Arrow | Left Arrow |
|
||||
| Move selected object right | Right Arrow | Right Arrow |
|
||||
| Drop object | Ctrl + Enter | Command + Enter |
|
||||
| Add Image to backgroud | Shift + Enter | Shift + Enter |
|
||||
| Cancel all actions | Escape | Escape |
|
||||
| Delete selected object | Delete | Delete |
|
||||
| Use Line tool when pen is active (Not changeable) | Shift (Hold) | Shift (Hold) |
|
||||
|
||||
## URL Parameters
|
||||
|
||||
Call your site with GET parameters to change the WhiteboardID or the Username
|
||||
|
||||
`http://YOURIP:8080?whiteboardid=MYID&username=MYNAME`
|
||||
|
||||
* whiteboardid => All people with the same ID are drawing on the same board
|
||||
* username => The name witch is showing to others while drawing
|
||||
* title => Change the name of the Browser Tab
|
||||
* randomid => if set to true, a random whiteboardId will be generated if not given aswell
|
||||
- whiteboardid => All people with the same ID are drawing on the same board
|
||||
- username => The name witch is showing to others while drawing
|
||||
- title => Change the name of the Browser Tab
|
||||
- randomid => if set to true, a random whiteboardId will be generated if not given aswell
|
||||
|
||||
## 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.
|
||||
## Configuration
|
||||
|
||||
<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:
|
||||
|
||||
@ -105,14 +139,11 @@ Then set the same token on the client side as well:
|
||||
|
||||
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.
|
||||
|
||||
To enable it:
|
||||
|
||||
<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`
|
||||
To enable set `enableWebdav` to `true` in the [configuration](./config.default.yml).
|
||||
|
||||
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!
|
||||
|
||||
### 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
|
||||
* 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)
|
||||
* accesstoken => take a look at "Security - AccessToken" for a full explanation
|
||||
* 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)
|
||||
- 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)
|
||||
|
||||
## ToDo
|
||||
* Make undo function more reliable on texts
|
||||
|
||||
- Make undo function more reliable on texts
|
||||
|
||||
## Nginx Reverse Proxy configuration
|
||||
|
||||
Add this to your server part:
|
||||
|
||||
```
|
||||
location /whiteboard/ {
|
||||
proxy_set_header HOST $host;
|
||||
@ -148,22 +181,22 @@ Add this to your server part:
|
||||
proxy_pass http://YOURIP:8080/;
|
||||
}
|
||||
```
|
||||
|
||||
To run it at /whiteboard. Don't forget to change -> YOURIP!
|
||||
|
||||
## Nextcloud integration
|
||||
|
||||
1. Install this app on your server
|
||||
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}`
|
||||
You can give each group its own whiteboard by changeing the WHITEBOARDNAME in the URL if you want.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
![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.
|
||||
|
||||
|
||||
|
||||
|
||||
___ MIT License ___
|
||||
**_ MIT License _**
|
||||
|
BIN
assets/images/bg_dots.png
Normal file
BIN
assets/images/bg_dots.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 673 B |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
54
config.default.yml
Normal file
54
config.default.yml
Normal 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
|
@ -1,61 +1,59 @@
|
||||
const webpack = require("webpack");
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const path = require("path");
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
main: ["./src/js/index.js"],
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, "..", "dist"),
|
||||
filename: "[name]-[hash].js"
|
||||
},
|
||||
resolve: {
|
||||
extensions: ["*", ".json", ".js"]
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
compact: true
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
},
|
||||
entry: {
|
||||
main: ["./src/js/index.js"],
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, "..", "dist"),
|
||||
filename: "[name]-[hash].js",
|
||||
},
|
||||
resolve: {
|
||||
extensions: ["*", ".json", ".js"],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
compact: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: "file-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.ProvidePlugin({
|
||||
$: 'jquery',
|
||||
jQuery: 'jquery',
|
||||
"window.jQuery": "jquery",
|
||||
"window.$": "jquery",
|
||||
}),
|
||||
new CopyPlugin([
|
||||
{ from: 'assets', to: '' },
|
||||
]),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html',
|
||||
minify: false,
|
||||
inject: true
|
||||
})
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.ProvidePlugin({
|
||||
$: "jquery",
|
||||
jQuery: "jquery",
|
||||
"window.jQuery": "jquery",
|
||||
"window.$": "jquery",
|
||||
}),
|
||||
new CopyPlugin([{ from: "assets", to: "" }]),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "src/index.html",
|
||||
minify: false,
|
||||
inject: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
@ -2,10 +2,10 @@ const merge = require("webpack-merge");
|
||||
const baseConfig = require("./webpack.base");
|
||||
|
||||
module.exports = merge(baseConfig, {
|
||||
mode: "production",
|
||||
optimization: {
|
||||
minimize: true,
|
||||
nodeEnv: "production",
|
||||
},
|
||||
devtool: false
|
||||
mode: "production",
|
||||
optimization: {
|
||||
minimize: true,
|
||||
nodeEnv: "production",
|
||||
},
|
||||
devtool: false,
|
||||
});
|
||||
|
@ -3,16 +3,16 @@ const baseConfig = require("./webpack.base");
|
||||
const webpack = require("webpack");
|
||||
|
||||
const devConfig = merge(baseConfig, {
|
||||
mode: "development",
|
||||
devtool: "eval-source-map",
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
].concat(baseConfig.plugins),
|
||||
mode: "development",
|
||||
devtool: "eval-source-map",
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
].concat(baseConfig.plugins),
|
||||
});
|
||||
|
||||
module.exports = devConfig;
|
||||
|
13
doc/updating_guide.md
Normal file
13
doc/updating_guide.md
Normal 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`
|
@ -1,9 +1,8 @@
|
||||
version: '3.1'
|
||||
version: "3.1"
|
||||
services:
|
||||
whiteboard:
|
||||
image: rofl256/whiteboard
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080/tcp"
|
||||
environment:
|
||||
- ACCESSTOKEN=mysecrettoken
|
||||
command: --config=./config.default.yml
|
||||
|
4577
package-lock.json
generated
4577
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -8,7 +8,10 @@
|
||||
"build": "webpack --config config/webpack.build.js",
|
||||
"start:dev": "node scripts/server.js --mode=development",
|
||||
"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": {
|
||||
"type": "git",
|
||||
@ -19,12 +22,20 @@
|
||||
"Sketchboard",
|
||||
"lightweight"
|
||||
],
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "6.12.2",
|
||||
"dompurify": "^2.0.7",
|
||||
"express": "4.*",
|
||||
"formidable": "1.*",
|
||||
"fs-extra": "7.*",
|
||||
"jquery-ui-rotatable": "^1.1.0",
|
||||
"html2canvas": "^1.0.0-rc.5",
|
||||
"js-yaml": "3.13.1",
|
||||
"jsdom": "^14.0.0",
|
||||
"pdfjs-dist": "^2.3.200",
|
||||
"socket.io": "2.*",
|
||||
@ -46,9 +57,13 @@
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^3.5.2",
|
||||
"html-webpack-plugin": "^4.2.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "26.0.1",
|
||||
"jquery": "^3.2.1",
|
||||
"jquery-ui": "^1.12.1",
|
||||
"keymage": "^1.1.3",
|
||||
"prettier": "^2.0.5",
|
||||
"pretty-quick": "^2.0.1",
|
||||
"style-loader": "^1.1.4",
|
||||
"vanilla-picker": "^2.10.1",
|
||||
"webpack": "^4.42.1",
|
||||
|
99
scripts/WhiteboardServerSideInfo.js
Normal file
99
scripts/WhiteboardServerSideInfo.js
Normal 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;
|
99
scripts/config/config-schema.json
Normal file
99
scripts/config/config-schema.json
Normal 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
80
scripts/config/config.js
Normal 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
92
scripts/config/utils.js
Normal 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;
|
48
scripts/config/utils.test.js
Normal file
48
scripts/config/utils.test.js
Normal 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);
|
||||
});
|
@ -7,10 +7,12 @@ module.exports = {
|
||||
var tool = content["t"]; //Tool witch is used
|
||||
var wid = content["wid"]; //whiteboard ID
|
||||
var username = content["username"];
|
||||
if (tool === "clear") { //Clear the whiteboard
|
||||
if (tool === "clear") {
|
||||
//Clear the whiteboard
|
||||
delete savedBoards[wid];
|
||||
delete savedUndos[wid];
|
||||
} else if (tool === "undo") { //Undo an action
|
||||
} else if (tool === "undo") {
|
||||
//Undo an action
|
||||
if (!savedUndos[wid]) {
|
||||
savedUndos[wid] = [];
|
||||
}
|
||||
@ -19,7 +21,10 @@ module.exports = {
|
||||
if (savedBoards[wid][i]["username"] == username) {
|
||||
var drawId = savedBoards[wid][i]["drawId"];
|
||||
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]);
|
||||
savedBoards[wid].splice(i, 1);
|
||||
}
|
||||
@ -27,7 +32,7 @@ module.exports = {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(savedUndos[wid].length > 1000) {
|
||||
if (savedUndos[wid].length > 1000) {
|
||||
savedUndos[wid].splice(0, savedUndos[wid].length - 1000);
|
||||
}
|
||||
}
|
||||
@ -42,7 +47,10 @@ module.exports = {
|
||||
if (savedUndos[wid][i]["username"] == username) {
|
||||
var drawId = savedUndos[wid][i]["drawId"];
|
||||
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]);
|
||||
savedUndos[wid].splice(i, 1);
|
||||
}
|
||||
@ -50,14 +58,36 @@ module.exports = {
|
||||
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]) {
|
||||
savedBoards[wid] = [];
|
||||
}
|
||||
delete content["wid"]; //Delete id from content so we don't store it twice
|
||||
if (tool === "setTextboxText") {
|
||||
for (var i = savedBoards[wid].length - 1; i >= 0; i--) { //Remove old textbox tex -> dont store it twice
|
||||
if (savedBoards[wid][i]["t"] === "setTextboxText" && savedBoards[wid][i]["d"][0] === content["d"][0]) {
|
||||
for (var i = savedBoards[wid].length - 1; i >= 0; i--) {
|
||||
//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);
|
||||
}
|
||||
}
|
||||
@ -65,7 +95,8 @@ module.exports = {
|
||||
savedBoards[wid].push(content);
|
||||
}
|
||||
},
|
||||
loadStoredData: function (wid) { //Load saved whiteboard
|
||||
loadStoredData: function (wid) {
|
||||
//Load saved whiteboard
|
||||
return savedBoards[wid] ? savedBoards[wid] : [];
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,63 +1,33 @@
|
||||
const { getArgs } = require("./utils");
|
||||
const path = require("path");
|
||||
|
||||
const config = require("./config/config");
|
||||
const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo");
|
||||
|
||||
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 express = require('express');
|
||||
var formidable = require('formidable'); //form upload processing
|
||||
|
||||
const createDOMPurify = require('dompurify'); //Prevent xss
|
||||
const { JSDOM } = require('jsdom');
|
||||
const window = (new JSDOM('')).window;
|
||||
var express = require("express");
|
||||
var formidable = require("formidable"); //form upload processing
|
||||
|
||||
const createDOMPurify = require("dompurify"); //Prevent xss
|
||||
const { JSDOM } = require("jsdom");
|
||||
const window = new JSDOM("").window;
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
|
||||
const { createClient } = require("webdav");
|
||||
|
||||
|
||||
var s_whiteboard = require("./s_whiteboard.js");
|
||||
|
||||
|
||||
var app = express();
|
||||
app.use(express.static(path.join(__dirname, '..', 'dist')));
|
||||
app.use("/uploads", express.static(path.join(__dirname, '..', 'public', 'uploads')));
|
||||
var server = require('http').Server(app);
|
||||
app.use(express.static(path.join(__dirname, "..", "dist")));
|
||||
app.use("/uploads", express.static(path.join(__dirname, "..", "public", "uploads")));
|
||||
var server = require("http").Server(app);
|
||||
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);
|
||||
if (process.env.accesstoken) {
|
||||
accessToken = process.env.accesstoken;
|
||||
}
|
||||
if (process.env.disablesmallestscreen) {
|
||||
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) {
|
||||
|
||||
const { accessToken, enableWebdav } = config.backend;
|
||||
|
||||
app.get("/api/loadwhiteboard", function (req, res) {
|
||||
var wid = req["query"]["wid"];
|
||||
var at = req["query"]["at"]; //accesstoken
|
||||
if (accessToken === "" || accessToken == at) {
|
||||
@ -65,31 +35,32 @@ function startBackendServer(port) {
|
||||
res.send(ret);
|
||||
res.end();
|
||||
} else {
|
||||
res.status(401); //Unauthorized
|
||||
res.status(401); //Unauthorized
|
||||
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 formData = {
|
||||
files: {},
|
||||
fields: {}
|
||||
}
|
||||
|
||||
form.on('file', function (name, file) {
|
||||
fields: {},
|
||||
};
|
||||
|
||||
form.on("file", function (name, file) {
|
||||
formData["files"][file.name] = file;
|
||||
});
|
||||
|
||||
form.on('field', function (name, value) {
|
||||
|
||||
form.on("field", function (name, value) {
|
||||
formData["fields"][name] = value;
|
||||
});
|
||||
|
||||
form.on('error', function (err) {
|
||||
console.log('File uplaod Error!');
|
||||
|
||||
form.on("error", function (err) {
|
||||
console.log("File uplaod Error!");
|
||||
});
|
||||
|
||||
form.on('end', function () {
|
||||
|
||||
form.on("end", function () {
|
||||
if (accessToken === "" || accessToken == formData["fields"]["at"]) {
|
||||
progressUploadFormData(formData, function (err) {
|
||||
if (err) {
|
||||
@ -104,22 +75,22 @@ function startBackendServer(port) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.status(401); //Unauthorized
|
||||
res.status(401); //Unauthorized
|
||||
res.end();
|
||||
}
|
||||
//End file upload
|
||||
});
|
||||
form.parse(req);
|
||||
});
|
||||
|
||||
|
||||
function progressUploadFormData(formData, callback) {
|
||||
console.log("Progress new Form Data");
|
||||
var fields = escapeAllContentStrings(formData.fields);
|
||||
var files = formData.files;
|
||||
var whiteboardId = fields["whiteboardId"];
|
||||
|
||||
|
||||
var name = fields["name"] || "";
|
||||
var date = fields["date"] || (+new Date());
|
||||
var date = fields["date"] || +new Date();
|
||||
var filename = whiteboardId + "_" + date + ".png";
|
||||
var webdavaccess = fields["webdavaccess"] || false;
|
||||
try {
|
||||
@ -133,24 +104,33 @@ function startBackendServer(port) {
|
||||
return;
|
||||
}
|
||||
var imagedata = fields["imagedata"];
|
||||
if (imagedata && imagedata != "") { //Save from base64 data
|
||||
imagedata = imagedata.replace(/^data:image\/png;base64,/, "").replace(/^data:image\/jpeg;base64,/, "");
|
||||
if (imagedata && imagedata != "") {
|
||||
//Save from base64 data
|
||||
imagedata = imagedata
|
||||
.replace(/^data:image\/png;base64,/, "")
|
||||
.replace(/^data:image\/jpeg;base64,/, "");
|
||||
console.log(filename, "uploaded");
|
||||
fs.writeFile('./public/uploads/' + filename, imagedata, 'base64', function (err) {
|
||||
fs.writeFile("./public/uploads/" + filename, imagedata, "base64", function (err) {
|
||||
if (err) {
|
||||
console.log("error", err);
|
||||
callback(err);
|
||||
} else {
|
||||
if (webdavaccess) { //Save image to webdav
|
||||
if (webdav) {
|
||||
saveImageToWebdav('./public/uploads/' + filename, filename, webdavaccess, function (err) {
|
||||
if (err) {
|
||||
console.log("error", err);
|
||||
callback(err);
|
||||
} else {
|
||||
callback();
|
||||
if (webdavaccess) {
|
||||
//Save image to webdav
|
||||
if (enableWebdav) {
|
||||
saveImageToWebdav(
|
||||
"./public/uploads/" + filename,
|
||||
filename,
|
||||
webdavaccess,
|
||||
function (err) {
|
||||
if (err) {
|
||||
console.log("error", err);
|
||||
callback(err);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
callback("Webdav is not enabled on the server!");
|
||||
}
|
||||
@ -165,114 +145,138 @@ function startBackendServer(port) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function saveImageToWebdav(imagepath, filename, webdavaccess, callback) {
|
||||
if (webdavaccess) {
|
||||
var webdavserver = webdavaccess["webdavserver"] || "";
|
||||
var webdavpath = webdavaccess["webdavpath"] || "/";
|
||||
var webdavusername = webdavaccess["webdavusername"] || "";
|
||||
var webdavpassword = webdavaccess["webdavpassword"] || "";
|
||||
|
||||
const client = createClient(
|
||||
webdavserver,
|
||||
{
|
||||
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!")
|
||||
|
||||
const client = createClient(webdavserver, {
|
||||
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!");
|
||||
});
|
||||
} else {
|
||||
callback("Error: no access data!")
|
||||
callback("Error: no access data!");
|
||||
}
|
||||
}
|
||||
|
||||
var smallestScreenResolutions = {};
|
||||
io.on('connection', function (socket) {
|
||||
var whiteboardId = null;
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
if (smallestScreenResolutions && smallestScreenResolutions[whiteboardId] && socket && socket.id) {
|
||||
delete smallestScreenResolutions[whiteboardId][socket.id];
|
||||
|
||||
/**
|
||||
* @type {Map<string, WhiteboardServerSideInfo>}
|
||||
*/
|
||||
const infoByWhiteboard = new Map();
|
||||
|
||||
setInterval(() => {
|
||||
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();
|
||||
});
|
||||
|
||||
socket.on('drawToWhiteboard', function (content) {
|
||||
}, (1 / config.backend.performance.whiteboardInfoBroadcastFreq) * 1000);
|
||||
|
||||
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);
|
||||
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
|
||||
} else {
|
||||
socket.emit('wrongAccessToken', true);
|
||||
socket.emit("wrongAccessToken", true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('joinWhiteboard', function (content) {
|
||||
|
||||
socket.on("joinWhiteboard", function (content) {
|
||||
content = escapeAllContentStrings(content);
|
||||
if (accessToken === "" || accessToken == content["at"]) {
|
||||
socket.emit("whiteboardConfig", { common: config.frontend });
|
||||
|
||||
whiteboardId = content["wid"];
|
||||
socket.join(whiteboardId); //Joins room name=wid
|
||||
smallestScreenResolutions[whiteboardId] = smallestScreenResolutions[whiteboardId] ? smallestScreenResolutions[whiteboardId] : {};
|
||||
smallestScreenResolutions[whiteboardId][socket.id] = content["windowWidthHeight"] || { w: 10000, h: 10000 };
|
||||
sendSmallestScreenResolution();
|
||||
if (!infoByWhiteboard.has(whiteboardId)) {
|
||||
infoByWhiteboard.set(whiteboardId, new WhiteboardServerSideInfo());
|
||||
}
|
||||
|
||||
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||
whiteboardServerSideInfo.incrementNbConnectedUsers();
|
||||
whiteboardServerSideInfo.setScreenResolutionForClient(
|
||||
socket.id,
|
||||
content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution
|
||||
);
|
||||
} else {
|
||||
socket.emit('wrongAccessToken', true);
|
||||
socket.emit("wrongAccessToken", true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('updateScreenResolution', function (content) {
|
||||
|
||||
socket.on("updateScreenResolution", function (content) {
|
||||
content = escapeAllContentStrings(content);
|
||||
if (smallestScreenResolutions[whiteboardId] && (accessToken === "" || accessToken == content["at"])) {
|
||||
smallestScreenResolutions[whiteboardId][socket.id] = content["windowWidthHeight"] || { w: 10000, h: 10000 };
|
||||
sendSmallestScreenResolution();
|
||||
if (accessToken === "" || accessToken == content["at"]) {
|
||||
const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId);
|
||||
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)
|
||||
function escapeAllContentStrings(content, cnt) {
|
||||
if (!cnt)
|
||||
cnt = 0;
|
||||
|
||||
if (typeof (content) === "string") {
|
||||
if (!cnt) cnt = 0;
|
||||
|
||||
if (typeof content === "string") {
|
||||
return DOMPurify.sanitize(content);
|
||||
}
|
||||
for (var i in content) {
|
||||
if (typeof (content[i]) === "string") {
|
||||
if (typeof content[i] === "string") {
|
||||
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);
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', error => {
|
||||
|
||||
process.on("unhandledRejection", (error) => {
|
||||
// Will print "unhandledRejection err is not defined"
|
||||
console.log('unhandledRejection', error.message);
|
||||
})
|
||||
console.log("unhandledRejection", error.message);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = startBackendServer;
|
||||
module.exports = startBackendServer;
|
||||
|
@ -1,34 +1,33 @@
|
||||
const devServerConfig = {
|
||||
hot: true,
|
||||
inline: true,
|
||||
stats: {
|
||||
children: false,
|
||||
maxModules: 0
|
||||
},
|
||||
proxy: {
|
||||
// proxies for the backend
|
||||
'/api': 'http://localhost:3000',
|
||||
'/uploads': 'http://localhost:3000',
|
||||
'/ws-api': {
|
||||
target: 'ws://localhost:3000',
|
||||
ws: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
hot: true,
|
||||
inline: true,
|
||||
stats: {
|
||||
children: false,
|
||||
maxModules: 0,
|
||||
},
|
||||
proxy: {
|
||||
// proxies for the backend
|
||||
"/api": "http://localhost:3000",
|
||||
"/uploads": "http://localhost:3000",
|
||||
"/ws-api": {
|
||||
target: "ws://localhost:3000",
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function startFrontendDevServer(port) {
|
||||
// require here to prevent prod dependency to webpack
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const config = require("../config/webpack.dev");
|
||||
// require here to prevent prod dependency to webpack
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const config = require("../config/webpack.dev");
|
||||
|
||||
new WebpackDevServer(webpack(config), devServerConfig)
|
||||
.listen(port, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
new WebpackDevServer(webpack(config), devServerConfig).listen(port, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
console.log("Listening on port " + port);
|
||||
console.log("Listening on port " + port);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,23 +4,23 @@ const startBackendServer = require("./server-backend");
|
||||
|
||||
const SERVER_MODES = {
|
||||
PRODUCTION: 1,
|
||||
DEVELOPMENT: 2
|
||||
}
|
||||
DEVELOPMENT: 2,
|
||||
};
|
||||
|
||||
const args = getArgs();
|
||||
|
||||
if ( typeof args.mode === "undefined") {
|
||||
if (typeof args.mode === "undefined") {
|
||||
// default to production mode
|
||||
args.mode = "production";
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (server_mode === SERVER_MODES.DEVELOPMENT){
|
||||
if (server_mode === SERVER_MODES.DEVELOPMENT) {
|
||||
console.info("Starting server in development mode.");
|
||||
startFrontendDevServer(8080);
|
||||
// this time, it's the frontend server that is on port 8080
|
||||
|
@ -1,22 +1,20 @@
|
||||
function getArgs() {
|
||||
const args = {}
|
||||
process.argv
|
||||
.slice(2, process.argv.length)
|
||||
.forEach(arg => {
|
||||
// long arg
|
||||
if (arg.slice(0, 2) === '--') {
|
||||
const longArg = arg.split('=')
|
||||
args[longArg[0].slice(2, longArg[0].length)] = longArg[1]
|
||||
}
|
||||
// flags
|
||||
else if (arg[0] === '-') {
|
||||
const flags = arg.slice(1, arg.length).split('')
|
||||
flags.forEach(flag => {
|
||||
args[flag] = true
|
||||
})
|
||||
}
|
||||
})
|
||||
return args
|
||||
const args = {};
|
||||
process.argv.slice(2, process.argv.length).forEach((arg) => {
|
||||
// long arg
|
||||
if (arg.slice(0, 2) === "--") {
|
||||
const longArg = arg.split("=");
|
||||
args[longArg[0].slice(2, longArg[0].length)] = longArg[1];
|
||||
}
|
||||
// flags
|
||||
else if (arg[0] === "-") {
|
||||
const flags = arg.slice(1, arg.length).split("");
|
||||
flags.forEach((flag) => {
|
||||
args[flag] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
return args;
|
||||
}
|
||||
|
||||
module.exports.getArgs = getArgs;
|
||||
module.exports.getArgs = getArgs;
|
||||
|
@ -1,3 +1,7 @@
|
||||
:root {
|
||||
--selected-icon-bg-color: #dfdfdf;
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
@ -13,7 +17,8 @@ body {
|
||||
|
||||
.btn-group button {
|
||||
background: transparent;
|
||||
border: 1px solid #636060;
|
||||
border: 2px solid #636060;
|
||||
margin: -1px;
|
||||
/* Green border */
|
||||
color: black;
|
||||
/* White text */
|
||||
@ -25,15 +30,29 @@ body {
|
||||
/* Float the buttons side by side */
|
||||
font-size: 1.2em;
|
||||
height: 45px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.btn-group button:not(:last-child) {
|
||||
border-right: none;
|
||||
/* Prevent double borders */
|
||||
.whiteboard-edit-group.group-disabled {
|
||||
background: repeating-linear-gradient(
|
||||
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) */
|
||||
@ -57,12 +76,13 @@ button {
|
||||
.btn-group {
|
||||
background-color: #808080ab;
|
||||
margin-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.whiteboardTool.active {
|
||||
background: #bfbfbf;
|
||||
.whiteboard-tool.active:not(:disabled) {
|
||||
background: var(--selected-icon-bg-color);
|
||||
}
|
||||
|
||||
#whiteboardThicknessSlider {
|
||||
@ -73,19 +93,21 @@ button {
|
||||
background: transparent;
|
||||
outline: none;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity .15s ease-in-out;
|
||||
transition: opacity .15s ease-in-out;
|
||||
-webkit-transition: opacity 0.15s ease-in-out;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.textBox.active {
|
||||
border: 1px dashed gray;
|
||||
}
|
||||
|
||||
.textBox>.removeIcon, .textBox>.moveIcon {
|
||||
.textBox > .removeIcon,
|
||||
.textBox > .moveIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.textBox.active>.removeIcon, .textBox.active>.moveIcon {
|
||||
.textBox.active > .removeIcon,
|
||||
.textBox.active > .moveIcon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -95,4 +117,18 @@ button {
|
||||
border: 0px;
|
||||
min-width: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#displayWhiteboardInfoBtn.active {
|
||||
background: var(--selected-icon-bg-color);
|
||||
}
|
||||
|
||||
#whiteboardInfoContainer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.displayNone {
|
||||
display: none;
|
||||
}
|
||||
|
361
src/index.html
361
src/index.html
@ -1,129 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Whiteboard</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/vnd.microsoft.icon" href="favicon.ico" />
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<title>Whiteboard</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/vnd.microsoft.icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<!---Whiteboard container -!-->
|
||||
<div id="whiteboardContainer"></div>
|
||||
|
||||
<body>
|
||||
<!---Whiteboard container -!-->
|
||||
<div id="whiteboardContainer"></div>
|
||||
<!---Toolbar -!-->
|
||||
<div id="toolbar" style="position: absolute; top: 10px; left: 10px;">
|
||||
<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 id="toolbar" style="position: absolute; top: 10px; left: 10px;">
|
||||
<div class="btn-group">
|
||||
<button id="whiteboardTrashBtn" title="Clear the whiteboard" type="button" class="whiteboardBtn">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button style="position:absolute; left:0px; top:0px; width: 46px; display:none;"
|
||||
id="whiteboardTrashBtnConfirm" title="Confirm clear..." type="button" class="whiteboardBtn">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button id="whiteboardUndoBtn" title="Undo your last step" type="button" class="whiteboardBtn">
|
||||
<i class="fa fa-undo"></i>
|
||||
</button>
|
||||
<button id="whiteboardRedoBtn" title="Redo your last undo" type="button" class="whiteboardBtn">
|
||||
<i class="fa fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group whiteboard-edit-group">
|
||||
<button id="whiteboardTrashBtn" title="Clear the whiteboard" type="button">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button
|
||||
style="position: absolute; left: 0px; top: 0px; width: 46px; display: none;"
|
||||
id="whiteboardTrashBtnConfirm"
|
||||
title="Confirm clear..."
|
||||
type="button"
|
||||
>
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button id="whiteboardUndoBtn" title="Undo your last step" type="button">
|
||||
<i class="fa fa-undo"></i>
|
||||
</button>
|
||||
<button id="whiteboardRedoBtn" title="Redo your last undo" type="button">
|
||||
<i class="fa fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button tool="mouse" title="Take the mouse" type="button" class="whiteboardTool">
|
||||
<i class="fa fa-mouse-pointer"></i>
|
||||
</button>
|
||||
<button style="padding-bottom: 11px;" tool="recSelect" title="Select an area" type="button"
|
||||
class="whiteboardTool">
|
||||
<img src="./images/dottedRec.png">
|
||||
</button>
|
||||
<button tool="pen" title="Take the pen" type="button" class="whiteboardTool active">
|
||||
<i class="fa fa-pencil-alt"></i>
|
||||
</button>
|
||||
<button style="padding-bottom: 8px; padding-top: 6px;" tool="line" title="draw a line" type="button"
|
||||
class="whiteboardTool">
|
||||
╱
|
||||
</button>
|
||||
<button tool="rect" title="draw a rectangle" type="button" class="whiteboardTool">
|
||||
<i class="far fa-square"></i>
|
||||
</button>
|
||||
<button tool="circle" title="draw a circle" type="button" class="whiteboardTool">
|
||||
<i class="far fa-circle"></i>
|
||||
</button>
|
||||
<button tool="text" title="write text" type="button" class="whiteboardTool">
|
||||
<i class="fas fa-font"></i>
|
||||
</button>
|
||||
<button tool="eraser" title="take the eraser" type="button" class="whiteboardTool">
|
||||
<i class="fa fa-eraser"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group whiteboard-edit-group">
|
||||
<button tool="mouse" title="Take the mouse" type="button" class="whiteboard-tool">
|
||||
<i class="fa fa-mouse-pointer"></i>
|
||||
</button>
|
||||
<button
|
||||
style="padding-bottom: 11px;"
|
||||
tool="recSelect"
|
||||
title="Select an area"
|
||||
type="button"
|
||||
class="whiteboard-tool"
|
||||
>
|
||||
<img src="./images/dottedRec.png" />
|
||||
</button>
|
||||
<button
|
||||
tool="pen"
|
||||
title="Take the pen"
|
||||
type="button"
|
||||
class="whiteboard-tool active"
|
||||
>
|
||||
<i class="fa fa-pencil-alt"></i>
|
||||
</button>
|
||||
<button
|
||||
style="padding-bottom: 8px; padding-top: 6px;"
|
||||
tool="line"
|
||||
title="draw a line"
|
||||
type="button"
|
||||
class="whiteboard-tool"
|
||||
>
|
||||
╱
|
||||
</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">
|
||||
<button style="width: 190px; cursor:default;">
|
||||
<div class="activeToolIcon" style="position:absolute; top:2px; left:2px; font-size: 0.6em;"><i
|
||||
class="fa fa-pencil-alt"></i></div>
|
||||
<img style="position: absolute; left: 11px; top: 16px; height:14px; 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 whiteboard-edit-group">
|
||||
<button style="width: 190px; cursor: default;">
|
||||
<div
|
||||
class="activeToolIcon"
|
||||
style="position: absolute; top: 2px; left: 2px; font-size: 0.6em;"
|
||||
>
|
||||
<i class="fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<img
|
||||
style="
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 16px;
|
||||
height: 14px;
|
||||
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">
|
||||
<button id="saveAsImageBtn" title="Save whiteboard as image" type="button" class="whiteboardBtn">
|
||||
<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" class="whiteboardBtn">
|
||||
<div class="btn-group whiteboard-edit-group">
|
||||
<button id="addImgToCanvasBtn" title="Upload Image to whiteboard" 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-upload"
|
||||
></i>
|
||||
</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"
|
||||
class="whiteboardBtn">
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
style="position: relative;"
|
||||
id="uploadJsonBtn"
|
||||
title="Load saved JSON to whiteboard"
|
||||
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-upload"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="addImgToCanvasBtn" title="Upload Image to whiteboard" type="button" class="whiteboardBtn">
|
||||
<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>
|
||||
<input style="display: none;" id="myFile" type="file" />
|
||||
</div>
|
||||
|
||||
<button style="position: relative;" id="uploadJsonBtn" title="Load saved JSON to whiteboard" type="button"
|
||||
class="whiteboardBtn">
|
||||
<div class="btn-group">
|
||||
<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>
|
||||
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; "
|
||||
class="fas fa-upload"></i>
|
||||
</button>
|
||||
<input style="display:none;" id="myFile" type="file" />
|
||||
<button id="shareWhiteboardBtn" title="share whiteboard" type="button">
|
||||
<i class="fas fa-share-square"></i>
|
||||
</button>
|
||||
|
||||
<button id="shareWhiteboardBtn" title="share whiteboard" type="button">
|
||||
<i class="fas fa-share-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button id="displayWhiteboardInfoBtn" title="Show whiteboard info" type="button">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group minGroup">
|
||||
<button style="width: 25px; padding: 11px 11px;" id="minMaxBtn" title="hide buttons" type="button">
|
||||
<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>
|
||||
<div class="btn-group minGroup">
|
||||
<button
|
||||
style="width: 25px; padding: 11px 11px;"
|
||||
id="minMaxBtn"
|
||||
title="hide buttons"
|
||||
type="button"
|
||||
>
|
||||
<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>
|
||||
|
||||
</html>
|
||||
<div id="whiteboardInfoContainer">
|
||||
<p><b>Whiteboard information:</b></p>
|
||||
<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
79
src/js/classes/Point.js
Normal 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;
|
@ -16,6 +16,9 @@ import {
|
||||
faAngleRight,
|
||||
faSortDown,
|
||||
faExpandArrowsAlt,
|
||||
faLock,
|
||||
faLockOpen,
|
||||
faInfoCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faSquare,
|
||||
@ -46,7 +49,10 @@ library.add(
|
||||
faCircle,
|
||||
faFile,
|
||||
faFileAlt,
|
||||
faPlusSquare
|
||||
faPlusSquare,
|
||||
faLock,
|
||||
faLockOpen,
|
||||
faInfoCircle
|
||||
);
|
||||
|
||||
dom.i2svg()
|
||||
dom.i2svg();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "jquery-ui/ui/core";
|
||||
import "jquery-ui/ui/widgets/draggable";
|
||||
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 "../css/main.css";
|
||||
|
||||
@ -9,19 +9,19 @@ import "./icons";
|
||||
import main from "./main";
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
// Set correct width height on mobile browsers
|
||||
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
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 {
|
||||
$('head').append('<meta name="viewport" content="width=1400" />');
|
||||
$("head").append('<meta name="viewport" content="width=1400" />');
|
||||
}
|
||||
|
||||
main();
|
||||
});
|
||||
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept();
|
||||
}
|
||||
|
@ -1,48 +1,48 @@
|
||||
/* -----------
|
||||
KEYBINDINGS
|
||||
----------- */
|
||||
----------- */
|
||||
|
||||
//> 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 = {
|
||||
// 'key(s)' : 'function',
|
||||
'defmod-shift-z' : 'clearWhiteboard',
|
||||
'defmod-z' : 'undoStep',
|
||||
'defmod-y' : 'redoStep',
|
||||
'defmod-x' : 'setTool_recSelect',
|
||||
'defmod-m' : 'setTool_mouse',
|
||||
'defmod-p' : 'setTool_pen',
|
||||
'defmod-l' : 'setTool_line',
|
||||
'defmod-r' : 'setTool_rect',
|
||||
'defmod-c' : 'setTool_circle',
|
||||
'defmod-shift-f' : 'toggleLineRecCircle',
|
||||
'defmod-shift-x' : 'togglePenEraser',
|
||||
'defmod-shift-r' : 'toggleMainColors',
|
||||
'defmod-a' : 'setTool_text',
|
||||
'defmod-e' : 'setTool_eraser',
|
||||
'defmod-up' : 'thickness_bigger',
|
||||
'defmod-down' : 'thickness_smaller',
|
||||
'defmod-shift-c' : 'openColorPicker',
|
||||
'defmod-shift-1' : 'setDrawColorBlack',
|
||||
'defmod-shift-2' : 'setDrawColorBlue',
|
||||
'defmod-shift-3' : 'setDrawColorGreen',
|
||||
'defmod-shift-4' : 'setDrawColorYellow',
|
||||
'defmod-shift-5' : 'setDrawColorRed',
|
||||
'defmod-s' : 'saveWhiteboardAsImage',
|
||||
'defmod-shift-k' : 'saveWhiteboardAsJson',
|
||||
'defmod-shift-i' : 'uploadWhiteboardToWebDav',
|
||||
'defmod-shift-j' : 'uploadJsonToWhiteboard',
|
||||
'defmod-shift-s' : 'shareWhiteboard',
|
||||
'tab' : 'hideShowControls',
|
||||
'up' : 'moveDraggableUp',
|
||||
'down' : 'moveDraggableDown',
|
||||
'left' : 'moveDraggableLeft',
|
||||
'right' : 'moveDraggableRight',
|
||||
'defmod-enter' : 'dropDraggable',
|
||||
'shift-enter' : 'addToBackground',
|
||||
'escape' : 'cancelAllActions',
|
||||
'del' : 'deleteSelection'
|
||||
}
|
||||
"defmod-shift-z": "clearWhiteboard",
|
||||
"defmod-z": "undoStep",
|
||||
"defmod-y": "redoStep",
|
||||
"defmod-x": "setTool_recSelect",
|
||||
"defmod-m": "setTool_mouse",
|
||||
"defmod-p": "setTool_pen",
|
||||
"defmod-l": "setTool_line",
|
||||
"defmod-r": "setTool_rect",
|
||||
"defmod-c": "setTool_circle",
|
||||
"defmod-shift-f": "toggleLineRecCircle",
|
||||
"defmod-shift-x": "togglePenEraser",
|
||||
"defmod-shift-r": "toggleMainColors",
|
||||
"defmod-a": "setTool_text",
|
||||
"defmod-e": "setTool_eraser",
|
||||
"defmod-up": "thickness_bigger",
|
||||
"defmod-down": "thickness_smaller",
|
||||
"defmod-shift-c": "openColorPicker",
|
||||
"defmod-shift-1": "setDrawColorBlack",
|
||||
"defmod-shift-2": "setDrawColorBlue",
|
||||
"defmod-shift-3": "setDrawColorGreen",
|
||||
"defmod-shift-4": "setDrawColorYellow",
|
||||
"defmod-shift-5": "setDrawColorRed",
|
||||
"defmod-s": "saveWhiteboardAsImage",
|
||||
"defmod-shift-k": "saveWhiteboardAsJson",
|
||||
"defmod-shift-i": "uploadWhiteboardToWebDav",
|
||||
"defmod-shift-j": "uploadJsonToWhiteboard",
|
||||
"defmod-shift-s": "shareWhiteboard",
|
||||
tab: "hideShowControls",
|
||||
up: "moveDraggableUp",
|
||||
down: "moveDraggableDown",
|
||||
left: "moveDraggableLeft",
|
||||
right: "moveDraggableRight",
|
||||
"defmod-enter": "dropDraggable",
|
||||
"shift-enter": "addToBackground",
|
||||
escape: "cancelAllActions",
|
||||
del: "deleteSelection",
|
||||
};
|
||||
|
||||
export default keybinds;
|
||||
export default keybinds;
|
||||
|
868
src/js/main.js
868
src/js/main.js
File diff suppressed because it is too large
Load Diff
118
src/js/services/ConfigService.js
Normal file
118
src/js/services/ConfigService.js
Normal 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();
|
21
src/js/services/ConfigService.utils.js
Normal file
21
src/js/services/ConfigService.utils.js
Normal 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) };
|
||||
}
|
29
src/js/services/ConfigService.utils.test.js
Normal file
29
src/js/services/ConfigService.utils.test.js
Normal 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);
|
||||
});
|
137
src/js/services/InfoService.js
Normal file
137
src/js/services/InfoService.js
Normal 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();
|
57
src/js/services/ReadOnlyService.js
Normal file
57
src/js/services/ReadOnlyService.js
Normal 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();
|
48
src/js/services/ThrottlingService.js
Normal file
48
src/js/services/ThrottlingService.js
Normal 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
147
src/js/shortcutFunctions.js
Normal 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
44
src/js/utils.js
Normal 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
Loading…
Reference in New Issue
Block a user