Merge branch 'master' into master

This commit is contained in:
Adam Macumber 2023-11-27 09:52:48 -05:00 committed by GitHub
commit 05d0ea2d2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 3721 additions and 3246 deletions

View file

@ -5,13 +5,12 @@ name: Node.js CI
on: on:
push: push:
branches: [ master ] branches: [master]
pull_request: pull_request:
branches: [ master ] branches: [master]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -19,13 +18,26 @@ jobs:
node-version: [16.x, 18.x, 20.x] node-version: [16.x, 18.x, 20.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm ci - run: npm ci
- run: npm run build --if-present - run: npm run build --if-present
- run: npm test - run: npm test
env: env:
CI: true CI: true
format:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Run Prettier check
run: npx prettier --check .

View file

@ -1,7 +1,7 @@
name: deploy name: deploy
on: on:
push: push:
tags: tags:
- v* - v*
jobs: jobs:
@ -9,15 +9,15 @@ jobs:
name: deploy name: deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Dokku - name: Dokku
uses: dokku/github-action@bbb8818f9bad88edd3099a2399ef9be366754ff9 uses: dokku/github-action@bbb8818f9bad88edd3099a2399ef9be366754ff9
with: with:
git_remote_url: ssh://dokku@${{ secrets.SSH_SERVER }}:22/wbo git_remote_url: ssh://dokku@${{ secrets.SSH_SERVER }}:22/wbo
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
git_push_flags: --force git_push_flags: --force
push_to_registry: push_to_registry:
name: Push Docker image to Docker Hub name: Push Docker image to Docker Hub
runs-on: ubuntu-latest runs-on: ubuntu-latest

5
.prettierignore Normal file
View file

@ -0,0 +1,5 @@
package.json
package-lock.json
node_modules/
.DS_Store
*.html

12
.prettierrc Normal file
View file

@ -0,0 +1,12 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"endOfLine": "lf",
"insertPragma": false,
"proseWrap": "preserve",
"requirePragma": false,
"tabWidth": 2,
"useTabs": false,
"printWidth": 80
}

View file

@ -27,7 +27,7 @@ If you have your own web server, and want to run a private instance of WBO on it
### Running the code in a container (safer) ### Running the code in a container (safer)
If you use the [docker](https://www.docker.com/) containerization service, you can easily run WBO as a container. If you use the [docker](https://www.docker.com/) containerization service, you can easily run WBO as a container.
An official docker image for WBO is hosted on dockerhub as [`lovasoa/wbo`](https://hub.docker.com/r/lovasoa/wbo): [![WBO 1M docker pulls](https://img.shields.io/docker/pulls/lovasoa/wbo?style=flat)](https://hub.docker.com/repository/docker/lovasoa/wbo). An official docker image for WBO is hosted on dockerhub as [`lovasoa/wbo`](https://hub.docker.com/r/lovasoa/wbo): [![WBO 1M docker pulls](https://img.shields.io/docker/pulls/lovasoa/wbo?style=flat)](https://hub.docker.com/repository/docker/lovasoa/wbo).
You can run the following bash command to launch WBO on port 5001, while persisting the boards outside of docker: You can run the following bash command to launch WBO on port 5001, while persisting the boards outside of docker:
@ -45,6 +45,7 @@ You can then access WBO at `http://localhost:5001`.
Alternatively, you can run the code with [node.js](https://nodejs.org/) directly, without docker. Alternatively, you can run the code with [node.js](https://nodejs.org/) directly, without docker.
First, download the sources: First, download the sources:
``` ```
git clone https://github.com/lovasoa/whitebophir.git git clone https://github.com/lovasoa/whitebophir.git
cd whitebophir cd whitebophir
@ -58,14 +59,17 @@ npm install --production
``` ```
Finally, you can start the server: Finally, you can start the server:
``` ```
PORT=5001 npm start PORT=5001 npm start
``` ```
This will run WBO directly on your machine, on port 5001, without any isolation from the other services. You can also use an invokation like This will run WBO directly on your machine, on port 5001, without any isolation from the other services. You can also use an invokation like
``` ```
PORT=5001 HOST=127.0.0.1 npm start PORT=5001 HOST=127.0.0.1 npm start
``` ```
to make whitebophir only listen on the loopback device. This is useful if you want to put whitebophir behind a reverse proxy. to make whitebophir only listen on the loopback device. This is useful if you want to put whitebophir behind a reverse proxy.
### Running WBO on a subfolder ### Running WBO on a subfolder
@ -76,7 +80,7 @@ See instructions on our Wiki about [how to setup a reverse proxy for WBO](https:
## Translations ## Translations
WBO is available in multiple languages. The translations are stored in [`server/translations.json`](./server/translations.json). WBO is available in multiple languages. The translations are stored in [`server/translations.json`](./server/translations.json).
If you feel like contributing to this collaborative project, you can [translate WBO into your own language](https://github.com/lovasoa/whitebophir/wiki/How-to-translate-WBO-into-your-own-language). If you feel like contributing to this collaborative project, you can [translate WBO into your own language](https://github.com/lovasoa/whitebophir/wiki/How-to-translate-WBO-into-your-own-language).
## Authentication ## Authentication
@ -86,10 +90,10 @@ WBO supports authentication using [Json Web Tokens](https://jwt.io/introduction)
The `AUTH_SECRET_KEY` variable in [`configuration.js`](./server/configuration.js) should be filled with the secret key for the JWT. The `AUTH_SECRET_KEY` variable in [`configuration.js`](./server/configuration.js) should be filled with the secret key for the JWT.
Within the payload, you can declare the user's roles as an array. Within the payload, you can declare the user's roles as an array.
Currently the only accepted roles are `moderator` and `editor`. Currently the only accepted roles are `moderator` and `editor`.
- `moderator` will give the user an additional tool to wipe all data from the board. To declare this role, see the example below. - `moderator` will give the user an additional tool to wipe all data from the board. To declare this role, see the example below.
- `editor` will give the user the ability to edit the board. This is the default role for all users. - `editor` will give the user the ability to edit the board. This is the default role for all users.
```json ```json
{ {
@ -98,6 +102,7 @@ Currently the only accepted roles are `moderator` and `editor`.
"roles": ["moderator"] "roles": ["moderator"]
} }
``` ```
Moderators have access to the Clear tool, which will wipe all content from the board. Moderators have access to the Clear tool, which will wipe all content from the board.
## Board name verification in the JWT ## Board name verification in the JWT
@ -108,9 +113,15 @@ To check for a valid board name just add the board name to the role with a ":".
```json ```json
{ {
"roles": ["moderator:<boardName1>","moderator:<boardName2>","editor:<boardName3>","editor:<boardName4>"] "roles": [
"moderator:<boardName1>",
"moderator:<boardName2>",
"editor:<boardName3>",
"editor:<boardName4>"
]
} }
``` ```
eg, `http://myboard.com/boards/mySecretBoardName?token={token}` eg, `http://myboard.com/boards/mySecretBoardName?token={token}`
```json ```json
@ -128,9 +139,10 @@ You can now be sure that only users who have the correct token have access to th
When you start a WBO server, it loads its configuration from several environment variables. When you start a WBO server, it loads its configuration from several environment variables.
You can see a list of these variables in [`configuration.js`](./server/configuration.js). You can see a list of these variables in [`configuration.js`](./server/configuration.js).
Some important environment variables are : Some important environment variables are :
- `WBO_HISTORY_DIR` : configures the directory where the boards are saved. Defaults to `./server-data/`.
- `WBO_MAX_EMIT_COUNT` : the maximum number of messages that a client can send per unit of time. Increase this value if you want smoother drawings, at the expense of being susceptible to denial of service attacks if your server does not have enough processing power. By default, the units of this quantity are messages per 4 seconds, and the default value is `192`. - `WBO_HISTORY_DIR` : configures the directory where the boards are saved. Defaults to `./server-data/`.
- `AUTH_SECRET_KEY` : If you would like to authenticate your boards using jwt, this declares the secret key. - `WBO_MAX_EMIT_COUNT` : the maximum number of messages that a client can send per unit of time. Increase this value if you want smoother drawings, at the expense of being susceptible to denial of service attacks if your server does not have enough processing power. By default, the units of this quantity are messages per 4 seconds, and the default value is `192`.
- `AUTH_SECRET_KEY` : If you would like to authenticate your boards using jwt, this declares the secret key.
## Troubleshooting ## Troubleshooting
@ -146,5 +158,5 @@ metrics collection agent.
Example: `docker run -e STATSD_URL=udp://127.0.0.1:8125 lovasoa/wbo`. Example: `docker run -e STATSD_URL=udp://127.0.0.1:8125 lovasoa/wbo`.
- If you use **prometheus**, you can collect the metrics with [statsd-exporter](https://hub.docker.com/r/prom/statsd-exporter). - If you use **prometheus**, you can collect the metrics with [statsd-exporter](https://hub.docker.com/r/prom/statsd-exporter).
- If you use **datadog**, you can collect the metrics with [dogstatsd](https://docs.datadoghq.com/developers/dogstatsd). - If you use **datadog**, you can collect the metrics with [dogstatsd](https://docs.datadoghq.com/developers/dogstatsd).

View file

@ -1,286 +1,289 @@
html, body, svg { html,
padding:0; body,
margin:0; svg {
font-family: Liberation sans, sans-serif; padding: 0;
margin: 0;
font-family:
Liberation sans,
sans-serif;
} }
#canvas { #canvas {
transform-origin: 0 0; transform-origin: 0 0;
} }
#loadingMessage { #loadingMessage {
font-size: 1.5em; font-size: 1.5em;
background: #eee linear-gradient(#eeeeee, #cccccc); background: #eee linear-gradient(#eeeeee, #cccccc);
padding: 20px; padding: 20px;
width: 40%; width: 40%;
line-height: 50px; line-height: 50px;
text-align: center; text-align: center;
border-radius: 10px; border-radius: 10px;
position:fixed; position: fixed;
top: 40%; top: 40%;
left: 30%; left: 30%;
z-index: 1; z-index: 1;
box-shadow: 0 0 2px #333333; box-shadow: 0 0 2px #333333;
transition: 1s; transition: 1s;
} }
#loadingMessage.hidden { #loadingMessage.hidden {
display: none; display: none;
opacity: 0; opacity: 0;
z-index: -1; z-index: -1;
} }
#loadingMessage::after { #loadingMessage::after {
content: "..."; content: "...";
} }
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */
#menu::-webkit-scrollbar { #menu::-webkit-scrollbar {
display: none; display: none;
} }
#menu { #menu {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
font-size: 16px; font-size: 16px;
border-radius: 0; border-radius: 0;
overflow-y: scroll; overflow-y: scroll;
position: fixed; position: fixed;
margin-bottom: 30px; margin-bottom: 30px;
left: 0; left: 0;
top: 0; top: 0;
color: black; color: black;
max-height: 100%; max-height: 100%;
transition-duration: 1s; transition-duration: 1s;
cursor: default; cursor: default;
padding: 10px; padding: 10px;
} }
#menu.closed { #menu.closed {
border-radius:3px; border-radius: 3px;
left:10px; left: 10px;
top:10px; top: 10px;
background-color:rgba(100,200,255,0.7); background-color: rgba(100, 200, 255, 0.7);
width:6vw; width: 6vw;
height:2em; height: 2em;
transition-duration:1s; transition-duration: 1s;
} }
#menu h2{ /*Menu title ("Menu")*/ #menu h2 {
display: none; /*Menu title ("Menu")*/
font-size:4vh; display: none;
text-align: center; font-size: 4vh;
letter-spacing:.5vw; text-align: center;
text-shadow: 0px 0px 5px white; letter-spacing: 0.5vw;
color:black; text-shadow: 0px 0px 5px white;
padding:0; color: black;
margin:0; padding: 0;
margin: 0;
} }
#menu .tools { #menu .tools {
list-style-type:none; list-style-type: none;
padding:0; padding: 0;
} }
#settings { #settings {
margin-bottom: 20px; margin-bottom: 20px;
} }
#menu .tool { #menu .tool {
position: relative; position: relative;
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */ -webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */ -khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */ -moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */ -ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently user-select: none; /* Non-prefixed version, currently
supported by Chrome, Opera and Firefox */ supported by Chrome, Opera and Firefox */
pointer-events: auto; pointer-events: auto;
white-space: nowrap; white-space: nowrap;
list-style-position:inside; list-style-position: inside;
border:1px solid #eeeeee; border: 1px solid #eeeeee;
text-decoration:none; text-decoration: none;
cursor:pointer; cursor: pointer;
background: #ffffff; background: #ffffff;
margin-top: 10px; margin-top: 10px;
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
border-radius: 0px; border-radius: 0px;
max-width: 40px; max-width: 40px;
transition-duration: .2s; transition-duration: 0.2s;
overflow: hidden; overflow: hidden;
width: max-content; width: max-content;
box-shadow: inset 0 0 3px #8FA2BC; box-shadow: inset 0 0 3px #8fa2bc;
} }
#menu .tool:hover { #menu .tool:hover {
max-width: 100%; max-width: 100%;
} }
@media (hover: none), (pointer: coarse) { @media (hover: none), (pointer: coarse) {
#menu .tool:hover { #menu .tool:hover {
max-width: 40px; max-width: 40px;
} }
#menu .tool:focus { #menu .tool:focus {
max-width: 100%; max-width: 100%;
} }
#menu { #menu {
pointer-events: auto; pointer-events: auto;
} }
#menu:focus-within {
pointer-events: none;
}
#menu:focus-within {
pointer-events: none;
}
} }
#menu .oneTouch:active { #menu .oneTouch:active {
border-radius: 3px; border-radius: 3px;
background-color:#eeeeff; background-color: #eeeeff;
} }
#menu .tool:active { #menu .tool:active {
box-shadow: inset 0 0 1px #ddeeff; box-shadow: inset 0 0 1px #ddeeff;
background-color:#eeeeff; background-color: #eeeeff;
} }
#menu .tool.curTool { #menu .tool.curTool {
box-shadow: 0 0 5px #0074D9; box-shadow: 0 0 5px #0074d9;
background: linear-gradient(#96E1FF, #36A2FF); background: linear-gradient(#96e1ff, #36a2ff);
} }
#menu .tool-icon { #menu .tool-icon {
display: inline-block; display: inline-block;
text-align:center; text-align: center;
width: 35px; width: 35px;
height: 35px; height: 35px;
margin: 2.5px; margin: 2.5px;
font-family: mono, monospace; font-family: mono, monospace;
overflow: hidden; overflow: hidden;
} }
#menu img.tool-icon { #menu img.tool-icon {
pointer-events: none; pointer-events: none;
} }
#menu .tool-icon > * { #menu .tool-icon > * {
display: block; display: block;
margin: auto; margin: auto;
} }
#menu .tool-name { #menu .tool-name {
text-align: center; text-align: center;
font-size: 23px; font-size: 23px;
margin-right: 20px; margin-right: 20px;
margin-left: 20px; margin-left: 20px;
margin-bottom: 2.5px; margin-bottom: 2.5px;
display: inline-block; display: inline-block;
vertical-align: text-bottom; vertical-align: text-bottom;
} }
#menu .tool-name.slider { #menu .tool-name.slider {
display: inline-block; display: inline-block;
width: 150px; width: 150px;
height: 30px; height: 30px;
font-size: .9em; font-size: 0.9em;
line-height: 15px; line-height: 15px;
vertical-align: top; vertical-align: top;
padding: 6px; padding: 6px;
} }
#menu .tool.hasSecondary .tool-icon{ #menu .tool.hasSecondary .tool-icon {
margin-top:0px; margin-top: 0px;
margin-left:0px; margin-left: 0px;
} }
#menu .tool .tool-icon.secondaryIcon{ #menu .tool .tool-icon.secondaryIcon {
display: none; display: none;
} }
#menu .tool.hasSecondary .tool-icon.secondaryIcon{ #menu .tool.hasSecondary .tool-icon.secondaryIcon {
display: block; display: block;
position: absolute; position: absolute;
bottom: 0px; bottom: 0px;
left: 26px; left: 26px;
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
input { input {
font-size:16px; font-size: 16px;
} }
#chooseColor { #chooseColor {
width: 100%; width: 100%;
height:100%; height: 100%;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
color:black; color: black;
display: block; display: block;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.colorPresets { .colorPresets {
margin-right: 20px; margin-right: 20px;
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
} }
.colorPresetButton { .colorPresetButton {
width: 30px; width: 30px;
height: 30px; height: 30px;
border: 1px solid black; border: 1px solid black;
border-radius: 3px; border-radius: 3px;
display: inline-block; display: inline-block;
margin-right: 6px; margin-right: 6px;
padding: 0; padding: 0;
vertical-align: middle; vertical-align: middle;
} }
.rangeChooser { .rangeChooser {
display: block; display: block;
border: 0; border: 0;
width: 100%; width: 100%;
margin: 0; margin: 0;
background: transparent; background: transparent;
} }
line { line {
fill: none; fill: none;
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
} }
path { path {
fill: none; fill: none;
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
} }
text { text {
font-family:"Arial", "Helvetica", sans-serif; font-family: "Arial", "Helvetica", sans-serif;
user-select:none; user-select: none;
-moz-user-select:none; -moz-user-select: none;
} }
circle.opcursor { circle.opcursor {
pointer-events: none; pointer-events: none;
transition: .1s; transition: 0.1s;
} }
#cursor-me { #cursor-me {
transition: 0s; transition: 0s;
} }
/* Internet Explorer specific CSS */ /* Internet Explorer specific CSS */
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
#chooseColor { #chooseColor {
color: transparent; color: transparent;
} }
label.tool-name[for=chooseColor] { label.tool-name[for="chooseColor"] {
line-height: 10px; line-height: 10px;
} }
} }

View file

@ -234,7 +234,10 @@ footer a:hover {
width: 300px; width: 300px;
box-shadow: -150px 0 150px 150px black; box-shadow: -150px 0 150px 150px black;
background: black; background: black;
transition: width 0.5s, box-shadow 0.3s ease 0.2s, background-color 0.5s; transition:
width 0.5s,
box-shadow 0.3s ease 0.2s,
background-color 0.5s;
} }
.lang-selector ul { .lang-selector ul {
@ -253,7 +256,10 @@ footer a:hover {
text-align: right; text-align: right;
text-transform: uppercase; text-transform: uppercase;
list-style: none; list-style: none;
transition: box-shadow 0.3s, width 0.5s, background-color 0.5s ease 0.3s; transition:
box-shadow 0.3s,
width 0.5s,
background-color 0.5s ease 0.3s;
} }
.lang-selector li a { .lang-selector li a {

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,6 @@
* @licend * @licend
*/ */
/*jshint bitwise:false*/ /*jshint bitwise:false*/
// ==ClosureCompiler== // ==ClosureCompiler==
@ -35,208 +34,246 @@
// @use_types_for_optimization true // @use_types_for_optimization true
// ==/ClosureCompiler== // ==/ClosureCompiler==
var canvascolor = (function() {//Code Isolation var canvascolor = (function () {
"use strict"; //Code Isolation
"use strict";
(function addCSS () { (function addCSS() {
var styleTag = document.createElement("style"); var styleTag = document.createElement("style");
styleTag.innerHTML = [".canvascolor-container{", styleTag.innerHTML = [
"background-color:black;", ".canvascolor-container{",
"border-radius:5px;", "background-color:black;",
"overflow:hidden;", "border-radius:5px;",
"width:179px;", "overflow:hidden;",
"padding:2px;", "width:179px;",
"display:none;", "padding:2px;",
"}", "display:none;",
".canvascolor-container canvas{", "}",
"cursor:crosshair;", ".canvascolor-container canvas{",
"}", "cursor:crosshair;",
".canvascolor-history{", "}",
"overflow:auto;", ".canvascolor-history{",
"}", "overflow:auto;",
".canvascolor-history > div{", "}",
"margin:2px;", ".canvascolor-history > div{",
"display:inline-block;", "margin:2px;",
"}"].join(""); "display:inline-block;",
document.head.appendChild(styleTag); "}",
})(); ].join("");
document.head.appendChild(styleTag);
})();
function hsv2rgb (h,s,v) { function hsv2rgb(h, s, v) {
if( s === 0 ) return [v,v,v]; // achromatic (grey) if (s === 0) return [v, v, v]; // achromatic (grey)
h /= (Math.PI/6); // sector 0 to 5 h /= Math.PI / 6; // sector 0 to 5
var i = h|0, var i = h | 0,
f = h - i, // factorial part of h f = h - i, // factorial part of h
p = v * ( 1 - s ), p = v * (1 - s),
q = v * ( 1 - s * f ), q = v * (1 - s * f),
t = v * ( 1 - s * ( 1 - f ) ); t = v * (1 - s * (1 - f));
switch( i%6 ) { switch (i % 6) {
case 0: return [v,t,p]; case 0:
case 1: return [q,v,p]; return [v, t, p];
case 2: return [p,v,t]; case 1:
case 3: return [p,q,v]; return [q, v, p];
case 4: return [t,p,v]; case 2:
case 5:return [v,p,q]; return [p, v, t];
} case 3:
return [p, q, v];
case 4:
return [t, p, v];
case 5:
return [v, p, q];
}
}
function isFixedPosition(elem) {
do {
if (getComputedStyle(elem).position === "fixed") return true;
} while ((elem = elem.parentElement) !== null);
return false;
}
var containerTemplate;
(function createContainer() {
containerTemplate = document.createElement("div");
containerTemplate.className = "canvascolor-container";
var canvas = document.createElement("canvas");
var historyDiv = document.createElement("div");
historyDiv.className = "canvascolor-history";
containerTemplate.appendChild(canvas);
containerTemplate.appendChild(historyDiv);
})();
function canvascolor(elem) {
var curcolor = elem.value || "#000";
var w = 200,
h = w / 2;
var container = containerTemplate.cloneNode(true);
container.style.width = w + "px";
container.style.position = isFixedPosition(elem) ? "fixed" : "absolute";
var canvas = container.getElementsByTagName("canvas")[0];
var ctx = canvas.getContext("2d");
canvas.width = w;
canvas.height = h;
var prevcolorsDiv = container.getElementsByClassName(
"canvascolor-history",
)[0];
prevcolorsDiv.style.width = w + "px";
prevcolorsDiv.style.maxHeight = h + "px";
var previewdiv = createColorDiv(curcolor);
previewdiv.style.border = "1px solid white";
previewdiv.style.borderRadius = "5px";
document.body.appendChild(container);
function displayContainer() {
var rect = elem.getBoundingClientRect();
var conttop = rect.top + rect.height + 3,
contleft = rect.left;
if (container.style.position !== "fixed") {
conttop += document.documentElement.scrollTop;
contleft += document.documentElement.scrollLeft;
}
container.style.top = conttop + "px";
container.style.left = contleft + "px";
container.style.display = "block";
}
function hideContainer() {
container.style.display = "none";
} }
function isFixedPosition(elem) { elem.addEventListener("mouseover", displayContainer, true);
do { container.addEventListener("mouseleave", hideContainer, false);
if (getComputedStyle(elem).position === "fixed") return true; elem.addEventListener(
} while ( (elem = elem.parentElement) !== null ); "keyup",
return false; function () {
}
var containerTemplate;
(function createContainer(){
containerTemplate = document.createElement("div");
containerTemplate.className = "canvascolor-container";
var canvas = document.createElement("canvas");
var historyDiv = document.createElement("div");
historyDiv.className = "canvascolor-history";
containerTemplate.appendChild(canvas);
containerTemplate.appendChild(historyDiv);
})();
function canvascolor(elem) {
var curcolor = elem.value || "#000";
var w=200, h=w/2;
var container = containerTemplate.cloneNode(true);
container.style.width = w+"px";
container.style.position = isFixedPosition(elem) ? "fixed" : "absolute";
var canvas = container.getElementsByTagName("canvas")[0];
var ctx = canvas.getContext("2d");
canvas.width = w; canvas.height=h;
var prevcolorsDiv = container.getElementsByClassName("canvascolor-history")[0];
prevcolorsDiv.style.width=w+"px";
prevcolorsDiv.style.maxHeight=h+"px";
var previewdiv = createColorDiv(curcolor);
previewdiv.style.border = "1px solid white";
previewdiv.style.borderRadius = "5px";
document.body.appendChild(container);
function displayContainer(){
var rect = elem.getBoundingClientRect();
var conttop=(rect.top+rect.height+3),
contleft=rect.left;
if (container.style.position !== "fixed") {
conttop += document.documentElement.scrollTop;
contleft += document.documentElement.scrollLeft;
}
container.style.top = conttop+"px";
container.style.left = contleft+"px";
container.style.display = "block";
}
function hideContainer(){
container.style.display = "none";
}
elem.addEventListener("mouseover", displayContainer, true);
container.addEventListener("mouseleave", hideContainer, false);
elem.addEventListener("keyup", function(){
changeColor(elem.value, true);
}, true);
changeColor(elem.value, true); changeColor(elem.value, true);
},
true,
);
var idata = ctx.createImageData(w,h); changeColor(elem.value, true);
function rgb2hex (rgb) { var idata = ctx.createImageData(w, h);
function num2hex (c) {return (c*15/255|0).toString(16);}
return "#"+num2hex(rgb[0])+num2hex(rgb[1])+num2hex(rgb[2]);
}
function colorAt(coords) { function rgb2hex(rgb) {
var x=coords[0], y=coords[1]; function num2hex(c) {
return hsv2rgb(x/w*Math.PI, 1, (1-y/h)*255); return (((c * 15) / 255) | 0).toString(16);
} }
return "#" + num2hex(rgb[0]) + num2hex(rgb[1]) + num2hex(rgb[2]);
function render() {
for (var x=0; x<w; x++) {
for (var y=0;y<h; y++) {
var i = 4*(x+y*w);
var rgb = colorAt([x,y]);
idata.data[i] = rgb[0];//Red
idata.data[i+1] = rgb[1];//Green
idata.data[i+2] = rgb[2];//Blue
idata.data[i+3] = 255;
}
}
ctx.putImageData(idata,0,0);
}
render();
/** Changes the current color (the value of the input field) and updates other variables accordingly
* @param {string} color The new color. Must be a valid CSS color string if ensureValid is not specified
* @param {boolean} [ensureValid=false] Do not make the change if color is not a valid CSS color
*/
function changeColor(color, ensureValid) {
elem.style.backgroundColor = color;
if (ensureValid && elem.style.backgroundColor.length === 0) {
elem.style.backgroundColor = curcolor;
return;
}
previewdiv.style.backgroundColor = color;
curcolor = color;
elem.value = color;
elem.focus();
}
function createColorDiv (color) {
var div = document.createElement("div");
div.style.width = (w/3-10)+"px";
div.style.height = (h/3-8)+"px";
div.style.backgroundColor = color;
div.addEventListener("click", function(){
changeColor(color);
}, true);
if (prevcolorsDiv.childElementCount <= 1) prevcolorsDiv.appendChild(div);
else prevcolorsDiv.insertBefore(div,prevcolorsDiv.children[1]);
return div;
}
function canvasPos(evt) {
var canvasrect = canvas.getBoundingClientRect();
return [evt.clientX - canvasrect.left, evt.clientY - canvasrect.top];
}
canvas.addEventListener("mousemove", function(evt){
var coords = canvasPos(evt);
previewdiv.style.backgroundColor = rgb2hex(colorAt(coords));
}, true);
canvas.addEventListener("click", function(evt){
var coords = canvasPos(evt);
var color = rgb2hex(colorAt(coords));
createColorDiv(color);
changeColor(color);
}, true);
canvas.addEventListener("mouseleave", function(){
previewdiv.style.backgroundColor = curcolor;
}, true);
} }
function colorAt(coords) {
//Put a color picker on every input[type=color] if the browser doesn't support this input type var x = coords[0],
//and on every input with the class canvascolor y = coords[1];
var pickers = document.querySelectorAll("input.canvascolor, input[type=color]"); return hsv2rgb((x / w) * Math.PI, 1, (1 - y / h) * 255);
for (var i=0;i <pickers.length; i++) {
var input = pickers.item(i);
//If the browser supports native color picker and the user didn't
//explicitly added canvascolor to the element, we do not add a custom color picker
if (input.type !== "color" ||
input.className.split(" ").indexOf("canvascolor") !== -1) {
canvascolor(input);
}
} }
return canvascolor; function render() {
}()); for (var x = 0; x < w; x++) {
for (var y = 0; y < h; y++) {
var i = 4 * (x + y * w);
var rgb = colorAt([x, y]);
idata.data[i] = rgb[0]; //Red
idata.data[i + 1] = rgb[1]; //Green
idata.data[i + 2] = rgb[2]; //Blue
idata.data[i + 3] = 255;
}
}
ctx.putImageData(idata, 0, 0);
}
render();
/** Changes the current color (the value of the input field) and updates other variables accordingly
* @param {string} color The new color. Must be a valid CSS color string if ensureValid is not specified
* @param {boolean} [ensureValid=false] Do not make the change if color is not a valid CSS color
*/
function changeColor(color, ensureValid) {
elem.style.backgroundColor = color;
if (ensureValid && elem.style.backgroundColor.length === 0) {
elem.style.backgroundColor = curcolor;
return;
}
previewdiv.style.backgroundColor = color;
curcolor = color;
elem.value = color;
elem.focus();
}
function createColorDiv(color) {
var div = document.createElement("div");
div.style.width = w / 3 - 10 + "px";
div.style.height = h / 3 - 8 + "px";
div.style.backgroundColor = color;
div.addEventListener(
"click",
function () {
changeColor(color);
},
true,
);
if (prevcolorsDiv.childElementCount <= 1) prevcolorsDiv.appendChild(div);
else prevcolorsDiv.insertBefore(div, prevcolorsDiv.children[1]);
return div;
}
function canvasPos(evt) {
var canvasrect = canvas.getBoundingClientRect();
return [evt.clientX - canvasrect.left, evt.clientY - canvasrect.top];
}
canvas.addEventListener(
"mousemove",
function (evt) {
var coords = canvasPos(evt);
previewdiv.style.backgroundColor = rgb2hex(colorAt(coords));
},
true,
);
canvas.addEventListener(
"click",
function (evt) {
var coords = canvasPos(evt);
var color = rgb2hex(colorAt(coords));
createColorDiv(color);
changeColor(color);
},
true,
);
canvas.addEventListener(
"mouseleave",
function () {
previewdiv.style.backgroundColor = curcolor;
},
true,
);
}
//Put a color picker on every input[type=color] if the browser doesn't support this input type
//and on every input with the class canvascolor
var pickers = document.querySelectorAll(
"input.canvascolor, input[type=color]",
);
for (var i = 0; i < pickers.length; i++) {
var input = pickers.item(i);
//If the browser supports native color picker and the user didn't
//explicitly added canvascolor to the element, we do not add a custom color picker
if (
input.type !== "color" ||
input.className.split(" ").indexOf("canvascolor") !== -1
) {
canvascolor(input);
}
}
return canvascolor;
})();

View file

@ -9,7 +9,7 @@ function showRecentBoards() {
var list = document.createElement("ul"); var list = document.createElement("ul");
recentBoards.forEach(function(name) { recentBoards.forEach(function (name) {
var listItem = document.createElement("li"); var listItem = document.createElement("li");
var link = document.createElement("a"); var link = document.createElement("a");
link.setAttribute("href", `/boards/${encodeURIComponent(name)}`); link.setAttribute("href", `/boards/${encodeURIComponent(name)}`);
@ -22,4 +22,4 @@ function showRecentBoards() {
parent.classList.remove("hidden"); parent.classList.remove("hidden");
} }
window.addEventListener("pageshow", showRecentBoards); window.addEventListener("pageshow", showRecentBoards);

View file

@ -1,7 +1,7 @@
/** /**
* INTERSEC * INTERSEC
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2021 Ophir LOJKINE * Copyright (C) 2021 Ophir LOJKINE
@ -24,101 +24,100 @@
* @licend * @licend
*/ */
if (!SVGGraphicsElement.prototype.transformedBBox || !SVGGraphicsElement.prototype.transformedBBoxContains) { if (
[pointInTransformedBBox, !SVGGraphicsElement.prototype.transformedBBox ||
transformedBBoxIntersects] = (function () { !SVGGraphicsElement.prototype.transformedBBoxContains
) {
[pointInTransformedBBox, transformedBBoxIntersects] = (function () {
var get_transform_matrix = function (elem) {
// Returns the first translate or transform matrix or makes one
var transform = null;
for (var i = 0; i < elem.transform.baseVal.numberOfItems; ++i) {
var baseVal = elem.transform.baseVal[i];
// quick tests showed that even if one changes only the fields e and f or uses createSVGTransformFromMatrix
// the brower may add a SVG_TRANSFORM_MATRIX instead of a SVG_TRANSFORM_TRANSLATE
if (baseVal.type === SVGTransform.SVG_TRANSFORM_MATRIX) {
transform = baseVal;
break;
}
}
if (transform == null) {
transform = elem.transform.baseVal.createSVGTransformFromMatrix(
Tools.svg.createSVGMatrix(),
);
elem.transform.baseVal.appendItem(transform);
}
return transform.matrix;
};
var get_transform_matrix = function (elem) { var transformRelative = function (m, t) {
// Returns the first translate or transform matrix or makes one return [m.a * t[0] + m.c * t[1], m.b * t[0] + m.d * t[1]];
var transform = null; };
for (var i = 0; i < elem.transform.baseVal.numberOfItems; ++i) {
var baseVal = elem.transform.baseVal[i];
// quick tests showed that even if one changes only the fields e and f or uses createSVGTransformFromMatrix
// the brower may add a SVG_TRANSFORM_MATRIX instead of a SVG_TRANSFORM_TRANSLATE
if (baseVal.type === SVGTransform.SVG_TRANSFORM_MATRIX) {
transform = baseVal;
break;
}
}
if (transform == null) {
transform = elem.transform.baseVal.createSVGTransformFromMatrix(Tools.svg.createSVGMatrix());
elem.transform.baseVal.appendItem(transform);
}
return transform.matrix;
}
var transformRelative = function (m,t) { var transformAbsolute = function (m, t) {
return [ return [m.a * t[0] + m.c * t[1] + m.e, m.b * t[0] + m.d * t[1] + m.f];
m.a*t[0]+m.c*t[1], };
m.b*t[0]+m.d*t[1]
]
}
var transformAbsolute = function (m,t) { SVGGraphicsElement.prototype.transformedBBox = function (scale = 1) {
return [ bbox = this.getBBox();
m.a*t[0]+m.c*t[1]+m.e, tmatrix = get_transform_matrix(this);
m.b*t[0]+m.d*t[1]+m.f tmatrix.e /= scale;
] tmatrix.f /= scale;
} return {
r: transformAbsolute(tmatrix, [bbox.x / scale, bbox.y / scale]),
a: transformRelative(tmatrix, [bbox.width / scale, 0]),
b: transformRelative(tmatrix, [0, bbox.height / scale]),
};
};
SVGGraphicsElement.prototype.transformedBBox = function (scale=1) { SVGSVGElement.prototype.transformedBBox = function (scale = 1) {
bbox = this.getBBox(); bbox = {
tmatrix = get_transform_matrix(this); x: this.x.baseVal.value,
tmatrix.e /= scale; y: this.y.baseVal.value,
tmatrix.f /= scale; width: this.width.baseVal.value,
return { height: this.height.baseVal.value,
r: transformAbsolute(tmatrix,[bbox.x/scale,bbox.y/scale]), };
a: transformRelative(tmatrix,[bbox.width/scale,0]), tmatrix = get_transform_matrix(this);
b: transformRelative(tmatrix,[0,bbox.height/scale]) tmatrix.e /= scale;
} tmatrix.f /= scale;
} return {
r: transformAbsolute(tmatrix, [bbox.x / scale, bbox.y / scale]),
a: transformRelative(tmatrix, [bbox.width / scale, 0]),
b: transformRelative(tmatrix, [0, bbox.height / scale]),
};
};
SVGSVGElement.prototype.transformedBBox = function (scale=1) { var pointInTransformedBBox = function ([x, y], { r, a, b }) {
bbox = { var d = [x - r[0], y - r[1]];
x: this.x.baseVal.value, var idet = a[0] * b[1] - a[1] * b[0];
y: this.y.baseVal.value, var c1 = (d[0] * b[1] - d[1] * b[0]) / idet;
width: this.width.baseVal.value, var c2 = (d[1] * a[0] - d[0] * a[1]) / idet;
height: this.height.baseVal.value return c1 >= 0 && c1 <= 1 && c2 >= 0 && c2 <= 1;
}; };
tmatrix = get_transform_matrix(this);
tmatrix.e /= scale;
tmatrix.f /= scale;
return {
r: transformAbsolute(tmatrix,[bbox.x/scale,bbox.y/scale]),
a: transformRelative(tmatrix,[bbox.width/scale,0]),
b: transformRelative(tmatrix,[0,bbox.height/scale])
}
}
var pointInTransformedBBox = function ([x,y],{r,a,b}) { SVGGraphicsElement.prototype.transformedBBoxContains = function (x, y) {
var d = [x-r[0],y-r[1]]; return pointInTransformedBBox([x, y], this.transformedBBox());
var idet = (a[0]*b[1]-a[1]*b[0]); };
var c1 = (d[0]*b[1]-d[1]*b[0]) / idet;
var c2 = (d[1]*a[0]-d[0]*a[1]) / idet;
return (c1>=0 && c1<=1 && c2>=0 && c2<=1)
}
SVGGraphicsElement.prototype.transformedBBoxContains = function (x,y) { function transformedBBoxIntersects(bbox_a, bbox_b) {
return pointInTransformedBBox([x, y], this.transformedBBox()) var corners = [
} bbox_b.r,
[bbox_b.r[0] + bbox_b.a[0], bbox_b.r[1] + bbox_b.a[1]],
[bbox_b.r[0] + bbox_b.b[0], bbox_b.r[1] + bbox_b.b[1]],
[
bbox_b.r[0] + bbox_b.a[0] + bbox_b.b[0],
bbox_b.r[1] + bbox_b.a[1] + bbox_b.b[1],
],
];
return corners.every(function (corner) {
return pointInTransformedBBox(corner, bbox_a);
});
}
function transformedBBoxIntersects(bbox_a,bbox_b) { SVGGraphicsElement.prototype.transformedBBoxIntersects = function (bbox) {
var corners = [ return transformedBBoxIntersects(this.transformedBBox(), bbox);
bbox_b.r, };
[bbox_b.r[0] + bbox_b.a[0], bbox_b.r[1] + bbox_b.a[1]],
[bbox_b.r[0] + bbox_b.b[0], bbox_b.r[1] + bbox_b.b[1]],
[bbox_b.r[0] + bbox_b.a[0] + bbox_b.b[0], bbox_b.r[1] + bbox_b.a[1] + bbox_b.b[1]]
]
return corners.every(function(corner) {
return pointInTransformedBBox(corner, bbox_a);
})
}
SVGGraphicsElement.prototype.transformedBBoxIntersects= function (bbox) { return [pointInTransformedBBox, transformedBBoxIntersects];
return transformedBBoxIntersects(this.transformedBBox(),bbox) })();
}
return [pointInTransformedBBox,
transformedBBoxIntersects]
})();
} }

View file

@ -1,7 +1,7 @@
/** /**
* MINITPL * MINITPL
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2013 Ophir LOJKINE * Copyright (C) 2013 Ophir LOJKINE
@ -25,40 +25,38 @@
*/ */
Minitpl = (function () { Minitpl = (function () {
function Minitpl(elem, data) {
this.elem = typeof elem === "string" ? document.querySelector(elem) : elem;
if (!elem) {
throw "Invalid element!";
}
this.parent = this.elem.parentNode;
this.parent.removeChild(this.elem);
}
function Minitpl(elem, data) { function transform(element, transformer) {
this.elem = (typeof (elem) === "string") ? document.querySelector(elem) : elem; if (typeof transformer === "function") {
if (!elem) { transformer(element);
throw "Invalid element!"; } else {
} element.textContent = transformer;
this.parent = this.elem.parentNode; }
this.parent.removeChild(this.elem); }
}
function transform(element, transformer) { Minitpl.prototype.add = function (data) {
if (typeof (transformer) === "function") { var newElem = this.elem.cloneNode(true);
transformer(element); if (typeof data === "object") {
} else { for (var key in data) {
element.textContent = transformer; var matches = newElem.querySelectorAll(key);
} for (var i = 0; i < matches.length; i++) {
} transform(matches[i], data[key]);
}
Minitpl.prototype.add = function (data) { }
var newElem = this.elem.cloneNode(true); } else {
if (typeof (data) === "object") { transform(newElem, data);
for (var key in data) { }
var matches = newElem.querySelectorAll(key); this.parent.appendChild(newElem);
for (var i = 0; i < matches.length; i++) { return newElem;
transform(matches[i], data[key]); };
}
}
} else {
transform(newElem, data);
}
this.parent.appendChild(newElem);
return newElem;
}
return Minitpl;
}());
return Minitpl;
})();

View file

@ -1,4 +1,3 @@
// @info // @info
// Polyfill for SVG getPathData() and setPathData() methods. Based on: // Polyfill for SVG getPathData() and setPathData() methods. Based on:
// - SVGPathSeg polyfill by Philip Rogers (MIT License) // - SVGPathSeg polyfill by Philip Rogers (MIT License)
@ -11,11 +10,32 @@
// Jarosław Foksa // Jarosław Foksa
// @license // @license
// MIT License // MIT License
if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathData) { if (
!SVGPathElement.prototype.getPathData ||
!SVGPathElement.prototype.setPathData
) {
(function () { (function () {
var commandsMap = { var commandsMap = {
"Z": "Z", "M": "M", "L": "L", "C": "C", "Q": "Q", "A": "A", "H": "H", "V": "V", "S": "S", "T": "T", Z: "Z",
"z": "Z", "m": "m", "l": "l", "c": "c", "q": "q", "a": "a", "h": "h", "v": "v", "s": "s", "t": "t" M: "M",
L: "L",
C: "C",
Q: "Q",
A: "A",
H: "H",
V: "V",
S: "S",
T: "T",
z: "Z",
m: "m",
l: "l",
c: "c",
q: "q",
a: "a",
h: "h",
v: "v",
s: "s",
t: "t",
}; };
var Source = function (string) { var Source = function (string) {
@ -41,27 +61,27 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
// Check for remaining coordinates in the current command. // Check for remaining coordinates in the current command.
if ( if (
(char === "+" || char === "-" || char === "." || (char >= "0" && char <= "9")) && this._prevCommand !== "Z" (char === "+" ||
char === "-" ||
char === "." ||
(char >= "0" && char <= "9")) &&
this._prevCommand !== "Z"
) { ) {
if (this._prevCommand === "M") { if (this._prevCommand === "M") {
command = "L"; command = "L";
} } else if (this._prevCommand === "m") {
else if (this._prevCommand === "m") {
command = "l"; command = "l";
} } else {
else {
command = this._prevCommand; command = this._prevCommand;
} }
} } else {
else {
command = null; command = null;
} }
if (command === null) { if (command === null) {
return null; return null;
} }
} } else {
else {
this._currentIndex += 1; this._currentIndex += 1;
} }
@ -72,24 +92,25 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (cmd === "H" || cmd === "V") { if (cmd === "H" || cmd === "V") {
values = [this._parseNumber()]; values = [this._parseNumber()];
} } else if (cmd === "M" || cmd === "L" || cmd === "T") {
else if (cmd === "M" || cmd === "L" || cmd === "T") {
values = [this._parseNumber(), this._parseNumber()]; values = [this._parseNumber(), this._parseNumber()];
} } else if (cmd === "S" || cmd === "Q") {
else if (cmd === "S" || cmd === "Q") { values = [
values = [this._parseNumber(), this._parseNumber(), this._parseNumber(), this._parseNumber()]; this._parseNumber(),
} this._parseNumber(),
else if (cmd === "C") { this._parseNumber(),
this._parseNumber(),
];
} else if (cmd === "C") {
values = [ values = [
this._parseNumber(), this._parseNumber(),
this._parseNumber(), this._parseNumber(),
this._parseNumber(), this._parseNumber(),
this._parseNumber(), this._parseNumber(),
this._parseNumber(), this._parseNumber(),
this._parseNumber() this._parseNumber(),
]; ];
} } else if (cmd === "A") {
else if (cmd === "A") {
values = [ values = [
this._parseNumber(), this._parseNumber(),
this._parseNumber(), this._parseNumber(),
@ -97,10 +118,9 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
this._parseArcFlag(), this._parseArcFlag(),
this._parseArcFlag(), this._parseArcFlag(),
this._parseNumber(), this._parseNumber(),
this._parseNumber() this._parseNumber(),
]; ];
} } else if (cmd === "Z") {
else if (cmd === "Z") {
this._skipOptionalSpaces(); this._skipOptionalSpaces();
values = []; values = [];
} }
@ -108,8 +128,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (values === null || values.indexOf(null) >= 0) { if (values === null || values.indexOf(null) >= 0) {
// Unknown command or known command with invalid values // Unknown command or known command with invalid values
return null; return null;
} } else {
else {
return { type: command, values: values }; return { type: command, values: values };
} }
}, },
@ -136,7 +155,14 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
_isCurrentSpace: function () { _isCurrentSpace: function () {
var char = this._string[this._currentIndex]; var char = this._string[this._currentIndex];
return char <= " " && (char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f"); return (
char <= " " &&
(char === " " ||
char === "\n" ||
char === "\t" ||
char === "\r" ||
char === "\f")
);
}, },
_skipOptionalSpaces: function () { _skipOptionalSpaces: function () {
@ -157,7 +183,10 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
} }
if (this._skipOptionalSpaces()) { if (this._skipOptionalSpaces()) {
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ",") { if (
this._currentIndex < this._endIndex &&
this._string[this._currentIndex] === ","
) {
this._currentIndex += 1; this._currentIndex += 1;
this._skipOptionalSpaces(); this._skipOptionalSpaces();
} }
@ -180,20 +209,24 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
this._skipOptionalSpaces(); this._skipOptionalSpaces();
// Read the sign. // Read the sign.
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "+") { if (
this._currentIndex < this._endIndex &&
this._string[this._currentIndex] === "+"
) {
this._currentIndex += 1; this._currentIndex += 1;
} } else if (
else if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "-") { this._currentIndex < this._endIndex &&
this._string[this._currentIndex] === "-"
) {
this._currentIndex += 1; this._currentIndex += 1;
sign = -1; sign = -1;
} }
if ( if (
this._currentIndex === this._endIndex || this._currentIndex === this._endIndex ||
( ((this._string[this._currentIndex] < "0" ||
(this._string[this._currentIndex] < "0" || this._string[this._currentIndex] > "9") && this._string[this._currentIndex] > "9") &&
this._string[this._currentIndex] !== "." this._string[this._currentIndex] !== ".")
)
) { ) {
// The first character of a number must be one of [0-9+-.]. // The first character of a number must be one of [0-9+-.].
return null; return null;
@ -222,7 +255,10 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
} }
// Read the decimals. // Read the decimals.
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ".") { if (
this._currentIndex < this._endIndex &&
this._string[this._currentIndex] === "."
) {
this._currentIndex += 1; this._currentIndex += 1;
// There must be a least one digit following the . // There must be a least one digit following the .
@ -249,16 +285,17 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if ( if (
this._currentIndex !== startIndex && this._currentIndex !== startIndex &&
this._currentIndex + 1 < this._endIndex && this._currentIndex + 1 < this._endIndex &&
(this._string[this._currentIndex] === "e" || this._string[this._currentIndex] === "E") && (this._string[this._currentIndex] === "e" ||
(this._string[this._currentIndex + 1] !== "x" && this._string[this._currentIndex + 1] !== "m") this._string[this._currentIndex] === "E") &&
this._string[this._currentIndex + 1] !== "x" &&
this._string[this._currentIndex + 1] !== "m"
) { ) {
this._currentIndex += 1; this._currentIndex += 1;
// Read the sign of the exponent. // Read the sign of the exponent.
if (this._string[this._currentIndex] === "+") { if (this._string[this._currentIndex] === "+") {
this._currentIndex += 1; this._currentIndex += 1;
} } else if (this._string[this._currentIndex] === "-") {
else if (this._string[this._currentIndex] === "-") {
this._currentIndex += 1; this._currentIndex += 1;
expsign = -1; expsign = -1;
} }
@ -278,7 +315,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
this._string[this._currentIndex] <= "9" this._string[this._currentIndex] <= "9"
) { ) {
exponent *= 10; exponent *= 10;
exponent += (this._string[this._currentIndex] - "0"); exponent += this._string[this._currentIndex] - "0";
this._currentIndex += 1; this._currentIndex += 1;
} }
} }
@ -311,17 +348,15 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (flagChar === "0") { if (flagChar === "0") {
flag = 0; flag = 0;
} } else if (flagChar === "1") {
else if (flagChar === "1") {
flag = 1; flag = 1;
} } else {
else {
return null; return null;
} }
this._skipOptionalSpacesOrDelimiter(); this._skipOptionalSpacesOrDelimiter();
return flag; return flag;
} },
}; };
var parsePathDataString = function (string) { var parsePathDataString = function (string) {
@ -336,25 +371,37 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (pathSeg === null) { if (pathSeg === null) {
break; break;
} } else {
else {
pathData.push(pathSeg); pathData.push(pathSeg);
} }
} }
} }
return pathData; return pathData;
} };
var setAttribute = SVGPathElement.prototype.setAttribute; var setAttribute = SVGPathElement.prototype.setAttribute;
var removeAttribute = SVGPathElement.prototype.removeAttribute; var removeAttribute = SVGPathElement.prototype.removeAttribute;
var $cachedPathData = window.Symbol ? Symbol() : "__cachedPathData"; var $cachedPathData = window.Symbol ? Symbol() : "__cachedPathData";
var $cachedNormalizedPathData = window.Symbol ? Symbol() : "__cachedNormalizedPathData"; var $cachedNormalizedPathData = window.Symbol
? Symbol()
: "__cachedNormalizedPathData";
// @info // @info
// Get an array of corresponding cubic bezier curve parameters for given arc curve paramters. // Get an array of corresponding cubic bezier curve parameters for given arc curve paramters.
var arcToCubicCurves = function (x1, y1, x2, y2, r1, r2, angle, largeArcFlag, sweepFlag, _recursive) { var arcToCubicCurves = function (
x1,
y1,
x2,
y2,
r1,
r2,
angle,
largeArcFlag,
sweepFlag,
_recursive,
) {
var degToRad = function (degrees) { var degToRad = function (degrees) {
return (Math.PI * degrees) / 180; return (Math.PI * degrees) / 180;
}; };
@ -374,8 +421,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
f2 = _recursive[1]; f2 = _recursive[1];
cx = _recursive[2]; cx = _recursive[2];
cy = _recursive[3]; cy = _recursive[3];
} } else {
else {
var p1 = rotate(x1, y1, -angleRad); var p1 = rotate(x1, y1, -angleRad);
x1 = p1.x; x1 = p1.x;
y1 = p1.y; y1 = p1.y;
@ -398,8 +444,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (largeArcFlag === sweepFlag) { if (largeArcFlag === sweepFlag) {
sign = -1; sign = -1;
} } else {
else {
sign = 1; sign = 1;
} }
@ -411,8 +456,8 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var k = sign * Math.sqrt(Math.abs(left / right)); var k = sign * Math.sqrt(Math.abs(left / right));
cx = k * r1 * y / r2 + (x1 + x2) / 2; cx = (k * r1 * y) / r2 + (x1 + x2) / 2;
cy = k * -r2 * x / r1 + (y1 + y2) / 2; cy = (k * -r2 * x) / r1 + (y1 + y2) / 2;
f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9))); f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9)));
f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9))); f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9)));
@ -441,21 +486,31 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var df = f2 - f1; var df = f2 - f1;
if (Math.abs(df) > (Math.PI * 120 / 180)) { if (Math.abs(df) > (Math.PI * 120) / 180) {
var f2old = f2; var f2old = f2;
var x2old = x2; var x2old = x2;
var y2old = y2; var y2old = y2;
if (sweepFlag && f2 > f1) { if (sweepFlag && f2 > f1) {
f2 = f1 + (Math.PI * 120 / 180) * (1); f2 = f1 + ((Math.PI * 120) / 180) * 1;
} } else {
else { f2 = f1 + ((Math.PI * 120) / 180) * -1;
f2 = f1 + (Math.PI * 120 / 180) * (-1);
} }
x2 = cx + r1 * Math.cos(f2); x2 = cx + r1 * Math.cos(f2);
y2 = cy + r2 * Math.sin(f2); y2 = cy + r2 * Math.sin(f2);
params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, [f2, f2old, cx, cy]); params = arcToCubicCurves(
x2,
y2,
x2old,
y2old,
r1,
r2,
angle,
0,
sweepFlag,
[f2, f2old, cx, cy],
);
} }
df = f2 - f1; df = f2 - f1;
@ -465,8 +520,8 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var c2 = Math.cos(f2); var c2 = Math.cos(f2);
var s2 = Math.sin(f2); var s2 = Math.sin(f2);
var t = Math.tan(df / 4); var t = Math.tan(df / 4);
var hx = 4 / 3 * r1 * t; var hx = (4 / 3) * r1 * t;
var hy = 4 / 3 * r2 * t; var hy = (4 / 3) * r2 * t;
var m1 = [x1, y1]; var m1 = [x1, y1];
var m2 = [x1 + hx * s1, y1 - hy * c1]; var m2 = [x1 + hx * s1, y1 - hy * c1];
@ -478,8 +533,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (_recursive) { if (_recursive) {
return [m2, m3, m4].concat(params); return [m2, m3, m4].concat(params);
} } else {
else {
params = [m2, m3, m4].concat(params); params = [m2, m3, m4].concat(params);
var curves = []; var curves = [];
@ -497,7 +551,10 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var clonePathData = function (pathData) { var clonePathData = function (pathData) {
return pathData.map(function (seg) { return pathData.map(function (seg) {
return { type: seg.type, values: Array.prototype.slice.call(seg.values) } return {
type: seg.type,
values: Array.prototype.slice.call(seg.values),
};
}); });
}; };
@ -526,9 +583,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "m") {
else if (type === "m") {
var x = currentX + seg.values[0]; var x = currentX + seg.values[0];
var y = currentY + seg.values[1]; var y = currentY + seg.values[1];
@ -539,9 +594,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "L") {
else if (type === "L") {
var x = seg.values[0]; var x = seg.values[0];
var y = seg.values[1]; var y = seg.values[1];
@ -549,9 +602,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "l") {
else if (type === "l") {
var x = currentX + seg.values[0]; var x = currentX + seg.values[0];
var y = currentY + seg.values[1]; var y = currentY + seg.values[1];
@ -559,9 +610,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "C") {
else if (type === "C") {
var x1 = seg.values[0]; var x1 = seg.values[0];
var y1 = seg.values[1]; var y1 = seg.values[1];
var x2 = seg.values[2]; var x2 = seg.values[2];
@ -569,13 +618,14 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var x = seg.values[4]; var x = seg.values[4];
var y = seg.values[5]; var y = seg.values[5];
absolutizedPathData.push({ type: "C", values: [x1, y1, x2, y2, x, y] }); absolutizedPathData.push({
type: "C",
values: [x1, y1, x2, y2, x, y],
});
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "c") {
else if (type === "c") {
var x1 = currentX + seg.values[0]; var x1 = currentX + seg.values[0];
var y1 = currentY + seg.values[1]; var y1 = currentY + seg.values[1];
var x2 = currentX + seg.values[2]; var x2 = currentX + seg.values[2];
@ -583,13 +633,14 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var x = currentX + seg.values[4]; var x = currentX + seg.values[4];
var y = currentY + seg.values[5]; var y = currentY + seg.values[5];
absolutizedPathData.push({ type: "C", values: [x1, y1, x2, y2, x, y] }); absolutizedPathData.push({
type: "C",
values: [x1, y1, x2, y2, x, y],
});
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "Q") {
else if (type === "Q") {
var x1 = seg.values[0]; var x1 = seg.values[0];
var y1 = seg.values[1]; var y1 = seg.values[1];
var x = seg.values[2]; var x = seg.values[2];
@ -599,9 +650,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "q") {
else if (type === "q") {
var x1 = currentX + seg.values[0]; var x1 = currentX + seg.values[0];
var y1 = currentY + seg.values[1]; var y1 = currentY + seg.values[1];
var x = currentX + seg.values[2]; var x = currentX + seg.values[2];
@ -611,59 +660,61 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "A") {
else if (type === "A") {
var x = seg.values[5]; var x = seg.values[5];
var y = seg.values[6]; var y = seg.values[6];
absolutizedPathData.push({ absolutizedPathData.push({
type: "A", type: "A",
values: [seg.values[0], seg.values[1], seg.values[2], seg.values[3], seg.values[4], x, y] values: [
seg.values[0],
seg.values[1],
seg.values[2],
seg.values[3],
seg.values[4],
x,
y,
],
}); });
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "a") {
else if (type === "a") {
var x = currentX + seg.values[5]; var x = currentX + seg.values[5];
var y = currentY + seg.values[6]; var y = currentY + seg.values[6];
absolutizedPathData.push({ absolutizedPathData.push({
type: "A", type: "A",
values: [seg.values[0], seg.values[1], seg.values[2], seg.values[3], seg.values[4], x, y] values: [
seg.values[0],
seg.values[1],
seg.values[2],
seg.values[3],
seg.values[4],
x,
y,
],
}); });
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "H") {
else if (type === "H") {
var x = seg.values[0]; var x = seg.values[0];
absolutizedPathData.push({ type: "H", values: [x] }); absolutizedPathData.push({ type: "H", values: [x] });
currentX = x; currentX = x;
} } else if (type === "h") {
else if (type === "h") {
var x = currentX + seg.values[0]; var x = currentX + seg.values[0];
absolutizedPathData.push({ type: "H", values: [x] }); absolutizedPathData.push({ type: "H", values: [x] });
currentX = x; currentX = x;
} } else if (type === "V") {
else if (type === "V") {
var y = seg.values[0]; var y = seg.values[0];
absolutizedPathData.push({ type: "V", values: [y] }); absolutizedPathData.push({ type: "V", values: [y] });
currentY = y; currentY = y;
} } else if (type === "v") {
else if (type === "v") {
var y = currentY + seg.values[0]; var y = currentY + seg.values[0];
absolutizedPathData.push({ type: "V", values: [y] }); absolutizedPathData.push({ type: "V", values: [y] });
currentY = y; currentY = y;
} } else if (type === "S") {
else if (type === "S") {
var x2 = seg.values[0]; var x2 = seg.values[0];
var y2 = seg.values[1]; var y2 = seg.values[1];
var x = seg.values[2]; var x = seg.values[2];
@ -673,9 +724,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "s") {
else if (type === "s") {
var x2 = currentX + seg.values[0]; var x2 = currentX + seg.values[0];
var y2 = currentY + seg.values[1]; var y2 = currentY + seg.values[1];
var x = currentX + seg.values[2]; var x = currentX + seg.values[2];
@ -685,29 +734,23 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "T") {
else if (type === "T") {
var x = seg.values[0]; var x = seg.values[0];
var y = seg.values[1] var y = seg.values[1];
absolutizedPathData.push({ type: "T", values: [x, y] }); absolutizedPathData.push({ type: "T", values: [x, y] });
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "t") {
else if (type === "t") {
var x = currentX + seg.values[0]; var x = currentX + seg.values[0];
var y = currentY + seg.values[1] var y = currentY + seg.values[1];
absolutizedPathData.push({ type: "T", values: [x, y] }); absolutizedPathData.push({ type: "T", values: [x, y] });
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (type === "Z" || type === "z") {
else if (type === "Z" || type === "z") {
absolutizedPathData.push({ type: "Z", values: [] }); absolutizedPathData.push({ type: "Z", values: [] });
currentX = subpathX; currentX = subpathX;
@ -746,9 +789,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (seg.type === "C") {
else if (seg.type === "C") {
var x1 = seg.values[0]; var x1 = seg.values[0];
var y1 = seg.values[1]; var y1 = seg.values[1];
var x2 = seg.values[2]; var x2 = seg.values[2];
@ -763,9 +804,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (seg.type === "L") {
else if (seg.type === "L") {
var x = seg.values[0]; var x = seg.values[0];
var y = seg.values[1]; var y = seg.values[1];
@ -773,25 +812,19 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (seg.type === "H") {
else if (seg.type === "H") {
var x = seg.values[0]; var x = seg.values[0];
reducedPathData.push({ type: "L", values: [x, currentY] }); reducedPathData.push({ type: "L", values: [x, currentY] });
currentX = x; currentX = x;
} } else if (seg.type === "V") {
else if (seg.type === "V") {
var y = seg.values[0]; var y = seg.values[0];
reducedPathData.push({ type: "L", values: [currentX, y] }); reducedPathData.push({ type: "L", values: [currentX, y] });
currentY = y; currentY = y;
} } else if (seg.type === "S") {
else if (seg.type === "S") {
var x2 = seg.values[0]; var x2 = seg.values[0];
var y2 = seg.values[1]; var y2 = seg.values[1];
var x = seg.values[2]; var x = seg.values[2];
@ -802,8 +835,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (lastType === "C" || lastType === "S") { if (lastType === "C" || lastType === "S") {
cx1 = currentX + (currentX - lastControlX); cx1 = currentX + (currentX - lastControlX);
cy1 = currentY + (currentY - lastControlY); cy1 = currentY + (currentY - lastControlY);
} } else {
else {
cx1 = currentX; cx1 = currentX;
cy1 = currentY; cy1 = currentY;
} }
@ -815,9 +847,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (seg.type === "T") {
else if (seg.type === "T") {
var x = seg.values[0]; var x = seg.values[0];
var y = seg.values[1]; var y = seg.values[1];
@ -826,47 +856,48 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (lastType === "Q" || lastType === "T") { if (lastType === "Q" || lastType === "T") {
x1 = currentX + (currentX - lastControlX); x1 = currentX + (currentX - lastControlX);
y1 = currentY + (currentY - lastControlY); y1 = currentY + (currentY - lastControlY);
} } else {
else {
x1 = currentX; x1 = currentX;
y1 = currentY; y1 = currentY;
} }
var cx1 = currentX + 2 * (x1 - currentX) / 3; var cx1 = currentX + (2 * (x1 - currentX)) / 3;
var cy1 = currentY + 2 * (y1 - currentY) / 3; var cy1 = currentY + (2 * (y1 - currentY)) / 3;
var cx2 = x + 2 * (x1 - x) / 3; var cx2 = x + (2 * (x1 - x)) / 3;
var cy2 = y + 2 * (y1 - y) / 3; var cy2 = y + (2 * (y1 - y)) / 3;
reducedPathData.push({ type: "C", values: [cx1, cy1, cx2, cy2, x, y] }); reducedPathData.push({
type: "C",
values: [cx1, cy1, cx2, cy2, x, y],
});
lastControlX = x1; lastControlX = x1;
lastControlY = y1; lastControlY = y1;
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (seg.type === "Q") {
else if (seg.type === "Q") {
var x1 = seg.values[0]; var x1 = seg.values[0];
var y1 = seg.values[1]; var y1 = seg.values[1];
var x = seg.values[2]; var x = seg.values[2];
var y = seg.values[3]; var y = seg.values[3];
var cx1 = currentX + 2 * (x1 - currentX) / 3; var cx1 = currentX + (2 * (x1 - currentX)) / 3;
var cy1 = currentY + 2 * (y1 - currentY) / 3; var cy1 = currentY + (2 * (y1 - currentY)) / 3;
var cx2 = x + 2 * (x1 - x) / 3; var cx2 = x + (2 * (x1 - x)) / 3;
var cy2 = y + 2 * (y1 - y) / 3; var cy2 = y + (2 * (y1 - y)) / 3;
reducedPathData.push({ type: "C", values: [cx1, cy1, cx2, cy2, x, y] }); reducedPathData.push({
type: "C",
values: [cx1, cy1, cx2, cy2, x, y],
});
lastControlX = x1; lastControlX = x1;
lastControlY = y1; lastControlY = y1;
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else if (seg.type === "A") {
else if (seg.type === "A") {
var r1 = Math.abs(seg.values[0]); var r1 = Math.abs(seg.values[0]);
var r2 = Math.abs(seg.values[1]); var r2 = Math.abs(seg.values[1]);
var angle = seg.values[2]; var angle = seg.values[2];
@ -876,14 +907,26 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var y = seg.values[6]; var y = seg.values[6];
if (r1 === 0 || r2 === 0) { if (r1 === 0 || r2 === 0) {
reducedPathData.push({ type: "C", values: [currentX, currentY, x, y, x, y] }); reducedPathData.push({
type: "C",
values: [currentX, currentY, x, y, x, y],
});
currentX = x; currentX = x;
currentY = y; currentY = y;
} } else {
else {
if (currentX !== x || currentY !== y) { if (currentX !== x || currentY !== y) {
var curves = arcToCubicCurves(currentX, currentY, x, y, r1, r2, angle, largeArcFlag, sweepFlag); var curves = arcToCubicCurves(
currentX,
currentY,
x,
y,
r1,
r2,
angle,
largeArcFlag,
sweepFlag,
);
curves.forEach(function (curve) { curves.forEach(function (curve) {
reducedPathData.push({ type: "C", values: curve }); reducedPathData.push({ type: "C", values: curve });
@ -893,9 +936,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
currentY = y; currentY = y;
} }
} }
} } else if (seg.type === "Z") {
else if (seg.type === "Z") {
reducedPathData.push(seg); reducedPathData.push(seg);
currentX = subpathX; currentX = subpathX;
@ -930,14 +971,12 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (options && options.normalize) { if (options && options.normalize) {
if (this[$cachedNormalizedPathData]) { if (this[$cachedNormalizedPathData]) {
return clonePathData(this[$cachedNormalizedPathData]); return clonePathData(this[$cachedNormalizedPathData]);
} } else {
else {
var pathData; var pathData;
if (this[$cachedPathData]) { if (this[$cachedPathData]) {
pathData = clonePathData(this[$cachedPathData]); pathData = clonePathData(this[$cachedPathData]);
} } else {
else {
pathData = parsePathDataString(this.getAttribute("d") || ""); pathData = parsePathDataString(this.getAttribute("d") || "");
this[$cachedPathData] = clonePathData(pathData); this[$cachedPathData] = clonePathData(pathData);
} }
@ -946,12 +985,10 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
this[$cachedNormalizedPathData] = clonePathData(normalizedPathData); this[$cachedNormalizedPathData] = clonePathData(normalizedPathData);
return normalizedPathData; return normalizedPathData;
} }
} } else {
else {
if (this[$cachedPathData]) { if (this[$cachedPathData]) {
return clonePathData(this[$cachedPathData]); return clonePathData(this[$cachedPathData]);
} } else {
else {
var pathData = parsePathDataString(this.getAttribute("d") || ""); var pathData = parsePathDataString(this.getAttribute("d") || "");
this[$cachedPathData] = clonePathData(pathData); this[$cachedPathData] = clonePathData(pathData);
return pathData; return pathData;
@ -964,12 +1001,10 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
if (isIE) { if (isIE) {
// @bugfix https://github.com/mbostock/d3/issues/1737 // @bugfix https://github.com/mbostock/d3/issues/1737
this.setAttribute("d", ""); this.setAttribute("d", "");
} } else {
else {
this.removeAttribute("d"); this.removeAttribute("d");
} }
} } else {
else {
var d = ""; var d = "";
for (var i = 0, l = pathData.length; i < l; i += 1) { for (var i = 0, l = pathData.length; i < l; i += 1) {
@ -995,8 +1030,12 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var y = this.y.baseVal.value; var y = this.y.baseVal.value;
var width = this.width.baseVal.value; var width = this.width.baseVal.value;
var height = this.height.baseVal.value; var height = this.height.baseVal.value;
var rx = this.hasAttribute("rx") ? this.rx.baseVal.value : this.ry.baseVal.value; var rx = this.hasAttribute("rx")
var ry = this.hasAttribute("ry") ? this.ry.baseVal.value : this.rx.baseVal.value; ? this.rx.baseVal.value
: this.ry.baseVal.value;
var ry = this.hasAttribute("ry")
? this.ry.baseVal.value
: this.rx.baseVal.value;
if (rx > width / 2) { if (rx > width / 2) {
rx = width / 2; rx = width / 2;
@ -1016,12 +1055,14 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
{ type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] }, { type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
{ type: "V", values: [y + ry] }, { type: "V", values: [y + ry] },
{ type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] }, { type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
{ type: "Z", values: [] } { type: "Z", values: [] },
]; ];
// Get rid of redundant "A" segs when either rx or ry is 0 // Get rid of redundant "A" segs when either rx or ry is 0
pathData = pathData.filter(function (s) { pathData = pathData.filter(function (s) {
return s.type === "A" && (s.values[0] === 0 || s.values[1] === 0) ? false : true; return s.type === "A" && (s.values[0] === 0 || s.values[1] === 0)
? false
: true;
}); });
if (options && options.normalize === true) { if (options && options.normalize === true) {
@ -1042,7 +1083,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
{ type: "A", values: [r, r, 0, 0, 1, cx - r, cy] }, { type: "A", values: [r, r, 0, 0, 1, cx - r, cy] },
{ type: "A", values: [r, r, 0, 0, 1, cx, cy - r] }, { type: "A", values: [r, r, 0, 0, 1, cx, cy - r] },
{ type: "A", values: [r, r, 0, 0, 1, cx + r, cy] }, { type: "A", values: [r, r, 0, 0, 1, cx + r, cy] },
{ type: "Z", values: [] } { type: "Z", values: [] },
]; ];
if (options && options.normalize === true) { if (options && options.normalize === true) {
@ -1064,7 +1105,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
{ type: "A", values: [rx, ry, 0, 0, 1, cx - rx, cy] }, { type: "A", values: [rx, ry, 0, 0, 1, cx - rx, cy] },
{ type: "A", values: [rx, ry, 0, 0, 1, cx, cy - ry] }, { type: "A", values: [rx, ry, 0, 0, 1, cx, cy - ry] },
{ type: "A", values: [rx, ry, 0, 0, 1, cx + rx, cy] }, { type: "A", values: [rx, ry, 0, 0, 1, cx + rx, cy] },
{ type: "Z", values: [] } { type: "Z", values: [] },
]; ];
if (options && options.normalize === true) { if (options && options.normalize === true) {
@ -1077,7 +1118,7 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
SVGLineElement.prototype.getPathData = function () { SVGLineElement.prototype.getPathData = function () {
return [ return [
{ type: "M", values: [this.x1.baseVal.value, this.y1.baseVal.value] }, { type: "M", values: [this.x1.baseVal.value, this.y1.baseVal.value] },
{ type: "L", values: [this.x2.baseVal.value, this.y2.baseVal.value] } { type: "L", values: [this.x2.baseVal.value, this.y2.baseVal.value] },
]; ];
}; };
@ -1088,8 +1129,8 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var point = this.points.getItem(i); var point = this.points.getItem(i);
pathData.push({ pathData.push({
type: (i === 0 ? "M" : "L"), type: i === 0 ? "M" : "L",
values: [point.x, point.y] values: [point.x, point.y],
}); });
} }
@ -1103,14 +1144,14 @@ if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathDa
var point = this.points.getItem(i); var point = this.points.getItem(i);
pathData.push({ pathData.push({
type: (i === 0 ? "M" : "L"), type: i === 0 ? "M" : "L",
values: [point.x, point.y] values: [point.x, point.y],
}); });
} }
pathData.push({ pathData.push({
type: "Z", type: "Z",
values: [] values: [],
}); });
return pathData; return pathData;

View file

@ -1,25 +1,25 @@
{ {
"name": "WBO Online Whiteboard", "name": "WBO Online Whiteboard",
"short_name": "WBO", "short_name": "WBO",
"display": "standalone", "display": "standalone",
"background_color": "#8FA2BC", "background_color": "#8FA2BC",
"theme_color": "#C4DFFF", "theme_color": "#C4DFFF",
"description": "A free and opensource online collaborative drawing tool.", "description": "A free and opensource online collaborative drawing tool.",
"icons": [ "icons": [
{ {
"src": "favicon.svg", "src": "favicon.svg",
"sizes": "1024x1024", "sizes": "1024x1024",
"type": "image/svg+xml" "type": "image/svg+xml"
}, },
{ {
"src": "apple-touch-icon.png", "src": "apple-touch-icon.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "16x16", "sizes": "16x16",
"type": "image/x-icon" "type": "image/x-icon"
} }
] ]
} }

View file

@ -24,30 +24,31 @@
* @licend * @licend
*/ */
(function clear() { //Code isolation (function clear() {
//Code isolation
function clearBoard() { function clearBoard() {
var msg = { var msg = {
"type": "clear", type: "clear",
"id": "", id: "",
"token": Tools.token token: Tools.token,
}; };
Tools.drawAndSend(msg, Tools.list["Clear"]); Tools.drawAndSend(msg, Tools.list["Clear"]);
} }
function draw(data) { function draw(data) {
Tools.drawingArea.innerHTML = ""; Tools.drawingArea.innerHTML = "";
} }
Tools.add({ //The new tool Tools.add({
"name": "Clear", //The new tool
"shortcut": "c", name: "Clear",
"listeners": {}, shortcut: "c",
"icon": "tools/clear/clear.svg", listeners: {},
"oneTouch": true, icon: "tools/clear/clear.svg",
"onstart": clearBoard, oneTouch: true,
"draw": draw, onstart: clearBoard,
"mouseCursor": "crosshair", draw: draw,
}); mouseCursor: "crosshair",
});
})(); //End of code isolation })(); //End of code isolation

View file

@ -24,90 +24,109 @@
* @licend * @licend
*/ */
(function () { // Code isolation (function () {
// Code isolation
// Allocate half of the maximum server updates to cursor updates // Allocate half of the maximum server updates to cursor updates
var MIN_CURSOR_UPDATES_INTERVAL_MS = Tools.server_config.MAX_EMIT_COUNT_PERIOD / Tools.server_config.MAX_EMIT_COUNT * 2; var MIN_CURSOR_UPDATES_INTERVAL_MS =
(Tools.server_config.MAX_EMIT_COUNT_PERIOD /
Tools.server_config.MAX_EMIT_COUNT) *
2;
var CURSOR_DELETE_AFTER_MS = 1000 * 5; var CURSOR_DELETE_AFTER_MS = 1000 * 5;
var lastCursorUpdate = 0; var lastCursorUpdate = 0;
var sending = true; var sending = true;
var cursorTool = { var cursorTool = {
"name": "Cursor", name: "Cursor",
"listeners": { listeners: {
"press": function () { sending = false }, press: function () {
"move": handleMarker, sending = false;
"release": function () { sending = true }, },
}, move: handleMarker,
"onSizeChange": onSizeChange, release: function () {
"draw": draw, sending = true;
"mouseCursor": "crosshair", },
"icon": "tools/pencil/icon.svg", },
}; onSizeChange: onSizeChange,
Tools.register(cursorTool); draw: draw,
Tools.addToolListeners(cursorTool); mouseCursor: "crosshair",
icon: "tools/pencil/icon.svg",
};
Tools.register(cursorTool);
Tools.addToolListeners(cursorTool);
var message = { var message = {
type: "update", type: "update",
x: 0, x: 0,
y: 0, y: 0,
color: Tools.getColor(), color: Tools.getColor(),
size: Tools.getSize(), size: Tools.getSize(),
}; };
function handleMarker(x, y) { function handleMarker(x, y) {
// throttle local cursor updates // throttle local cursor updates
message.x = x; message.x = x;
message.y = y; message.y = y;
message.color = Tools.getColor(); message.color = Tools.getColor();
message.size = Tools.getSize(); message.size = Tools.getSize();
updateMarker(); updateMarker();
}
function onSizeChange(size) {
message.size = size;
updateMarker();
}
function updateMarker() {
if (!Tools.showMarker || !Tools.showMyCursor) return;
var cur_time = Date.now();
if (
cur_time - lastCursorUpdate > MIN_CURSOR_UPDATES_INTERVAL_MS &&
(sending || Tools.curTool.showMarker)
) {
Tools.drawAndSend(message, cursorTool);
lastCursorUpdate = cur_time;
} else {
draw(message);
} }
}
function onSizeChange(size) { var cursorsElem = Tools.svg.getElementById("cursors");
message.size = size;
updateMarker();
}
function updateMarker() { function createCursor(id) {
if (!Tools.showMarker || !Tools.showMyCursor) return; var cursor = document.createElementNS(
var cur_time = Date.now(); "http://www.w3.org/2000/svg",
if (cur_time - lastCursorUpdate > MIN_CURSOR_UPDATES_INTERVAL_MS && "circle",
(sending || Tools.curTool.showMarker)) { );
Tools.drawAndSend(message, cursorTool); cursor.setAttributeNS(null, "class", "opcursor");
lastCursorUpdate = cur_time; cursor.setAttributeNS(null, "id", id);
} else { cursor.setAttributeNS(null, "cx", 0);
draw(message); cursor.setAttributeNS(null, "cy", 0);
} cursor.setAttributeNS(null, "r", 10);
} cursorsElem.appendChild(cursor);
setTimeout(function () {
cursorsElem.removeChild(cursor);
}, CURSOR_DELETE_AFTER_MS);
return cursor;
}
var cursorsElem = Tools.svg.getElementById("cursors"); function getCursor(id) {
return document.getElementById(id) || createCursor(id);
}
function createCursor(id) { function draw(message) {
var cursor = document.createElementNS("http://www.w3.org/2000/svg", "circle"); var cursor = getCursor("cursor-" + (message.socket || "me"));
cursor.setAttributeNS(null, "class", "opcursor"); cursor.style.transform =
cursor.setAttributeNS(null, "id", id); "translate(" + message.x + "px, " + message.y + "px)";
cursor.setAttributeNS(null, "cx", 0); if (Tools.isIE)
cursor.setAttributeNS(null, "cy", 0); cursor.setAttributeNS(
cursor.setAttributeNS(null, "r", 10); null,
cursorsElem.appendChild(cursor); "transform",
setTimeout(function () { "translate(" + message.x + " " + message.y + ")",
cursorsElem.removeChild(cursor); );
}, CURSOR_DELETE_AFTER_MS); cursor.setAttributeNS(null, "fill", message.color);
return cursor; cursor.setAttributeNS(null, "r", message.size / 2);
} }
})();
function getCursor(id) {
return document.getElementById(id) || createCursor(id);
}
function draw(message) {
var cursor = getCursor("cursor-" + (message.socket || 'me'));
cursor.style.transform = "translate(" + message.x + "px, " + message.y + "px)";
if (Tools.isIE) cursor.setAttributeNS(null, "transform", "translate(" + message.x + " " + message.y + ")");
cursor.setAttributeNS(null, "fill", message.color);
cursor.setAttributeNS(null, "r", message.size / 2);
}
})();

View file

@ -24,59 +24,67 @@
* @licend * @licend
*/ */
(function download() { //Code isolation (function download() {
//Code isolation
function downloadSVGFile() { function downloadSVGFile() {
var canvasCopy = Tools.svg.cloneNode(true); var canvasCopy = Tools.svg.cloneNode(true);
canvasCopy.removeAttribute("style", ""); // Remove css transform canvasCopy.removeAttribute("style", ""); // Remove css transform
var styleNode = document.createElement("style"); var styleNode = document.createElement("style");
// Copy the stylesheets from the whiteboard to the exported SVG // Copy the stylesheets from the whiteboard to the exported SVG
styleNode.innerHTML = Array.from(document.styleSheets) styleNode.innerHTML = Array.from(document.styleSheets)
.filter(function (stylesheet) { .filter(function (stylesheet) {
if (stylesheet.href && (stylesheet.href.match(/boards\/tools\/.*\.css/) if (
|| stylesheet.href.match(/board\.css/))) { stylesheet.href &&
// This is a Stylesheet from a Tool or the Board itself, so we should include it (stylesheet.href.match(/boards\/tools\/.*\.css/) ||
return true; stylesheet.href.match(/board\.css/))
} ) {
// Not a stylesheet of the tool, so we can ignore it for export // This is a Stylesheet from a Tool or the Board itself, so we should include it
return false; return true;
})
.map(function (stylesheet) {
return Array.from(stylesheet.cssRules)
.map(function (rule) { return rule.cssText })
}).join("\n")
canvasCopy.appendChild(styleNode);
var outerHTML = canvasCopy.outerHTML || new XMLSerializer().serializeToString(canvasCopy);
var blob = new Blob([outerHTML], { type: 'image/svg+xml;charset=utf-8' });
downloadContent(blob, Tools.boardName + ".svg");
}
function downloadContent(blob, filename) {
if (window.navigator.msSaveBlob) { // Internet Explorer
window.navigator.msSaveBlob(blob, filename);
} else {
const url = URL.createObjectURL(blob);
var element = document.createElement('a');
element.setAttribute('href', url);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
window.URL.revokeObjectURL(url);
} }
// Not a stylesheet of the tool, so we can ignore it for export
return false;
})
.map(function (stylesheet) {
return Array.from(stylesheet.cssRules).map(function (rule) {
return rule.cssText;
});
})
.join("\n");
canvasCopy.appendChild(styleNode);
var outerHTML =
canvasCopy.outerHTML || new XMLSerializer().serializeToString(canvasCopy);
var blob = new Blob([outerHTML], { type: "image/svg+xml;charset=utf-8" });
downloadContent(blob, Tools.boardName + ".svg");
}
function downloadContent(blob, filename) {
if (window.navigator.msSaveBlob) {
// Internet Explorer
window.navigator.msSaveBlob(blob, filename);
} else {
const url = URL.createObjectURL(blob);
var element = document.createElement("a");
element.setAttribute("href", url);
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
window.URL.revokeObjectURL(url);
} }
}
Tools.add({ //The new tool Tools.add({
"name": "Download", //The new tool
"shortcut": "d", name: "Download",
"listeners": {}, shortcut: "d",
"icon": "tools/download/download.svg", listeners: {},
"oneTouch": true, icon: "tools/download/download.svg",
"onstart": downloadSVGFile, oneTouch: true,
"mouseCursor": "crosshair", onstart: downloadSVGFile,
}); mouseCursor: "crosshair",
});
})(); //End of code isolation })(); //End of code isolation

View file

@ -1,3 +1,3 @@
#canvas ellipse { #canvas ellipse {
fill: none; fill: none;
} }

View file

@ -24,149 +24,160 @@
* @licend * @licend
*/ */
(function () { //Code isolation (function () {
var curUpdate = { //The data of the message that will be sent for every new point //Code isolation
'type': 'update', var curUpdate = {
'id': "", //The data of the message that will be sent for every new point
'x': 0, type: "update",
'y': 0, id: "",
'x2': 0, x: 0,
'y2': 0 y: 0,
x2: 0,
y2: 0,
}, },
lastPos = { x: 0, y: 0 }, lastPos = { x: 0, y: 0 },
lastTime = performance.now(); //The time at which the last point was drawn lastTime = performance.now(); //The time at which the last point was drawn
function start(x, y, evt) { function start(x, y, evt) {
//Prevent the press from being interpreted by the browser
evt.preventDefault();
//Prevent the press from being interpreted by the browser curUpdate.id = Tools.generateUID("e"); //"e" for ellipse
evt.preventDefault();
curUpdate.id = Tools.generateUID("e"); //"e" for ellipse Tools.drawAndSend({
type: "ellipse",
id: curUpdate.id,
color: Tools.getColor(),
size: Tools.getSize(),
opacity: Tools.getOpacity(),
x: x,
y: y,
x2: x,
y2: y,
});
Tools.drawAndSend({ curUpdate.id = curUpdate.id;
'type': 'ellipse', curUpdate.x = x;
'id': curUpdate.id, curUpdate.y = y;
'color': Tools.getColor(), }
'size': Tools.getSize(),
'opacity': Tools.getOpacity(),
'x': x,
'y': y,
'x2': x,
'y2': y
});
curUpdate.id = curUpdate.id; function move(x, y, evt) {
curUpdate.x = x; if (!curUpdate.id) return; // Not currently drawing
curUpdate.y = y; if (evt) {
circleTool.secondary.active = circleTool.secondary.active || evt.shiftKey;
evt.preventDefault();
}
lastPos.x = x;
lastPos.y = y;
doUpdate();
}
function doUpdate(force) {
if (!curUpdate.id) return; // Not currently drawing
if (drawingCircle()) {
var x0 = curUpdate["x"],
y0 = curUpdate["y"];
var deltaX = lastPos.x - x0,
deltaY = lastPos.y - y0;
var diameter = Math.max(Math.abs(deltaX), Math.abs(deltaY));
curUpdate["x2"] = x0 + (deltaX > 0 ? diameter : -diameter);
curUpdate["y2"] = y0 + (deltaY > 0 ? diameter : -diameter);
} else {
curUpdate["x2"] = lastPos.x;
curUpdate["y2"] = lastPos.y;
} }
function move(x, y, evt) { if (performance.now() - lastTime > 70 || force) {
if (!curUpdate.id) return; // Not currently drawing Tools.drawAndSend(curUpdate);
if (evt) { lastTime = performance.now();
circleTool.secondary.active = circleTool.secondary.active || evt.shiftKey; } else {
evt.preventDefault(); draw(curUpdate);
}
}
function stop(x, y) {
lastPos.x = x;
lastPos.y = y;
doUpdate(true);
curUpdate.id = "";
}
function draw(data) {
Tools.drawingEvent = true;
switch (data.type) {
case "ellipse":
createShape(data);
break;
case "update":
var shape = svg.getElementById(data["id"]);
if (!shape) {
console.error(
"Ellipse: Hmmm... I received an update for a shape that has not been created (%s).",
data["id"],
);
createShape({
//create a new shape in order not to loose the points
id: data["id"],
x: data["x2"],
y: data["y2"],
});
} }
lastPos.x = x;
lastPos.y = y;
doUpdate();
}
function doUpdate(force) {
if (!curUpdate.id) return; // Not currently drawing
if (drawingCircle()) {
var x0 = curUpdate['x'], y0 = curUpdate['y'];
var deltaX = lastPos.x - x0, deltaY = lastPos.y - y0;
var diameter = Math.max(Math.abs(deltaX), Math.abs(deltaY));
curUpdate['x2'] = x0 + (deltaX > 0 ? diameter : -diameter);
curUpdate['y2'] = y0 + (deltaY > 0 ? diameter : -diameter);
} else {
curUpdate['x2'] = lastPos.x;
curUpdate['y2'] = lastPos.y;
}
if (performance.now() - lastTime > 70 || force) {
Tools.drawAndSend(curUpdate);
lastTime = performance.now();
} else {
draw(curUpdate);
}
}
function stop(x, y) {
lastPos.x = x;
lastPos.y = y;
doUpdate(true);
curUpdate.id = "";
}
function draw(data) {
Tools.drawingEvent = true;
switch (data.type) {
case "ellipse":
createShape(data);
break;
case "update":
var shape = svg.getElementById(data['id']);
if (!shape) {
console.error("Ellipse: Hmmm... I received an update for a shape that has not been created (%s).", data['id']);
createShape({ //create a new shape in order not to loose the points
"id": data['id'],
"x": data['x2'],
"y": data['y2']
});
}
updateShape(shape, data);
break;
default:
console.error("Ellipse: Draw instruction with unknown type. ", data);
break;
}
}
var svg = Tools.svg;
function createShape(data) {
//Creates a new shape on the canvas, or update a shape that already exists with new information
var shape = svg.getElementById(data.id) || Tools.createSVGElement("ellipse");
updateShape(shape, data); updateShape(shape, data);
shape.id = data.id; break;
//If some data is not provided, choose default value. The shape may be updated later default:
shape.setAttribute("stroke", data.color || "black"); console.error("Ellipse: Draw instruction with unknown type. ", data);
shape.setAttribute("stroke-width", data.size || 10); break;
shape.setAttribute("opacity", Math.max(0.1, Math.min(1, data.opacity)) || 1);
Tools.drawingArea.appendChild(shape);
return shape;
} }
}
function updateShape(shape, data) { var svg = Tools.svg;
shape.cx.baseVal.value = Math.round((data['x2'] + data['x']) / 2); function createShape(data) {
shape.cy.baseVal.value = Math.round((data['y2'] + data['y']) / 2); //Creates a new shape on the canvas, or update a shape that already exists with new information
shape.rx.baseVal.value = Math.abs(data['x2'] - data['x']) / 2; var shape =
shape.ry.baseVal.value = Math.abs(data['y2'] - data['y']) / 2; svg.getElementById(data.id) || Tools.createSVGElement("ellipse");
} updateShape(shape, data);
shape.id = data.id;
//If some data is not provided, choose default value. The shape may be updated later
shape.setAttribute("stroke", data.color || "black");
shape.setAttribute("stroke-width", data.size || 10);
shape.setAttribute(
"opacity",
Math.max(0.1, Math.min(1, data.opacity)) || 1,
);
Tools.drawingArea.appendChild(shape);
return shape;
}
function drawingCircle() { function updateShape(shape, data) {
return circleTool.secondary.active; shape.cx.baseVal.value = Math.round((data["x2"] + data["x"]) / 2);
} shape.cy.baseVal.value = Math.round((data["y2"] + data["y"]) / 2);
shape.rx.baseVal.value = Math.abs(data["x2"] - data["x"]) / 2;
shape.ry.baseVal.value = Math.abs(data["y2"] - data["y"]) / 2;
}
var circleTool = { //The new tool function drawingCircle() {
"name": "Ellipse", return circleTool.secondary.active;
"icon": "tools/ellipse/icon-ellipse.svg", }
"secondary": {
"name": "Circle",
"icon": "tools/ellipse/icon-circle.svg",
"active": false,
"switch": doUpdate,
},
"shortcut": "c",
"listeners": {
"press": start,
"move": move,
"release": stop,
},
"draw": draw,
"mouseCursor": "crosshair",
"stylesheet": "tools/ellipse/ellipse.css"
};
Tools.add(circleTool);
var circleTool = {
//The new tool
name: "Ellipse",
icon: "tools/ellipse/icon-ellipse.svg",
secondary: {
name: "Circle",
icon: "tools/ellipse/icon-circle.svg",
active: false,
switch: doUpdate,
},
shortcut: "c",
listeners: {
press: start,
move: move,
release: stop,
},
draw: draw,
mouseCursor: "crosshair",
stylesheet: "tools/ellipse/ellipse.css",
};
Tools.add(circleTool);
})(); //End of code isolation })(); //End of code isolation

View file

@ -1,7 +1,7 @@
/** /**
* WHITEBOPHIR * WHITEBOPHIR
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2013 Ophir LOJKINE * Copyright (C) 2013 Ophir LOJKINE
@ -24,74 +24,83 @@
* @licend * @licend
*/ */
(function eraser() { //Code isolation (function eraser() {
//Code isolation
var erasing = false; var erasing = false;
function startErasing(x, y, evt) { function startErasing(x, y, evt) {
//Prevent the press from being interpreted by the browser //Prevent the press from being interpreted by the browser
evt.preventDefault(); evt.preventDefault();
erasing = true; erasing = true;
erase(x, y, evt); erase(x, y, evt);
} }
var msg = { var msg = {
"type": "delete", type: "delete",
"id": "" id: "",
}; };
function inDrawingArea(elem) { function inDrawingArea(elem) {
return Tools.drawingArea.contains(elem); return Tools.drawingArea.contains(elem);
} }
function erase(x, y, evt) { function erase(x, y, evt) {
// evt.target should be the element over which the mouse is... // evt.target should be the element over which the mouse is...
var target = evt.target; var target = evt.target;
if (evt.type === "touchmove") { if (evt.type === "touchmove") {
// ... the target of touchmove events is the element that was initially touched, // ... the target of touchmove events is the element that was initially touched,
// not the one **currently** being touched // not the one **currently** being touched
var touch = evt.touches[0]; var touch = evt.touches[0];
target = document.elementFromPoint(touch.clientX, touch.clientY); target = document.elementFromPoint(touch.clientX, touch.clientY);
} }
if (erasing && target !== Tools.svg && target !== Tools.drawingArea && inDrawingArea(target)) { if (
msg.id = target.id; erasing &&
Tools.drawAndSend(msg); target !== Tools.svg &&
} target !== Tools.drawingArea &&
} inDrawingArea(target)
) {
msg.id = target.id;
Tools.drawAndSend(msg);
}
}
function stopErasing() { function stopErasing() {
erasing = false; erasing = false;
} }
function draw(data) { function draw(data) {
var elem; var elem;
switch (data.type) { switch (data.type) {
//TODO: add the ability to erase only some points in a line //TODO: add the ability to erase only some points in a line
case "delete": case "delete":
elem = svg.getElementById(data.id); elem = svg.getElementById(data.id);
if (elem === null) console.error("Eraser: Tried to delete an element that does not exist."); if (elem === null)
else Tools.drawingArea.removeChild(elem); console.error(
break; "Eraser: Tried to delete an element that does not exist.",
default: );
console.error("Eraser: 'delete' instruction with unknown type. ", data); else Tools.drawingArea.removeChild(elem);
break; break;
} default:
} console.error("Eraser: 'delete' instruction with unknown type. ", data);
break;
}
}
var svg = Tools.svg; var svg = Tools.svg;
Tools.add({ //The new tool
"name": "Eraser",
"shortcut": "e",
"listeners": {
"press": startErasing,
"move": erase,
"release": stopErasing,
},
"draw": draw,
"icon": "tools/eraser/icon.svg",
"mouseCursor": "crosshair",
"showMarker": true,
});
Tools.add({
//The new tool
name: "Eraser",
shortcut: "e",
listeners: {
press: startErasing,
move: erase,
release: stopErasing,
},
draw: draw,
icon: "tools/eraser/icon.svg",
mouseCursor: "crosshair",
showMarker: true,
});
})(); //End of code isolation })(); //End of code isolation

View file

@ -24,95 +24,102 @@
* @licend * @licend
*/ */
(function grid() { //Code isolation (function grid() {
//Code isolation
var index = 0; //grid off by default var index = 0; //grid off by default
var states = ["none", "url(#grid)", "url(#dots)"]; var states = ["none", "url(#grid)", "url(#dots)"];
function toggleGrid(evt) { function toggleGrid(evt) {
index = (index + 1) % states.length; index = (index + 1) % states.length;
gridContainer.setAttributeNS(null, "fill", states[index]); gridContainer.setAttributeNS(null, "fill", states[index]);
} }
function createPatterns() { function createPatterns() {
// create patterns // create patterns
// small (inner) grid // small (inner) grid
var smallGrid = Tools.createSVGElement("pattern", { var smallGrid = Tools.createSVGElement("pattern", {
id: "smallGrid", id: "smallGrid",
width: "30", width: "30",
height: "30", height: "30",
patternUnits: "userSpaceOnUse" patternUnits: "userSpaceOnUse",
});
smallGrid.appendChild(
Tools.createSVGElement("path", {
d: "M 30 0 L 0 0 0 30",
fill: "none",
stroke: "gray",
'stroke-width': "0.5"
})
);
// (outer) grid
var grid = Tools.createSVGElement("pattern", {
id: "grid",
width: "300",
height: "300",
patternUnits: "userSpaceOnUse"
});
grid.appendChild(Tools.createSVGElement("rect", {
width: "300",
height: "300",
fill: "url(#smallGrid)"
}));
grid.appendChild(
Tools.createSVGElement("path", {
d: "M 300 0 L 0 0 0 300",
fill: "none",
stroke: "gray", 'stroke-width': "1"
})
);
// dots
var dots = Tools.createSVGElement("pattern", {
id: "dots",
width: "30",
height: "30",
x: "-10",
y: "-10",
patternUnits: "userSpaceOnUse"
});
dots.appendChild(Tools.createSVGElement("circle", {
fill: "gray",
cx: "10",
cy: "10",
r: "2"
}));
var defs = Tools.svg.getElementById("defs");
defs.appendChild(smallGrid);
defs.appendChild(grid);
defs.appendChild(dots);
}
var gridContainer = (function init() {
// initialize patterns
createPatterns();
// create grid container
var gridContainer = Tools.createSVGElement("rect", {
id: "gridContainer",
width: "100%", height: "100%",
fill: states[index]
});
Tools.svg.insertBefore(gridContainer, Tools.drawingArea);
return gridContainer;
})();
Tools.add({ //The new tool
"name": "Grid",
"shortcut": "g",
"listeners": {},
"icon": "tools/grid/icon.svg",
"oneTouch": true,
"onstart": toggleGrid,
"mouseCursor": "crosshair",
}); });
smallGrid.appendChild(
Tools.createSVGElement("path", {
d: "M 30 0 L 0 0 0 30",
fill: "none",
stroke: "gray",
"stroke-width": "0.5",
}),
);
// (outer) grid
var grid = Tools.createSVGElement("pattern", {
id: "grid",
width: "300",
height: "300",
patternUnits: "userSpaceOnUse",
});
grid.appendChild(
Tools.createSVGElement("rect", {
width: "300",
height: "300",
fill: "url(#smallGrid)",
}),
);
grid.appendChild(
Tools.createSVGElement("path", {
d: "M 300 0 L 0 0 0 300",
fill: "none",
stroke: "gray",
"stroke-width": "1",
}),
);
// dots
var dots = Tools.createSVGElement("pattern", {
id: "dots",
width: "30",
height: "30",
x: "-10",
y: "-10",
patternUnits: "userSpaceOnUse",
});
dots.appendChild(
Tools.createSVGElement("circle", {
fill: "gray",
cx: "10",
cy: "10",
r: "2",
}),
);
})(); //End of code isolation var defs = Tools.svg.getElementById("defs");
defs.appendChild(smallGrid);
defs.appendChild(grid);
defs.appendChild(dots);
}
var gridContainer = (function init() {
// initialize patterns
createPatterns();
// create grid container
var gridContainer = Tools.createSVGElement("rect", {
id: "gridContainer",
width: "100%",
height: "100%",
fill: states[index],
});
Tools.svg.insertBefore(gridContainer, Tools.drawingArea);
return gridContainer;
})();
Tools.add({
//The new tool
name: "Grid",
shortcut: "g",
listeners: {},
icon: "tools/grid/icon.svg",
oneTouch: true,
onstart: toggleGrid,
mouseCursor: "crosshair",
});
})(); //End of code isolation

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
line { line {
fill: none; fill: none;
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
} }

View file

@ -1,7 +1,7 @@
/** /**
* WHITEBOPHIR * WHITEBOPHIR
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2013 Ophir LOJKINE * Copyright (C) 2013 Ophir LOJKINE
@ -24,127 +24,138 @@
* @licend * @licend
*/ */
(function () { //Code isolation (function () {
//Indicates the id of the line the user is currently drawing or an empty string while the user is not drawing //Code isolation
var curLine = null, //Indicates the id of the line the user is currently drawing or an empty string while the user is not drawing
lastTime = performance.now(); //The time at which the last point was drawn var curLine = null,
lastTime = performance.now(); //The time at which the last point was drawn
//The data of the message that will be sent for every update //The data of the message that will be sent for every update
function UpdateMessage(x, y) { function UpdateMessage(x, y) {
this.type = 'update'; this.type = "update";
this.id = curLine.id; this.id = curLine.id;
this.x2 = x; this.x2 = x;
this.y2 = y; this.y2 = y;
} }
function startLine(x, y, evt) { function startLine(x, y, evt) {
//Prevent the press from being interpreted by the browser
evt.preventDefault();
//Prevent the press from being interpreted by the browser curLine = {
evt.preventDefault(); type: "straight",
id: Tools.generateUID("s"), //"s" for straight line
color: Tools.getColor(),
size: Tools.getSize(),
opacity: Tools.getOpacity(),
x: x,
y: y,
};
curLine = { Tools.drawAndSend(curLine);
'type': 'straight', }
'id': Tools.generateUID("s"), //"s" for straight line
'color': Tools.getColor(),
'size': Tools.getSize(),
'opacity': Tools.getOpacity(),
'x': x,
'y': y
}
Tools.drawAndSend(curLine); function continueLine(x, y, evt) {
} /*Wait 70ms before adding any point to the currently drawing line.
function continueLine(x, y, evt) {
/*Wait 70ms before adding any point to the currently drawing line.
This allows the animation to be smother*/ This allows the animation to be smother*/
if (curLine !== null) { if (curLine !== null) {
if (lineTool.secondary.active) { if (lineTool.secondary.active) {
var alpha = Math.atan2(y - curLine.y, x - curLine.x); var alpha = Math.atan2(y - curLine.y, x - curLine.x);
var d = Math.hypot(y - curLine.y, x - curLine.x); var d = Math.hypot(y - curLine.y, x - curLine.x);
var increment = 2 * Math.PI / 16; var increment = (2 * Math.PI) / 16;
alpha = Math.round(alpha / increment) * increment; alpha = Math.round(alpha / increment) * increment;
x = curLine.x + d * Math.cos(alpha); x = curLine.x + d * Math.cos(alpha);
y = curLine.y + d * Math.sin(alpha); y = curLine.y + d * Math.sin(alpha);
} }
if (performance.now() - lastTime > 70) { if (performance.now() - lastTime > 70) {
Tools.drawAndSend(new UpdateMessage(x, y)); Tools.drawAndSend(new UpdateMessage(x, y));
lastTime = performance.now(); lastTime = performance.now();
} else { } else {
draw(new UpdateMessage(x, y)); draw(new UpdateMessage(x, y));
} }
} }
if (evt) evt.preventDefault(); if (evt) evt.preventDefault();
} }
function stopLine(x, y) { function stopLine(x, y) {
//Add a last point to the line //Add a last point to the line
continueLine(x, y); continueLine(x, y);
curLine = null; curLine = null;
} }
function draw(data) { function draw(data) {
switch (data.type) { switch (data.type) {
case "straight": case "straight":
createLine(data); createLine(data);
break; break;
case "update": case "update":
var line = svg.getElementById(data['id']); var line = svg.getElementById(data["id"]);
if (!line) { if (!line) {
console.error("Straight line: Hmmm... I received a point of a line that has not been created (%s).", data['id']); console.error(
createLine({ //create a new line in order not to loose the points "Straight line: Hmmm... I received a point of a line that has not been created (%s).",
"id": data['id'], data["id"],
"x": data['x2'], );
"y": data['y2'] createLine({
}); //create a new line in order not to loose the points
} id: data["id"],
updateLine(line, data); x: data["x2"],
break; y: data["y2"],
default: });
console.error("Straight Line: Draw instruction with unknown type. ", data); }
break; updateLine(line, data);
} break;
} default:
console.error(
"Straight Line: Draw instruction with unknown type. ",
data,
);
break;
}
}
var svg = Tools.svg; var svg = Tools.svg;
function createLine(lineData) { function createLine(lineData) {
//Creates a new line on the canvas, or update a line that already exists with new information //Creates a new line on the canvas, or update a line that already exists with new information
var line = svg.getElementById(lineData.id) || Tools.createSVGElement("line"); var line =
line.id = lineData.id; svg.getElementById(lineData.id) || Tools.createSVGElement("line");
line.x1.baseVal.value = lineData['x']; line.id = lineData.id;
line.y1.baseVal.value = lineData['y']; line.x1.baseVal.value = lineData["x"];
line.x2.baseVal.value = lineData['x2'] || lineData['x']; line.y1.baseVal.value = lineData["y"];
line.y2.baseVal.value = lineData['y2'] || lineData['y']; line.x2.baseVal.value = lineData["x2"] || lineData["x"];
//If some data is not provided, choose default value. The line may be updated later line.y2.baseVal.value = lineData["y2"] || lineData["y"];
line.setAttribute("stroke", lineData.color || "black"); //If some data is not provided, choose default value. The line may be updated later
line.setAttribute("stroke-width", lineData.size || 10); line.setAttribute("stroke", lineData.color || "black");
line.setAttribute("opacity", Math.max(0.1, Math.min(1, lineData.opacity)) || 1); line.setAttribute("stroke-width", lineData.size || 10);
Tools.drawingArea.appendChild(line); line.setAttribute(
return line; "opacity",
} Math.max(0.1, Math.min(1, lineData.opacity)) || 1,
);
Tools.drawingArea.appendChild(line);
return line;
}
function updateLine(line, data) { function updateLine(line, data) {
line.x2.baseVal.value = data['x2']; line.x2.baseVal.value = data["x2"];
line.y2.baseVal.value = data['y2']; line.y2.baseVal.value = data["y2"];
} }
var lineTool = { var lineTool = {
"name": "Straight line", name: "Straight line",
"shortcut": "l", shortcut: "l",
"listeners": { listeners: {
"press": startLine, press: startLine,
"move": continueLine, move: continueLine,
"release": stopLine, release: stopLine,
}, },
"secondary": { secondary: {
"name": "Straight line", name: "Straight line",
"icon": "tools/line/icon-straight.svg", icon: "tools/line/icon-straight.svg",
"active": false, active: false,
}, },
"draw": draw, draw: draw,
"mouseCursor": "crosshair", mouseCursor: "crosshair",
"icon": "tools/line/icon.svg", icon: "tools/line/icon.svg",
"stylesheet": "tools/line/line.css" stylesheet: "tools/line/line.css",
}; };
Tools.add(lineTool); Tools.add(lineTool);
})(); //End of code isolation })(); //End of code isolation

View file

@ -1,5 +1,5 @@
path { path {
fill: none; fill: none;
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
} }

View file

@ -1,7 +1,7 @@
/** /**
* WHITEBOPHIR * WHITEBOPHIR
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2013 Ophir LOJKINE * Copyright (C) 2013 Ophir LOJKINE
@ -24,208 +24,222 @@
* @licend * @licend
*/ */
(function () { //Code isolation (function () {
//Code isolation
// Allocate the full maximum server update rate to pencil messages. // Allocate the full maximum server update rate to pencil messages.
// This feels a bit risky in terms of dropped messages, but any less // This feels a bit risky in terms of dropped messages, but any less
// gives terrible results with the default parameters. In practice it // gives terrible results with the default parameters. In practice it
// seems to work, either because writing tends to happen in bursts, or // seems to work, either because writing tends to happen in bursts, or
// maybe because the messages are sent when the time interval is *greater* // maybe because the messages are sent when the time interval is *greater*
// than this? // than this?
var MIN_PENCIL_INTERVAL_MS = Tools.server_config.MAX_EMIT_COUNT_PERIOD / Tools.server_config.MAX_EMIT_COUNT; var MIN_PENCIL_INTERVAL_MS =
Tools.server_config.MAX_EMIT_COUNT_PERIOD /
Tools.server_config.MAX_EMIT_COUNT;
var AUTO_FINGER_WHITEOUT = Tools.server_config.AUTO_FINGER_WHITEOUT; var AUTO_FINGER_WHITEOUT = Tools.server_config.AUTO_FINGER_WHITEOUT;
var hasUsedStylus = false; var hasUsedStylus = false;
//Indicates the id of the line the user is currently drawing or an empty string while the user is not drawing //Indicates the id of the line the user is currently drawing or an empty string while the user is not drawing
var curLineId = "", var curLineId = "",
lastTime = performance.now(); //The time at which the last point was drawn lastTime = performance.now(); //The time at which the last point was drawn
//The data of the message that will be sent for every new point //The data of the message that will be sent for every new point
function PointMessage(x, y) { function PointMessage(x, y) {
this.type = 'child'; this.type = "child";
this.parent = curLineId; this.parent = curLineId;
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
function handleAutoWhiteOut(evt) { function handleAutoWhiteOut(evt) {
if (evt.touches && evt.touches[0] && evt.touches[0].touchType == "stylus") { if (evt.touches && evt.touches[0] && evt.touches[0].touchType == "stylus") {
//When using stylus, switch back to the primary //When using stylus, switch back to the primary
if (hasUsedStylus && Tools.curTool.secondary.active) { if (hasUsedStylus && Tools.curTool.secondary.active) {
Tools.change("Pencil"); Tools.change("Pencil");
} }
//Remember if starting a line with a stylus //Remember if starting a line with a stylus
hasUsedStylus = true; hasUsedStylus = true;
} }
if (evt.touches && evt.touches[0] && evt.touches[0].touchType == "direct") { if (evt.touches && evt.touches[0] && evt.touches[0].touchType == "direct") {
//When used stylus and touched with a finger, switch to secondary //When used stylus and touched with a finger, switch to secondary
if (hasUsedStylus && !Tools.curTool.secondary.active) { if (hasUsedStylus && !Tools.curTool.secondary.active) {
Tools.change("Pencil"); Tools.change("Pencil");
} }
} }
} }
function startLine(x, y, evt) { function startLine(x, y, evt) {
//Prevent the press from being interpreted by the browser
evt.preventDefault();
//Prevent the press from being interpreted by the browser if (AUTO_FINGER_WHITEOUT) handleAutoWhiteOut(evt);
evt.preventDefault();
if (AUTO_FINGER_WHITEOUT) handleAutoWhiteOut(evt); curLineId = Tools.generateUID("l"); //"l" for line
curLineId = Tools.generateUID("l"); //"l" for line Tools.drawAndSend({
type: "line",
id: curLineId,
color: pencilTool.secondary.active ? "#ffffff" : Tools.getColor(),
size: Tools.getSize(),
opacity: pencilTool.secondary.active ? 1 : Tools.getOpacity(),
});
Tools.drawAndSend({ //Immediatly add a point to the line
'type': 'line', continueLine(x, y);
'id': curLineId, }
'color': (pencilTool.secondary.active ? "#ffffff" : Tools.getColor()),
'size': Tools.getSize(),
'opacity': (pencilTool.secondary.active ? 1 : Tools.getOpacity()),
});
//Immediatly add a point to the line function continueLine(x, y, evt) {
continueLine(x, y); /*Wait 70ms before adding any point to the currently drawing line.
}
function continueLine(x, y, evt) {
/*Wait 70ms before adding any point to the currently drawing line.
This allows the animation to be smother*/ This allows the animation to be smother*/
if (curLineId !== "" && performance.now() - lastTime > MIN_PENCIL_INTERVAL_MS) { if (
Tools.drawAndSend(new PointMessage(x, y)); curLineId !== "" &&
lastTime = performance.now(); performance.now() - lastTime > MIN_PENCIL_INTERVAL_MS
} ) {
if (evt) evt.preventDefault(); Tools.drawAndSend(new PointMessage(x, y));
} lastTime = performance.now();
}
if (evt) evt.preventDefault();
}
function stopLineAt(x, y) { function stopLineAt(x, y) {
//Add a last point to the line //Add a last point to the line
continueLine(x, y); continueLine(x, y);
stopLine(); stopLine();
} }
function stopLine() { function stopLine() {
curLineId = ""; curLineId = "";
} }
var renderingLine = {}; var renderingLine = {};
function draw(data) { function draw(data) {
Tools.drawingEvent = true; Tools.drawingEvent = true;
switch (data.type) { switch (data.type) {
case "line": case "line":
renderingLine = createLine(data); renderingLine = createLine(data);
break; break;
case "child": case "child":
var line = (renderingLine.id === data.parent) ? renderingLine : svg.getElementById(data.parent); var line =
if (!line) { renderingLine.id === data.parent
console.error("Pencil: Hmmm... I received a point of a line that has not been created (%s).", data.parent); ? renderingLine
line = renderingLine = createLine({ "id": data.parent }); //create a new line in order not to loose the points : svg.getElementById(data.parent);
} if (!line) {
addPoint(line, data.x, data.y); console.error(
break; "Pencil: Hmmm... I received a point of a line that has not been created (%s).",
case "endline": data.parent,
//TODO? );
break; line = renderingLine = createLine({ id: data.parent }); //create a new line in order not to loose the points
default: }
console.error("Pencil: Draw instruction with unknown type. ", data); addPoint(line, data.x, data.y);
break; break;
} case "endline":
} //TODO?
break;
default:
console.error("Pencil: Draw instruction with unknown type. ", data);
break;
}
}
var pathDataCache = {}; var pathDataCache = {};
function getPathData(line) { function getPathData(line) {
var pathData = pathDataCache[line.id]; var pathData = pathDataCache[line.id];
if (!pathData) { if (!pathData) {
pathData = line.getPathData(); pathData = line.getPathData();
pathDataCache[line.id] = pathData; pathDataCache[line.id] = pathData;
} }
return pathData; return pathData;
} }
var svg = Tools.svg; var svg = Tools.svg;
function addPoint(line, x, y) { function addPoint(line, x, y) {
var pts = getPathData(line); var pts = getPathData(line);
pts = wboPencilPoint(pts, x, y); pts = wboPencilPoint(pts, x, y);
line.setPathData(pts); line.setPathData(pts);
} }
function createLine(lineData) { function createLine(lineData) {
//Creates a new line on the canvas, or update a line that already exists with new information //Creates a new line on the canvas, or update a line that already exists with new information
var line = svg.getElementById(lineData.id) || Tools.createSVGElement("path"); var line =
line.id = lineData.id; svg.getElementById(lineData.id) || Tools.createSVGElement("path");
//If some data is not provided, choose default value. The line may be updated later line.id = lineData.id;
line.setAttribute("stroke", lineData.color || "black"); //If some data is not provided, choose default value. The line may be updated later
line.setAttribute("stroke-width", lineData.size || 10); line.setAttribute("stroke", lineData.color || "black");
line.setAttribute("opacity", Math.max(0.1, Math.min(1, lineData.opacity)) || 1); line.setAttribute("stroke-width", lineData.size || 10);
Tools.drawingArea.appendChild(line); line.setAttribute(
return line; "opacity",
} Math.max(0.1, Math.min(1, lineData.opacity)) || 1,
);
Tools.drawingArea.appendChild(line);
return line;
}
//Remember drawing and white-out sizes separately //Remember drawing and white-out sizes separately
var drawingSize = -1; var drawingSize = -1;
var whiteOutSize = -1; var whiteOutSize = -1;
function restoreDrawingSize() { function restoreDrawingSize() {
whiteOutSize = Tools.getSize(); whiteOutSize = Tools.getSize();
if (drawingSize != -1) { if (drawingSize != -1) {
Tools.setSize(drawingSize); Tools.setSize(drawingSize);
} }
} }
function restoreWhiteOutSize() { function restoreWhiteOutSize() {
drawingSize = Tools.getSize(); drawingSize = Tools.getSize();
if (whiteOutSize != -1) { if (whiteOutSize != -1) {
Tools.setSize(whiteOutSize); Tools.setSize(whiteOutSize);
} }
} }
//Restore remembered size after switch //Restore remembered size after switch
function toggleSize() { function toggleSize() {
if (pencilTool.secondary.active) { if (pencilTool.secondary.active) {
restoreWhiteOutSize(); restoreWhiteOutSize();
} else { } else {
restoreDrawingSize(); restoreDrawingSize();
} }
} }
var pencilTool = {
"name": "Pencil",
"shortcut": "p",
"listeners": {
"press": startLine,
"move": continueLine,
"release": stopLineAt,
},
"draw": draw,
"onstart": function(oldTool) {
//Reset stylus
hasUsedStylus = false;
},
"secondary": {
"name": "White-out",
"icon": "tools/pencil/whiteout_tape.svg",
"active": false,
"switch": function() {
stopLine();
toggleSize();
},
},
"onstart": function() {
//When switching from another tool to white-out, restore white-out size
if (pencilTool.secondary.active) {
restoreWhiteOutSize();
}
},
"onquit": function() {
//When switching from white-out to another tool, restore drawing size
if (pencilTool.secondary.active) {
restoreDrawingSize();
}
},
"mouseCursor": "url('tools/pencil/cursor.svg'), crosshair",
"icon": "tools/pencil/icon.svg",
"stylesheet": "tools/pencil/pencil.css",
};
Tools.add(pencilTool);
var pencilTool = {
name: "Pencil",
shortcut: "p",
listeners: {
press: startLine,
move: continueLine,
release: stopLineAt,
},
draw: draw,
onstart: function (oldTool) {
//Reset stylus
hasUsedStylus = false;
},
secondary: {
name: "White-out",
icon: "tools/pencil/whiteout_tape.svg",
active: false,
switch: function () {
stopLine();
toggleSize();
},
},
onstart: function () {
//When switching from another tool to white-out, restore white-out size
if (pencilTool.secondary.active) {
restoreWhiteOutSize();
}
},
onquit: function () {
//When switching from white-out to another tool, restore drawing size
if (pencilTool.secondary.active) {
restoreDrawingSize();
}
},
mouseCursor: "url('tools/pencil/cursor.svg'), crosshair",
icon: "tools/pencil/icon.svg",
stylesheet: "tools/pencil/pencil.css",
};
Tools.add(pencilTool);
})(); //End of code isolation })(); //End of code isolation

View file

@ -1,93 +1,91 @@
(function (global) { (function (global) {
"use strict"; "use strict";
function dist(x1, y1, x2, y2) { function dist(x1, y1, x2, y2) {
//Returns the distance between (x1,y1) and (x2,y2) //Returns the distance between (x1,y1) and (x2,y2)
return Math.hypot(x2 - x1, y2 - y1); return Math.hypot(x2 - x1, y2 - y1);
} }
/** /**
* Represents a single operation in an SVG path * Represents a single operation in an SVG path
* @param {string} type * @param {string} type
* @param {number[]} values * @param {number[]} values
*/ */
function PathDataPoint(type, values) { function PathDataPoint(type, values) {
this.type = type; this.type = type;
this.values = values; this.values = values;
} }
/** /**
* Given the existing points in a path, add a new point to get a smoothly interpolated path * Given the existing points in a path, add a new point to get a smoothly interpolated path
* @param {PathDataPoint[]} pts * @param {PathDataPoint[]} pts
* @param {number} x * @param {number} x
* @param {number} y * @param {number} y
*/ */
function wboPencilPoint(pts, x, y) { function wboPencilPoint(pts, x, y) {
// pts represents the points that are already in the line as a PathData // pts represents the points that are already in the line as a PathData
var nbr = pts.length; //The number of points already in the line var nbr = pts.length; //The number of points already in the line
var npoint; var npoint;
switch (nbr) { switch (nbr) {
case 0: //The first point in the line case 0: //The first point in the line
//If there is no point, we have to start the line with a moveTo statement //If there is no point, we have to start the line with a moveTo statement
pts.push(new PathDataPoint("M", [x, y])); pts.push(new PathDataPoint("M", [x, y]));
//Temporary first point so that clicks are shown and can be erased //Temporary first point so that clicks are shown and can be erased
npoint = new PathDataPoint("L", [x, y]); npoint = new PathDataPoint("L", [x, y]);
break; break;
case 1: //This should never happen case 1: //This should never happen
// First point will be the move. Add Line of zero length ensure there are two points and fall through // First point will be the move. Add Line of zero length ensure there are two points and fall through
pts.push(new PathDataPoint("L", [pts[0].values[0], pts[0].values[1]])); pts.push(new PathDataPoint("L", [pts[0].values[0], pts[0].values[1]]));
// noinspection FallThroughInSwitchStatementJS // noinspection FallThroughInSwitchStatementJS
case 2: //There are two points. The initial move and a line of zero length to make it visible case 2: //There are two points. The initial move and a line of zero length to make it visible
//Draw a curve that is segment between the old point and the new one //Draw a curve that is segment between the old point and the new one
npoint = new PathDataPoint("C", [ npoint = new PathDataPoint("C", [
pts[0].values[0], pts[0].values[1], pts[0].values[0],
x, y, pts[0].values[1],
x, y, x,
]); y,
break; x,
default: //There are at least two points in the line y,
npoint = pencilExtrapolatePoints(pts, x, y);
}
if (npoint) pts.push(npoint);
return pts;
}
function pencilExtrapolatePoints(pts, x, y) {
//We add the new point, and smoothen the line
var ANGULARITY = 3; //The lower this number, the smoother the line
var prev_values = pts[pts.length - 1].values; // Previous point
var ante_values = pts[pts.length - 2].values; // Point before the previous one
var prev_x = prev_values[prev_values.length - 2];
var prev_y = prev_values[prev_values.length - 1];
var ante_x = ante_values[ante_values.length - 2];
var ante_y = ante_values[ante_values.length - 1];
//We don't want to add the same point twice consecutively
if ((prev_x === x && prev_y === y)
|| (ante_x === x && ante_y === y)) return;
var vectx = x - ante_x,
vecty = y - ante_y;
var norm = Math.hypot(vectx, vecty);
var dist1 = dist(ante_x, ante_y, prev_x, prev_y) / norm,
dist2 = dist(x, y, prev_x, prev_y) / norm;
vectx /= ANGULARITY;
vecty /= ANGULARITY;
//Create 2 control points around the last point
var cx1 = prev_x - dist1 * vectx,
cy1 = prev_y - dist1 * vecty, //First control point
cx2 = prev_x + dist2 * vectx,
cy2 = prev_y + dist2 * vecty; //Second control point
prev_values[2] = cx1;
prev_values[3] = cy1;
return new PathDataPoint("C", [
cx2, cy2,
x, y,
x, y,
]); ]);
break;
default: //There are at least two points in the line
npoint = pencilExtrapolatePoints(pts, x, y);
} }
if (npoint) pts.push(npoint);
return pts;
}
global["wboPencilPoint"] = wboPencilPoint; function pencilExtrapolatePoints(pts, x, y) {
})("object" === typeof module && module.exports || window); //We add the new point, and smoothen the line
var ANGULARITY = 3; //The lower this number, the smoother the line
var prev_values = pts[pts.length - 1].values; // Previous point
var ante_values = pts[pts.length - 2].values; // Point before the previous one
var prev_x = prev_values[prev_values.length - 2];
var prev_y = prev_values[prev_values.length - 1];
var ante_x = ante_values[ante_values.length - 2];
var ante_y = ante_values[ante_values.length - 1];
//We don't want to add the same point twice consecutively
if ((prev_x === x && prev_y === y) || (ante_x === x && ante_y === y))
return;
var vectx = x - ante_x,
vecty = y - ante_y;
var norm = Math.hypot(vectx, vecty);
var dist1 = dist(ante_x, ante_y, prev_x, prev_y) / norm,
dist2 = dist(x, y, prev_x, prev_y) / norm;
vectx /= ANGULARITY;
vecty /= ANGULARITY;
//Create 2 control points around the last point
var cx1 = prev_x - dist1 * vectx,
cy1 = prev_y - dist1 * vecty, //First control point
cx2 = prev_x + dist2 * vectx,
cy2 = prev_y + dist2 * vecty; //Second control point
prev_values[2] = cx1;
prev_values[3] = cy1;
return new PathDataPoint("C", [cx2, cy2, x, y, x, y]);
}
global["wboPencilPoint"] = wboPencilPoint;
})(("object" === typeof module && module.exports) || window);

View file

@ -1,3 +1,3 @@
#drawingArea rect { #drawingArea rect {
fill: none; fill: none;
} }

View file

@ -24,137 +24,148 @@
* @licend * @licend
*/ */
(function () { //Code isolation (function () {
//Indicates the id of the shape the user is currently drawing or an empty string while the user is not drawing //Code isolation
var end = false, //Indicates the id of the shape the user is currently drawing or an empty string while the user is not drawing
curId = "", var end = false,
curUpdate = { //The data of the message that will be sent for every new point curId = "",
'type': 'update', curUpdate = {
'id': "", //The data of the message that will be sent for every new point
'x': 0, type: "update",
'y': 0, id: "",
'x2': 0, x: 0,
'y2': 0 y: 0,
}, x2: 0,
lastTime = performance.now(); //The time at which the last point was drawn y2: 0,
},
lastTime = performance.now(); //The time at which the last point was drawn
function start(x, y, evt) { function start(x, y, evt) {
//Prevent the press from being interpreted by the browser
evt.preventDefault();
//Prevent the press from being interpreted by the browser curId = Tools.generateUID("r"); //"r" for rectangle
evt.preventDefault();
curId = Tools.generateUID("r"); //"r" for rectangle Tools.drawAndSend({
type: "rect",
id: curId,
color: Tools.getColor(),
size: Tools.getSize(),
opacity: Tools.getOpacity(),
x: x,
y: y,
x2: x,
y2: y,
});
Tools.drawAndSend({ curUpdate.id = curId;
'type': 'rect', curUpdate.x = x;
'id': curId, curUpdate.y = y;
'color': Tools.getColor(), }
'size': Tools.getSize(),
'opacity': Tools.getOpacity(),
'x': x,
'y': y,
'x2': x,
'y2': y
});
curUpdate.id = curId; function move(x, y, evt) {
curUpdate.x = x; /*Wait 70ms before adding any point to the currently drawing shape.
curUpdate.y = y;
}
function move(x, y, evt) {
/*Wait 70ms before adding any point to the currently drawing shape.
This allows the animation to be smother*/ This allows the animation to be smother*/
if (curId !== "") { if (curId !== "") {
if (rectangleTool.secondary.active) { if (rectangleTool.secondary.active) {
var dx = x - curUpdate.x; var dx = x - curUpdate.x;
var dy = y - curUpdate.y; var dy = y - curUpdate.y;
var d = Math.max(Math.abs(dx), Math.abs(dy)); var d = Math.max(Math.abs(dx), Math.abs(dy));
x = curUpdate.x + (dx > 0 ? d : -d); x = curUpdate.x + (dx > 0 ? d : -d);
y = curUpdate.y + (dy > 0 ? d : -d); y = curUpdate.y + (dy > 0 ? d : -d);
} }
curUpdate['x2'] = x; curUpdate['y2'] = y; curUpdate["x2"] = x;
if (performance.now() - lastTime > 70 || end) { curUpdate["y2"] = y;
Tools.drawAndSend(curUpdate); if (performance.now() - lastTime > 70 || end) {
lastTime = performance.now(); Tools.drawAndSend(curUpdate);
} else { lastTime = performance.now();
draw(curUpdate); } else {
} draw(curUpdate);
} }
if (evt) evt.preventDefault(); }
} if (evt) evt.preventDefault();
}
function stop(x, y) { function stop(x, y) {
//Add a last point to the shape //Add a last point to the shape
end = true; end = true;
move(x, y); move(x, y);
end = false; end = false;
curId = ""; curId = "";
} }
function draw(data) { function draw(data) {
Tools.drawingEvent = true; Tools.drawingEvent = true;
switch (data.type) { switch (data.type) {
case "rect": case "rect":
createShape(data); createShape(data);
break; break;
case "update": case "update":
var shape = svg.getElementById(data['id']); var shape = svg.getElementById(data["id"]);
if (!shape) { if (!shape) {
console.error("Straight shape: Hmmm... I received a point of a rect that has not been created (%s).", data['id']); console.error(
createShape({ //create a new shape in order not to loose the points "Straight shape: Hmmm... I received a point of a rect that has not been created (%s).",
"id": data['id'], data["id"],
"x": data['x2'], );
"y": data['y2'] createShape({
}); //create a new shape in order not to loose the points
} id: data["id"],
updateShape(shape, data); x: data["x2"],
break; y: data["y2"],
default: });
console.error("Straight shape: Draw instruction with unknown type. ", data); }
break; updateShape(shape, data);
} break;
} default:
console.error(
"Straight shape: Draw instruction with unknown type. ",
data,
);
break;
}
}
var svg = Tools.svg; var svg = Tools.svg;
function createShape(data) { function createShape(data) {
//Creates a new shape on the canvas, or update a shape that already exists with new information //Creates a new shape on the canvas, or update a shape that already exists with new information
var shape = svg.getElementById(data.id) || Tools.createSVGElement("rect"); var shape = svg.getElementById(data.id) || Tools.createSVGElement("rect");
shape.id = data.id; shape.id = data.id;
updateShape(shape, data); updateShape(shape, data);
//If some data is not provided, choose default value. The shape may be updated later //If some data is not provided, choose default value. The shape may be updated later
shape.setAttribute("stroke", data.color || "black"); shape.setAttribute("stroke", data.color || "black");
shape.setAttribute("stroke-width", data.size || 10); shape.setAttribute("stroke-width", data.size || 10);
shape.setAttribute("opacity", Math.max(0.1, Math.min(1, data.opacity)) || 1); shape.setAttribute(
Tools.drawingArea.appendChild(shape); "opacity",
return shape; Math.max(0.1, Math.min(1, data.opacity)) || 1,
} );
Tools.drawingArea.appendChild(shape);
return shape;
}
function updateShape(shape, data) { function updateShape(shape, data) {
shape.x.baseVal.value = Math.min(data['x2'], data['x']); shape.x.baseVal.value = Math.min(data["x2"], data["x"]);
shape.y.baseVal.value = Math.min(data['y2'], data['y']); shape.y.baseVal.value = Math.min(data["y2"], data["y"]);
shape.width.baseVal.value = Math.abs(data['x2'] - data['x']); shape.width.baseVal.value = Math.abs(data["x2"] - data["x"]);
shape.height.baseVal.value = Math.abs(data['y2'] - data['y']); shape.height.baseVal.value = Math.abs(data["y2"] - data["y"]);
} }
var rectangleTool = {
"name": "Rectangle",
"shortcut": "r",
"listeners": {
"press": start,
"move": move,
"release": stop,
},
"secondary": {
"name": "Square",
"icon": "tools/rect/icon-square.svg",
"active": false,
},
"draw": draw,
"mouseCursor": "crosshair",
"icon": "tools/rect/icon.svg",
"stylesheet": "tools/rect/rect.css"
};
Tools.add(rectangleTool);
var rectangleTool = {
name: "Rectangle",
shortcut: "r",
listeners: {
press: start,
move: move,
release: stop,
},
secondary: {
name: "Square",
icon: "tools/rect/icon-square.svg",
active: false,
},
draw: draw,
mouseCursor: "crosshair",
icon: "tools/rect/icon.svg",
stylesheet: "tools/rect/rect.css",
};
Tools.add(rectangleTool);
})(); //End of code isolation })(); //End of code isolation

View file

@ -1,16 +1,15 @@
#textToolInput { #textToolInput {
position:fixed; position: fixed;
top:-1000px; /*Hidden*/ top: -1000px; /*Hidden*/
left: 80px; left: 80px;
width:500px; width: 500px;
} }
#textToolInput:focus { #textToolInput:focus {
top: 5px; top: 5px;
} }
text { text {
font-family:"Arial", "Helvetica", sans-serif; font-family: "Arial", "Helvetica", sans-serif;
user-select:none; user-select: none;
-moz-user-select:none; -moz-user-select: none;
} }

View file

@ -1,7 +1,7 @@
/** /**
* WHITEBOPHIR * WHITEBOPHIR
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2013 Ophir LOJKINE * Copyright (C) 2013 Ophir LOJKINE
@ -24,196 +24,212 @@
* @licend * @licend
*/ */
(function () { //Code isolation (function () {
var board = Tools.board; //Code isolation
var board = Tools.board;
var input = document.createElement("input"); var input = document.createElement("input");
input.id = "textToolInput"; input.id = "textToolInput";
input.type = "text"; input.type = "text";
input.setAttribute("autocomplete", "off"); input.setAttribute("autocomplete", "off");
var curText = { var curText = {
"x": 0, x: 0,
"y": 0, y: 0,
"size": 36, size: 36,
"rawSize": 16, rawSize: 16,
"oldSize": 0, oldSize: 0,
"opacity": 1, opacity: 1,
"color": "#000", color: "#000",
"id": 0, id: 0,
"sentText": "", sentText: "",
"lastSending": 0 lastSending: 0,
}; };
var active = false; var active = false;
function onStart() {
curText.oldSize = Tools.getSize();
Tools.setSize(curText.rawSize);
}
function onStart() { function onQuit() {
curText.oldSize = Tools.getSize(); stopEdit();
Tools.setSize(curText.rawSize); Tools.setSize(curText.oldSize);
} }
function onQuit() { function clickHandler(x, y, evt, isTouchEvent) {
stopEdit(); //if(document.querySelector("#menu").offsetWidth>Tools.menu_width+3) return;
Tools.setSize(curText.oldSize); if (evt.target === input) return;
} if (evt.target.tagName === "text") {
editOldText(evt.target);
evt.preventDefault();
return;
}
curText.rawSize = Tools.getSize();
curText.size = parseInt(curText.rawSize * 1.5 + 12);
curText.opacity = Tools.getOpacity();
curText.color = Tools.getColor();
curText.x = x;
curText.y = y + curText.size / 2;
function clickHandler(x, y, evt, isTouchEvent) { stopEdit();
//if(document.querySelector("#menu").offsetWidth>Tools.menu_width+3) return; startEdit();
if (evt.target === input) return; evt.preventDefault();
if (evt.target.tagName === "text") { }
editOldText(evt.target);
evt.preventDefault();
return;
}
curText.rawSize = Tools.getSize();
curText.size = parseInt(curText.rawSize * 1.5 + 12);
curText.opacity = Tools.getOpacity();
curText.color = Tools.getColor();
curText.x = x;
curText.y = y + curText.size / 2;
stopEdit(); function editOldText(elem) {
startEdit(); curText.id = elem.id;
evt.preventDefault(); var r = elem.getBoundingClientRect();
} var x = (r.left + document.documentElement.scrollLeft) / Tools.scale;
var y =
(r.top + r.height + document.documentElement.scrollTop) / Tools.scale;
function editOldText(elem) { curText.x = x;
curText.id = elem.id; curText.y = y;
var r = elem.getBoundingClientRect(); curText.sentText = elem.textContent;
var x = (r.left + document.documentElement.scrollLeft) / Tools.scale; curText.size = parseInt(elem.getAttribute("font-size"));
var y = (r.top + r.height + document.documentElement.scrollTop) / Tools.scale; curText.opacity = parseFloat(elem.getAttribute("opacity"));
curText.color = elem.getAttribute("fill");
startEdit();
input.value = elem.textContent;
}
curText.x = x; function startEdit() {
curText.y = y; active = true;
curText.sentText = elem.textContent; if (!input.parentNode) board.appendChild(input);
curText.size = parseInt(elem.getAttribute("font-size")); input.value = "";
curText.opacity = parseFloat(elem.getAttribute("opacity")); var left = curText.x - document.documentElement.scrollLeft + "px";
curText.color = elem.getAttribute("fill"); var clientW = Math.max(
startEdit(); document.documentElement.clientWidth,
input.value = elem.textContent; window.innerWidth || 0,
} );
var x = curText.x * Tools.scale - document.documentElement.scrollLeft;
if (x + 250 > clientW) {
x = Math.max(60, clientW - 260);
}
function startEdit() { input.style.left = x + "px";
active = true; input.style.top =
if (!input.parentNode) board.appendChild(input); curText.y * Tools.scale - document.documentElement.scrollTop + 20 + "px";
input.value = ""; input.focus();
var left = curText.x - document.documentElement.scrollLeft + 'px'; input.addEventListener("keyup", textChangeHandler);
var clientW = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); input.addEventListener("blur", textChangeHandler);
var x = curText.x * Tools.scale - document.documentElement.scrollLeft; input.addEventListener("blur", blur);
if (x + 250 > clientW) { }
x = Math.max(60, clientW - 260)
}
input.style.left = x + 'px'; function stopEdit() {
input.style.top = curText.y * Tools.scale - document.documentElement.scrollTop + 20 + 'px'; try {
input.focus(); input.blur();
input.addEventListener("keyup", textChangeHandler); } catch (e) {
input.addEventListener("blur", textChangeHandler); /* Internet Explorer */
input.addEventListener("blur", blur); }
} active = false;
blur();
curText.id = 0;
curText.sentText = "";
input.value = "";
input.removeEventListener("keyup", textChangeHandler);
}
function stopEdit() { function blur() {
try { input.blur(); } catch (e) { /* Internet Explorer */ } if (active) return;
active = false; input.style.top = "-1000px";
blur(); }
curText.id = 0;
curText.sentText = "";
input.value = "";
input.removeEventListener("keyup", textChangeHandler);
}
function blur() { function textChangeHandler(evt) {
if (active) return; if (evt.which === 13) {
input.style.top = '-1000px'; // enter
} curText.y += 1.5 * curText.size;
stopEdit();
startEdit();
} else if (evt.which === 27) {
// escape
stopEdit();
}
if (performance.now() - curText.lastSending > 100) {
if (curText.sentText !== input.value) {
//If the user clicked where there was no text, then create a new text field
if (curText.id === 0) {
curText.id = Tools.generateUID("t"); //"t" for text
Tools.drawAndSend({
type: "new",
id: curText.id,
color: curText.color,
size: curText.size,
opacity: curText.opacity,
x: curText.x,
y: curText.y,
});
}
Tools.drawAndSend({
type: "update",
id: curText.id,
txt: input.value.slice(0, 280),
});
curText.sentText = input.value;
curText.lastSending = performance.now();
}
} else {
clearTimeout(curText.timeout);
curText.timeout = setTimeout(textChangeHandler, 500, evt);
}
}
function textChangeHandler(evt) { function draw(data, isLocal) {
if (evt.which === 13) { // enter Tools.drawingEvent = true;
curText.y += 1.5 * curText.size; switch (data.type) {
stopEdit(); case "new":
startEdit(); createTextField(data);
} else if (evt.which === 27) { // escape break;
stopEdit(); case "update":
} var textField = document.getElementById(data.id);
if (performance.now() - curText.lastSending > 100) { if (textField === null) {
if (curText.sentText !== input.value) { console.error(
//If the user clicked where there was no text, then create a new text field "Text: Hmmm... I received text that belongs to an unknown text field",
if (curText.id === 0) { );
curText.id = Tools.generateUID("t"); //"t" for text return false;
Tools.drawAndSend({ }
'type': 'new', updateText(textField, data.txt);
'id': curText.id, break;
'color': curText.color, default:
'size': curText.size, console.error("Text: Draw instruction with unknown type. ", data);
'opacity': curText.opacity, break;
'x': curText.x, }
'y': curText.y }
})
}
Tools.drawAndSend({
'type': "update",
'id': curText.id,
'txt': input.value.slice(0, 280)
});
curText.sentText = input.value;
curText.lastSending = performance.now();
}
} else {
clearTimeout(curText.timeout);
curText.timeout = setTimeout(textChangeHandler, 500, evt);
}
}
function draw(data, isLocal) { function updateText(textField, text) {
Tools.drawingEvent = true; textField.textContent = text;
switch (data.type) { }
case "new":
createTextField(data);
break;
case "update":
var textField = document.getElementById(data.id);
if (textField === null) {
console.error("Text: Hmmm... I received text that belongs to an unknown text field");
return false;
}
updateText(textField, data.txt);
break;
default:
console.error("Text: Draw instruction with unknown type. ", data);
break;
}
}
function updateText(textField, text) { function createTextField(fieldData) {
textField.textContent = text; var elem = Tools.createSVGElement("text");
} elem.id = fieldData.id;
elem.setAttribute("x", fieldData.x);
function createTextField(fieldData) { elem.setAttribute("y", fieldData.y);
var elem = Tools.createSVGElement("text"); elem.setAttribute("font-size", fieldData.size);
elem.id = fieldData.id; elem.setAttribute("fill", fieldData.color);
elem.setAttribute("x", fieldData.x); elem.setAttribute(
elem.setAttribute("y", fieldData.y); "opacity",
elem.setAttribute("font-size", fieldData.size); Math.max(0.1, Math.min(1, fieldData.opacity)) || 1,
elem.setAttribute("fill", fieldData.color); );
elem.setAttribute("opacity", Math.max(0.1, Math.min(1, fieldData.opacity)) || 1); if (fieldData.txt) elem.textContent = fieldData.txt;
if (fieldData.txt) elem.textContent = fieldData.txt; Tools.drawingArea.appendChild(elem);
Tools.drawingArea.appendChild(elem); return elem;
return elem; }
}
Tools.add({ //The new tool
"name": "Text",
"shortcut": "t",
"listeners": {
"press": clickHandler,
},
"onstart": onStart,
"onquit": onQuit,
"draw": draw,
"stylesheet": "tools/text/text.css",
"icon": "tools/text/icon.svg",
"mouseCursor": "text"
});
Tools.add({
//The new tool
name: "Text",
shortcut: "t",
listeners: {
press: clickHandler,
},
onstart: onStart,
onquit: onQuit,
draw: draw,
stylesheet: "tools/text/text.css",
icon: "tools/text/icon.svg",
mouseCursor: "text",
});
})(); //End of code isolation })(); //End of code isolation

View file

@ -1,7 +1,7 @@
/** /**
* WHITEBOPHIR * WHITEBOPHIR
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2013 Ophir LOJKINE * Copyright (C) 2013 Ophir LOJKINE
@ -24,165 +24,182 @@
* @licend * @licend
*/ */
(function () { //Code isolation (function () {
var ZOOM_FACTOR = .5; //Code isolation
var origin = { var ZOOM_FACTOR = 0.5;
scrollX: document.documentElement.scrollLeft, var origin = {
scrollY: document.documentElement.scrollTop, scrollX: document.documentElement.scrollLeft,
x: 0.0, scrollY: document.documentElement.scrollTop,
y: 0.0, x: 0.0,
clientY: 0, y: 0.0,
scale: 1.0 clientY: 0,
}; scale: 1.0,
var moved = false, pressed = false; };
var moved = false,
pressed = false;
function zoom(origin, scale) { function zoom(origin, scale) {
var oldScale = origin.scale; var oldScale = origin.scale;
var newScale = Tools.setScale(scale); var newScale = Tools.setScale(scale);
window.scrollTo( window.scrollTo(
origin.scrollX + origin.x * (newScale - oldScale), origin.scrollX + origin.x * (newScale - oldScale),
origin.scrollY + origin.y * (newScale - oldScale) origin.scrollY + origin.y * (newScale - oldScale),
); );
}
var animation = null;
function animate(scale) {
cancelAnimationFrame(animation);
animation = requestAnimationFrame(function () {
zoom(origin, scale);
});
}
function setOrigin(x, y, evt, isTouchEvent) {
origin.scrollX = document.documentElement.scrollLeft;
origin.scrollY = document.documentElement.scrollTop;
origin.x = x;
origin.y = y;
origin.clientY = getClientY(evt, isTouchEvent);
origin.scale = Tools.getScale();
}
function press(x, y, evt, isTouchEvent) {
evt.preventDefault();
setOrigin(x, y, evt, isTouchEvent);
moved = false;
pressed = true;
}
function move(x, y, evt, isTouchEvent) {
if (pressed) {
evt.preventDefault();
var delta = getClientY(evt, isTouchEvent) - origin.clientY;
var scale = origin.scale * (1 + (delta * ZOOM_FACTOR) / 100);
if (Math.abs(delta) > 1) moved = true;
animation = animate(scale);
} }
}
var animation = null; function onwheel(evt) {
function animate(scale) { evt.preventDefault();
cancelAnimationFrame(animation); var multiplier =
animation = requestAnimationFrame(function () { evt.deltaMode === WheelEvent.DOM_DELTA_LINE
zoom(origin, scale); ? 30
}); : evt.deltaMode === WheelEvent.DOM_DELTA_PAGE
? 1000
: 1;
var deltaX = evt.deltaX * multiplier,
deltaY = evt.deltaY * multiplier;
if (!evt.ctrlKey) {
// zoom
var scale = Tools.getScale();
var x = evt.pageX / scale;
var y = evt.pageY / scale;
setOrigin(x, y, evt, false);
animate((1 - deltaY / 800) * Tools.getScale());
} else if (evt.altKey) {
// make finer changes if shift is being held
var change = evt.shiftKey ? 1 : 5;
// change tool size
Tools.setSize(Tools.getSize() - (deltaY / 100) * change);
} else if (evt.shiftKey) {
// scroll horizontally
window.scrollTo(
document.documentElement.scrollLeft + deltaY,
document.documentElement.scrollTop + deltaX,
);
} else {
// regular scrolling
window.scrollTo(
document.documentElement.scrollLeft + deltaX,
document.documentElement.scrollTop + deltaY,
);
} }
}
Tools.board.addEventListener("wheel", onwheel, { passive: false });
function setOrigin(x, y, evt, isTouchEvent) { Tools.board.addEventListener(
origin.scrollX = document.documentElement.scrollLeft; "touchmove",
origin.scrollY = document.documentElement.scrollTop; function ontouchmove(evt) {
origin.x = x; // 2-finger pan to zoom
origin.y = y; var touches = evt.touches;
origin.clientY = getClientY(evt, isTouchEvent); if (touches.length === 2) {
origin.scale = Tools.getScale(); var x0 = touches[0].clientX,
} x1 = touches[1].clientX,
y0 = touches[0].clientY,
function press(x, y, evt, isTouchEvent) { y1 = touches[1].clientY,
evt.preventDefault(); dx = x0 - x1,
setOrigin(x, y, evt, isTouchEvent); dy = y0 - y1;
moved = false; var x = (touches[0].pageX + touches[1].pageX) / 2 / Tools.getScale(),
pressed = true; y = (touches[0].pageY + touches[1].pageY) / 2 / Tools.getScale();
} var distance = Math.sqrt(dx * dx + dy * dy);
if (!pressed) {
function move(x, y, evt, isTouchEvent) { pressed = true;
if (pressed) { setOrigin(x, y, evt, true);
evt.preventDefault(); origin.distance = distance;
var delta = getClientY(evt, isTouchEvent) - origin.clientY;
var scale = origin.scale * (1 + delta * ZOOM_FACTOR / 100);
if (Math.abs(delta) > 1) moved = true;
animation = animate(scale);
}
}
function onwheel(evt) {
evt.preventDefault();
var multiplier =
(evt.deltaMode === WheelEvent.DOM_DELTA_LINE) ? 30 :
(evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) ? 1000 :
1;
var deltaX = evt.deltaX * multiplier, deltaY = evt.deltaY * multiplier;
if (!evt.ctrlKey) {
// zoom
var scale = Tools.getScale();
var x = evt.pageX / scale;
var y = evt.pageY / scale;
setOrigin(x, y, evt, false);
animate((1 - deltaY / 800) * Tools.getScale());
} else if (evt.altKey) {
// make finer changes if shift is being held
var change = evt.shiftKey ? 1 : 5;
// change tool size
Tools.setSize(Tools.getSize() - deltaY / 100 * change);
} else if (evt.shiftKey) {
// scroll horizontally
window.scrollTo(document.documentElement.scrollLeft + deltaY, document.documentElement.scrollTop + deltaX);
} else { } else {
// regular scrolling var delta = distance - origin.distance;
window.scrollTo(document.documentElement.scrollLeft + deltaX, document.documentElement.scrollTop + deltaY); var scale = origin.scale * (1 + (delta * ZOOM_FACTOR) / 100);
animate(scale);
} }
} }
Tools.board.addEventListener("wheel", onwheel, { passive: false }); },
{ passive: true },
);
function touchend() {
pressed = false;
}
Tools.board.addEventListener("touchend", touchend);
Tools.board.addEventListener("touchcancel", touchend);
Tools.board.addEventListener("touchmove", function ontouchmove(evt) { function release(x, y, evt, isTouchEvent) {
// 2-finger pan to zoom if (pressed && !moved) {
var touches = evt.touches; var delta = evt.shiftKey === true ? -1 : 1;
if (touches.length === 2) { var scale = Tools.getScale() * (1 + delta * ZOOM_FACTOR);
var x0 = touches[0].clientX, x1 = touches[1].clientX, zoom(origin, scale);
y0 = touches[0].clientY, y1 = touches[1].clientY,
dx = x0 - x1,
dy = y0 - y1;
var x = (touches[0].pageX + touches[1].pageX) / 2 / Tools.getScale(),
y = (touches[0].pageY + touches[1].pageY) / 2 / Tools.getScale();
var distance = Math.sqrt(dx * dx + dy * dy);
if (!pressed) {
pressed = true;
setOrigin(x, y, evt, true);
origin.distance = distance;
} else {
var delta = distance - origin.distance;
var scale = origin.scale * (1 + delta * ZOOM_FACTOR / 100);
animate(scale);
}
}
}, { passive: true });
function touchend() {
pressed = false;
} }
Tools.board.addEventListener("touchend", touchend); pressed = false;
Tools.board.addEventListener("touchcancel", touchend); }
function release(x, y, evt, isTouchEvent) { function key(down) {
if (pressed && !moved) { return function (evt) {
var delta = (evt.shiftKey === true) ? -1 : 1; if (evt.key === "Shift") {
var scale = Tools.getScale() * (1 + delta * ZOOM_FACTOR); Tools.svg.style.cursor = "zoom-" + (down ? "out" : "in");
zoom(origin, scale); }
}
pressed = false;
}
function key(down) {
return function (evt) {
if (evt.key === "Shift") {
Tools.svg.style.cursor = "zoom-" + (down ? "out" : "in");
}
}
}
function getClientY(evt, isTouchEvent) {
return isTouchEvent ? evt.changedTouches[0].clientY : evt.clientY;
}
var keydown = key(true);
var keyup = key(false);
function onstart() {
window.addEventListener("keydown", keydown);
window.addEventListener("keyup", keyup);
}
function onquit() {
window.removeEventListener("keydown", keydown);
window.removeEventListener("keyup", keyup);
}
var zoomTool = {
"name": "Zoom",
"shortcut": "z",
"listeners": {
"press": press,
"move": move,
"release": release,
},
"onstart": onstart,
"onquit": onquit,
"mouseCursor": "zoom-in",
"icon": "tools/zoom/icon.svg",
"helpText": "click_to_zoom",
"showMarker": true,
}; };
Tools.add(zoomTool); }
function getClientY(evt, isTouchEvent) {
return isTouchEvent ? evt.changedTouches[0].clientY : evt.clientY;
}
var keydown = key(true);
var keyup = key(false);
function onstart() {
window.addEventListener("keydown", keydown);
window.addEventListener("keyup", keyup);
}
function onquit() {
window.removeEventListener("keydown", keydown);
window.removeEventListener("keyup", keyup);
}
var zoomTool = {
name: "Zoom",
shortcut: "z",
listeners: {
press: press,
move: move,
release: release,
},
onstart: onstart,
onquit: onquit,
mouseCursor: "zoom-in",
icon: "tools/zoom/icon.svg",
helpText: "click_to_zoom",
showMarker: true,
};
Tools.add(zoomTool);
})(); //End of code isolation })(); //End of code isolation

View file

@ -1,10 +1,10 @@
version: '3' version: "3"
services: services:
www: www:
volumes: volumes:
- /opt/app/server-data:/opt/app/server-data - /opt/app/server-data:/opt/app/server-data
ports: ports:
- '80:80' - "80:80"
build: build:
context: . context: .
restart: on-failure restart: on-failure
@ -14,4 +14,3 @@ services:
delay: 5s delay: 5s
max_attempts: 5 max_attempts: 5
window: 60s window: 60s

View file

@ -1,49 +1,49 @@
// Autogenerated by Nightwatch // Autogenerated by Nightwatch
// Refer to the online docs for more details: https://nightwatchjs.org/gettingstarted/configuration/ // Refer to the online docs for more details: https://nightwatchjs.org/gettingstarted/configuration/
const Services = {}; loadServices(); const Services = {};
loadServices();
module.exports = { module.exports = {
"src_folders": ["tests"], src_folders: ["tests"],
"webdriver": { webdriver: {
"start_process": true, start_process: true,
"server_path": "./node_modules/.bin/geckodriver", server_path: "./node_modules/.bin/geckodriver",
"cli_args": [ cli_args: ["--log", "debug"],
"--log", "debug" port: 4444,
],
"port": 4444
}, },
"test_settings": { test_settings: {
"default": { default: {
"desiredCapabilities": { desiredCapabilities: {
"browserName": "firefox", browserName: "firefox",
"acceptInsecureCerts": true, acceptInsecureCerts: true,
"alwaysMatch": { alwaysMatch: {
"moz:firefoxOptions": { "moz:firefoxOptions": {
"args": ["-headless"] args: ["-headless"],
} },
} },
} },
}, },
"jwt": { jwt: {
"globals": { globals: {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.5mhBHqs5_DTLdINd9p5m7ZJ6XD0Xc55kIaCRY5r6HRA" token:
} "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.5mhBHqs5_DTLdINd9p5m7ZJ6XD0Xc55kIaCRY5r6HRA",
} },
} },
},
}; };
function loadServices() { function loadServices() {
try { try {
Services.seleniumServer = require('selenium-server'); Services.seleniumServer = require("selenium-server");
} catch (err) { } } catch (err) {}
try { try {
Services.chromedriver = require('chromedriver'); Services.chromedriver = require("chromedriver");
} catch (err) { } } catch (err) {}
try { try {
Services.geckodriver = require('geckodriver'); Services.geckodriver = require("geckodriver");
} catch (err) { } } catch (err) {}
} }

24
package-lock.json generated
View file

@ -21,7 +21,8 @@
}, },
"devDependencies": { "devDependencies": {
"geckodriver": "^4.2.1", "geckodriver": "^4.2.1",
"nightwatch": "^3.2.1" "nightwatch": "^3.2.1",
"prettier": "^3.1.0"
} }
}, },
"node_modules/@assemblyscript/loader": { "node_modules/@assemblyscript/loader": {
@ -3959,6 +3960,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/prettier": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -8397,6 +8413,12 @@
"toposort": "^2.0.2" "toposort": "^2.0.2"
} }
}, },
"prettier": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true
},
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

View file

@ -20,7 +20,9 @@
}, },
"scripts": { "scripts": {
"start": "node ./server/server.js", "start": "node ./server/server.js",
"test": "nightwatch tests && nightwatch tests --env jwt" "test": "nightwatch tests && nightwatch tests --env jwt",
"prettier": "prettier . --write",
"prettier-check": "prettier . --check"
}, },
"main": "./server/server.js", "main": "./server/server.js",
"repository": { "repository": {
@ -29,6 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"geckodriver": "^4.2.1", "geckodriver": "^4.2.1",
"nightwatch": "^3.2.1" "nightwatch": "^3.2.1",
"prettier": "^3.1.0"
} }
} }

View file

@ -46,7 +46,7 @@ class BoardData {
this.board = {}; this.board = {};
this.file = path.join( this.file = path.join(
config.HISTORY_DIR, config.HISTORY_DIR,
"board-" + encodeURIComponent(name) + ".json" "board-" + encodeURIComponent(name) + ".json",
); );
this.assetsDir = path.join( this.assetsDir = path.join(
config.HISTORY_DIR, config.HISTORY_DIR,
@ -202,7 +202,7 @@ class BoardData {
this.addChild(message.parent, message); this.addChild(message.parent, message);
break; break;
case "clear": case "clear":
if(jwtauth.roleInBoard(message.token,message.board) === 'moderator') { if (jwtauth.roleInBoard(message.token, message.board) === "moderator") {
this.clear(); this.clear();
} else { } else {
throw new Error("User is not a moderator"); throw new Error("User is not a moderator");

View file

@ -24,7 +24,8 @@ async function get_error(directory) {
let err_msg = "does not allow file creation and deletion. "; let err_msg = "does not allow file creation and deletion. ";
try { try {
const { uid, gid } = os.userInfo(); const { uid, gid } = os.userInfo();
err_msg += "Check the permissions of the directory, and if needed change them so that " + err_msg +=
"Check the permissions of the directory, and if needed change them so that " +
`user with UID ${uid} has access to them. This can be achieved by running the command: chown ${uid}:${gid} on the directory`; `user with UID ${uid} has access to them. This can be achieved by running the command: chown ${uid}:${gid} on the directory`;
} finally { } finally {
return err_msg; return err_msg;
@ -40,7 +41,7 @@ async function get_error(directory) {
fileChecks.push( fileChecks.push(
fs.promises.access(elemPath, R_OK | W_OK).catch(function () { fs.promises.access(elemPath, R_OK | W_OK).catch(function () {
return elemPath; return elemPath;
}) }),
); );
} }
} }
@ -66,8 +67,8 @@ function check_output_directory(directory) {
if (error) { if (error) {
console.error( console.error(
`The configured history directory in which boards are stored ${error}.` + `The configured history directory in which boards are stored ${error}.` +
`\nThe history directory can be configured with the environment variable WBO_HISTORY_DIR. ` + `\nThe history directory can be configured with the environment variable WBO_HISTORY_DIR. ` +
`It is currently set to "${directory}".` `It is currently set to "${directory}".`,
); );
process.exit(1); process.exit(1);
} }

View file

@ -46,22 +46,23 @@ module.exports = {
BLOCKED_TOOLS: (process.env["WBO_BLOCKED_TOOLS"] || "").split(","), BLOCKED_TOOLS: (process.env["WBO_BLOCKED_TOOLS"] || "").split(","),
/** Selection Buttons. A comma-separated list of selection buttons that should not be available. */ /** Selection Buttons. A comma-separated list of selection buttons that should not be available. */
BLOCKED_SELECTION_BUTTONS: (process.env["WBO_BLOCKED_SELECTION_BUTTONS"] || "").split(","), BLOCKED_SELECTION_BUTTONS: (
process.env["WBO_BLOCKED_SELECTION_BUTTONS"] || ""
).split(","),
/** Automatically switch to White-out on finger touch after drawing /** Automatically switch to White-out on finger touch after drawing
with Pencil using a stylus. Only supported on iPad with Apple Pencil. */ with Pencil using a stylus. Only supported on iPad with Apple Pencil. */
AUTO_FINGER_WHITEOUT: process.env["AUTO_FINGER_WHITEOUT"] !== "disabled", AUTO_FINGER_WHITEOUT: process.env["AUTO_FINGER_WHITEOUT"] !== "disabled",
/** If this variable is set, it should point to a statsd listener that will /** If this variable is set, it should point to a statsd listener that will
* receive WBO's monitoring information. * receive WBO's monitoring information.
* example: udp://127.0.0.1 * example: udp://127.0.0.1
*/ */
STATSD_URL: process.env["STATSD_URL"], STATSD_URL: process.env["STATSD_URL"],
/** Secret key for jwt */ /** Secret key for jwt */
AUTH_SECRET_KEY: (process.env["AUTH_SECRET_KEY"] || ""), AUTH_SECRET_KEY: process.env["AUTH_SECRET_KEY"] || "",
/** If this variable is set, automatically redirect to this board from the root of the application. */
DEFAULT_BOARD: (process.env["WBO_DEFAULT_BOARD"]),
/** If this variable is set, automatically redirect to this board from the root of the application. */
DEFAULT_BOARD: process.env["WBO_DEFAULT_BOARD"],
}; };

View file

@ -1,7 +1,7 @@
const fs = require("./fs_promises.js"), const fs = require("./fs_promises.js"),
path = require("path"), path = require("path"),
wboPencilPoint = require("../client-data/tools/pencil/wbo_pencil_point.js") wboPencilPoint =
.wboPencilPoint; require("../client-data/tools/pencil/wbo_pencil_point.js").wboPencilPoint;
function htmlspecialchars(str) { function htmlspecialchars(str) {
if (typeof str !== "string") return ""; if (typeof str !== "string") return "";
@ -181,7 +181,7 @@ async function toSVG(obj, writeable) {
Math.max((elem.y + margin + (elem.deltay | 0)) | 0, dim[1]), Math.max((elem.y + margin + (elem.deltay | 0)) | 0, dim[1]),
]; ];
}, },
[margin, margin] [margin, margin],
); );
writeable.write( writeable.write(
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' + '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' +
@ -194,7 +194,7 @@ async function toSVG(obj, writeable) {
'text {font-family:"Arial"}' + 'text {font-family:"Arial"}' +
"path {fill:none;stroke-linecap:round;stroke-linejoin:round;}" + "path {fill:none;stroke-linecap:round;stroke-linejoin:round;}" +
"rect {fill:none}" + "rect {fill:none}" +
"]]></style></defs>" "]]></style></defs>",
); );
await Promise.all( await Promise.all(
elems.map(async function (elem) { elems.map(async function (elem) {
@ -202,7 +202,7 @@ async function toSVG(obj, writeable) {
const renderFun = Tools[elem.tool]; const renderFun = Tools[elem.tool];
if (renderFun) writeable.write(renderFun(elem)); if (renderFun) writeable.write(renderFun(elem));
else console.warn("Missing render function for tool", elem.tool); else console.warn("Missing render function for tool", elem.tool);
}) }),
); );
writeable.write("</svg>"); writeable.write("</svg>");
} }

View file

@ -24,8 +24,8 @@
* @licend * @licend
*/ */
config = require("./configuration.js"), (config = require("./configuration.js")),
jsonwebtoken = require("jsonwebtoken"); (jsonwebtoken = require("jsonwebtoken"));
/** /**
* This function checks if a board name is set in the roles claim. * This function checks if a board name is set in the roles claim.
@ -37,15 +37,15 @@ config = require("./configuration.js"),
*/ */
function checkBoardnameInToken(url, boardNameIn) { function checkBoardnameInToken(url, boardNameIn) {
var token = url.searchParams.get("token"); var token = url.searchParams.get("token");
if (roleInBoard(token, boardNameIn) === 'forbidden') { if (roleInBoard(token, boardNameIn) === "forbidden") {
throw new Error("Acess Forbidden"); throw new Error("Acess Forbidden");
} }
} }
function parse_role(role) { function parse_role(role) {
let [_, role_name, board_name] = role.match(/^([^:]*):?(.*)$/); let [_, role_name, board_name] = role.match(/^([^:]*):?(.*)$/);
return {role_name, board_name} return { role_name, board_name };
} }
/** /**
@ -56,44 +56,44 @@ function parse_role(role) {
@returns {string} "moderator"|"editor"|"forbidden" @returns {string} "moderator"|"editor"|"forbidden"
*/ */
function roleInBoard(token, board = null) { function roleInBoard(token, board = null) {
if (config.AUTH_SECRET_KEY != "") { if (config.AUTH_SECRET_KEY != "") {
if (!token) { if (!token) {
throw new Error("No token provided"); throw new Error("No token provided");
}
var payload = jsonwebtoken.verify(token, config.AUTH_SECRET_KEY);
var roles = payload.roles;
var oneHasBoardName = false;
var oneHasModerator = false;
if (roles) {
for (var line of roles) {
var role = parse_role(line);
if (role.board_name !== '') {
oneHasBoardName = true;
}
if (role.role_name === "moderator") {
oneHasModerator = true;
}
if (role.board_name === board) {
return role.role_name;
}
}
if ((!board && oneHasModerator) || !oneHasBoardName) {
if (oneHasModerator) {
return "moderator";
} else {
return "editor";
}
}
return "forbidden";
} else {
return "editor";
}
} else {
return "editor";
} }
var payload = jsonwebtoken.verify(token, config.AUTH_SECRET_KEY);
var roles = payload.roles;
var oneHasBoardName = false;
var oneHasModerator = false;
if (roles) {
for (var line of roles) {
var role = parse_role(line);
if (role.board_name !== "") {
oneHasBoardName = true;
}
if (role.role_name === "moderator") {
oneHasModerator = true;
}
if (role.board_name === board) {
return role.role_name;
}
}
if ((!board && oneHasModerator) || !oneHasBoardName) {
if (oneHasModerator) {
return "moderator";
} else {
return "editor";
}
}
return "forbidden";
} else {
return "editor";
}
} else {
return "editor";
}
} }
module.exports = {checkBoardnameInToken, roleInBoard}; module.exports = { checkBoardnameInToken, roleInBoard };

View file

@ -1,7 +1,7 @@
/** /**
* WHITEBOPHIR * WHITEBOPHIR
********************************************************* *********************************************************
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page. * JavaScript code in this page.
* *
* Copyright (C) 2013 Ophir LOJKINE * Copyright (C) 2013 Ophir LOJKINE
@ -24,10 +24,9 @@
* @licend * @licend
*/ */
(config = require("./configuration.js")),
config = require("./configuration.js"), (jsonwebtoken = require("jsonwebtoken"));
jsonwebtoken = require("jsonwebtoken"); const { roleInBoard } = require("./jwtBoardnameAuth");
const {roleInBoard} = require("./jwtBoardnameAuth");
/** /**
* Validates jwt and returns whether user is a moderator * Validates jwt and returns whether user is a moderator
* @param {URL} url * @param {URL} url
@ -48,5 +47,4 @@ function checkUserPermission(url) {
return isModerator; return isModerator;
} }
module.exports = { checkUserPermission }; module.exports = { checkUserPermission };

View file

@ -1,6 +1,6 @@
var app = require("http").createServer(handler), var app = require("http").createServer(handler),
sockets = require("./sockets.js"), sockets = require("./sockets.js"),
{log, monitorFunction} = require("./log.js"), { log, monitorFunction } = require("./log.js"),
path = require("path"), path = require("path"),
fs = require("./fs_promises.js"), fs = require("./fs_promises.js"),
crypto = require("crypto"), crypto = require("crypto"),
@ -25,7 +25,7 @@ if (parseFloat(process.versions.node) < MIN_NODE_VERSION) {
process.version + process.version +
", wbo requires at least " + ", wbo requires at least " +
MIN_NODE_VERSION + MIN_NODE_VERSION +
" !!!" " !!!",
); );
} }
@ -109,10 +109,10 @@ function handler(request, response) {
} }
const boardTemplate = new templating.BoardTemplate( const boardTemplate = new templating.BoardTemplate(
path.join(config.WEBROOT, "board.html") path.join(config.WEBROOT, "board.html"),
); );
const indexTemplate = new templating.Template( const indexTemplate = new templating.Template(
path.join(config.WEBROOT, "index.html") path.join(config.WEBROOT, "index.html"),
); );
/** /**
@ -145,10 +145,10 @@ async function handleRequest(request, response) {
if (parts[0] === "") parts.shift(); if (parts[0] === "") parts.shift();
var fileExt = path.extname(parsedUrl.pathname); var fileExt = path.extname(parsedUrl.pathname);
var staticResources = ['.js','.css', '.svg', '.ico', '.png', '.jpg', 'gif']; var staticResources = [".js", ".css", ".svg", ".ico", ".png", ".jpg", "gif"];
// If we're not being asked for a file, then we should check permissions. // If we're not being asked for a file, then we should check permissions.
var isModerator = false; var isModerator = false;
if(!staticResources.includes(fileExt)) { if (!staticResources.includes(fileExt)) {
isModerator = jwtauth.checkUserPermission(parsedUrl); isModerator = jwtauth.checkUserPermission(parsedUrl);
} }
@ -174,25 +174,25 @@ async function handleRequest(request, response) {
break; break;
case "download": case "download":
var boardName = validateBoardName(parts[1]), var boardName = validateBoardName(parts[1]),
history_file = path.join( history_file = path.join(
config.HISTORY_DIR, config.HISTORY_DIR,
"board-" + boardName + ".json" "board-" + boardName + ".json",
); );
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName); jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
if (parts.length > 2 && /^[0-9A-Za-z.\-]+$/.test(parts[2])) { if (parts.length > 2 && /^[0-9A-Za-z.\-]+$/.test(parts[2])) {
history_file += "." + parts[2] + ".bak"; history_file += "." + parts[2] + ".bak";
} }
log("download", { file: history_file }); log("download", { file: history_file });
fs.readFile(history_file, function (err, data) { fs.readFile(history_file, function (err, data) {
if (err) return serveError(request, response)(err); if (err) return serveError(request, response)(err);
response.writeHead(200, { response.writeHead(200, {
"Content-Type": "application/json", "Content-Type": "application/json",
"Content-Disposition": 'attachment; filename="' + boardName + '.wbo"', "Content-Disposition": 'attachment; filename="' + boardName + '.wbo"',
"Content-Length": data.length, "Content-Length": data.length,
});
response.end(data);
}); });
response.end(data);
});
break; break;
case "image-upload": case "image-upload":
if (config.BLOCKED_TOOLS.includes('Image')) { if (config.BLOCKED_TOOLS.includes('Image')) {
@ -257,28 +257,28 @@ async function handleRequest(request, response) {
break; break;
case "export": case "export":
case "preview": case "preview":
var boardName = validateBoardName(parts[1]), var boardName = validateBoardName(parts[1]),
history_file = path.join( history_file = path.join(
config.HISTORY_DIR, config.HISTORY_DIR,
"board-" + boardName + ".json" "board-" + boardName + ".json",
); );
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName); jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
response.writeHead(200, { response.writeHead(200, {
"Content-Type": "image/svg+xml", "Content-Type": "image/svg+xml",
"Content-Security-Policy": CSP, "Content-Security-Policy": CSP,
"Cache-Control": "public, max-age=30", "Cache-Control": "public, max-age=30",
});
var t = Date.now();
createSVG
.renderBoard(history_file, response)
.then(function () {
log("preview", { board: boardName, time: Date.now() - t });
response.end();
})
.catch(function (err) {
log("error", { error: err.toString(), stack: err.stack });
response.end("<text>Sorry, an error occured</text>");
}); });
var t = Date.now();
createSVG
.renderBoard(history_file, response)
.then(function () {
log("preview", { board: boardName, time: Date.now() - t });
response.end();
})
.catch(function (err) {
log("error", { error: err.toString(), stack: err.stack });
response.end("<text>Sorry, an error occured</text>");
});
break; break;
case "random": case "random":
@ -310,7 +310,7 @@ async function handleRequest(request, response) {
.then(function (bundleString) { .then(function (bundleString) {
response.setHeader( response.setHeader(
"Cache-Control", "Cache-Control",
"private, max-age=172800, stale-while-revalidate=1728000" "private, max-age=172800, stale-while-revalidate=1728000",
); );
response.setHeader("Vary", "User-Agent"); response.setHeader("Vary", "User-Agent");
response.setHeader("Content-Type", "application/javascript"); response.setHeader("Content-Type", "application/javascript");
@ -321,10 +321,11 @@ async function handleRequest(request, response) {
case "": // Index page case "": // Index page
logRequest(request); logRequest(request);
if (config.DEFAULT_BOARD) { if (config.DEFAULT_BOARD) {
response.writeHead(302, { Location: 'boards/' + encodeURIComponent(config.DEFAULT_BOARD) }); response.writeHead(302, {
Location: "boards/" + encodeURIComponent(config.DEFAULT_BOARD),
});
response.end(name); response.end(name);
} else } else indexTemplate.serve(request, response);
indexTemplate.serve(request, response);
break; break;
default: default:

View file

@ -33,12 +33,17 @@ function startIO(app, boardDataList) {
io = iolib(app); io = iolib(app);
if (config.AUTH_SECRET_KEY) { if (config.AUTH_SECRET_KEY) {
// Middleware to check for valid jwt // Middleware to check for valid jwt
io.use(function(socket, next) { io.use(function (socket, next) {
if(socket.handshake.query && socket.handshake.query.token) { if (socket.handshake.query && socket.handshake.query.token) {
jsonwebtoken.verify(socket.handshake.query.token, config.AUTH_SECRET_KEY, function(err, decoded) { jsonwebtoken.verify(
if(err) return next(new Error("Authentication error: Invalid JWT")); socket.handshake.query.token,
next(); config.AUTH_SECRET_KEY,
}) function (err, decoded) {
if (err)
return next(new Error("Authentication error: Invalid JWT"));
next();
},
);
} else { } else {
next(new Error("Authentication error: No jwt provided")); next(new Error("Authentication error: No jwt provided"));
} }
@ -92,7 +97,7 @@ function handleSocketConnection(socket) {
"error", "error",
noFail(function onSocketError(error) { noFail(function onSocketError(error) {
log("ERROR", error); log("ERROR", error);
}) }),
); );
socket.on("getboard", async function onGetBoard(name) { socket.on("getboard", async function onGetBoard(name) {
@ -152,7 +157,7 @@ function handleSocketConnection(socket) {
//Send data to all other users connected on the same board //Send data to all other users connected on the same board
socket.broadcast.to(boardName).emit("broadcast", data); socket.broadcast.to(boardName).emit("broadcast", data);
}) }),
); );
socket.on("disconnecting", function onDisconnecting(reason) { socket.on("disconnecting", function onDisconnecting(reason) {

View file

@ -11,7 +11,7 @@ const client_config = require("./client_configuration");
* @type {object} * @type {object}
*/ */
const TRANSLATIONS = JSON.parse( const TRANSLATIONS = JSON.parse(
fs.readFileSync(path.join(__dirname, "translations.json")) fs.readFileSync(path.join(__dirname, "translations.json")),
); );
const languages = Object.keys(TRANSLATIONS); const languages = Object.keys(TRANSLATIONS);
@ -33,7 +33,8 @@ class Template {
this.template = handlebars.compile(contents); this.template = handlebars.compile(contents);
} }
parameters(parsedUrl, request, isModerator) { parameters(parsedUrl, request, isModerator) {
const accept_language_str = parsedUrl.query.lang || request.headers["accept-language"]; const accept_language_str =
parsedUrl.query.lang || request.headers["accept-language"];
const accept_languages = accept_language_parser.parse(accept_language_str); const accept_languages = accept_language_parser.parse(accept_language_str);
const opts = { loose: true }; const opts = { loose: true };
let language = let language =
@ -41,7 +42,8 @@ class Template {
// The loose matcher returns the first language that partially matches, so we need to // The loose matcher returns the first language that partially matches, so we need to
// check if the preferred language is supported to return it // check if the preferred language is supported to return it
if (accept_languages.length > 0) { if (accept_languages.length > 0) {
const preferred_language = accept_languages[0].code + "-" + accept_languages[0].region; const preferred_language =
accept_languages[0].code + "-" + accept_languages[0].region;
if (languages.includes(preferred_language)) { if (languages.includes(preferred_language)) {
language = preferred_language; language = preferred_language;
} }
@ -51,7 +53,14 @@ class Template {
const prefix = request.url.split("/boards/")[0].substr(1); const prefix = request.url.split("/boards/")[0].substr(1);
const baseUrl = findBaseUrl(request) + (prefix ? prefix + "/" : ""); const baseUrl = findBaseUrl(request) + (prefix ? prefix + "/" : "");
const moderator = isModerator; const moderator = isModerator;
return { baseUrl, languages, language, translations, configuration, moderator }; return {
baseUrl,
languages,
language,
translations,
configuration,
moderator,
};
} }
serve(request, response, isModerator) { serve(request, response, isModerator) {
const parsedUrl = url.parse(request.url, true); const parsedUrl = url.parse(request.url, true);

View file

@ -2,98 +2,122 @@ const fs = require("../server/fs_promises.js");
const os = require("os"); const os = require("os");
const path = require("path"); const path = require("path");
const PORT = 8487 const PORT = 8487;
const SERVER = 'http://localhost:' + PORT; const SERVER = "http://localhost:" + PORT;
let wbo, data_path, tokenQuery, currentPath; let wbo, data_path, tokenQuery, currentPath;
async function beforeEach(browser, done) { async function beforeEach(browser, done) {
data_path = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'wbo-test-data-')); data_path = await fs.promises.mkdtemp(
currentPath = process.cwd(); path.join(os.tmpdir(), "wbo-test-data-"),
process.env["PORT"] = PORT; );
process.env["WBO_HISTORY_DIR"] = data_path; process.env["PORT"] = PORT;
if(browser.globals.token) { process.env["WBO_HISTORY_DIR"] = data_path;
process.env["AUTH_SECRET_KEY"] = "test"; if (browser.globals.token) {
tokenQuery = "token=" + browser.globals.token; process.env["AUTH_SECRET_KEY"] = "test";
} tokenQuery = "token=" + browser.globals.token;
console.log("Launching WBO in " + data_path); }
wbo = require("../server/server.js"); console.log("Launching WBO in " + data_path);
done(); wbo = require("../server/server.js");
done();
} }
async function afterEach(browser, done) { async function afterEach(browser, done) {
wbo.close(); wbo.close();
done(); done();
} }
function testPencil(browser) { function testPencil(browser) {
return browser return browser.assert
.assert.titleContains('WBO') .titleContains("WBO")
.click('.tool[title ~= Crayon]') // pencil .click(".tool[title ~= Crayon]") // pencil
.assert.cssClassPresent('.tool[title ~= Crayon]', ['curTool']) .assert.cssClassPresent(".tool[title ~= Crayon]", ["curTool"])
.executeAsync(async function (done) { .executeAsync(async function (done) {
function sleep(t) { function sleep(t) {
return new Promise(function (accept) { setTimeout(accept, t); }); return new Promise(function (accept) {
} setTimeout(accept, t);
// A straight path with just two points });
Tools.setColor('#123456'); }
Tools.curTool.listeners.press(100, 200, new Event("mousedown")); // A straight path with just two points
await sleep(80); Tools.setColor("#123456");
Tools.curTool.listeners.release(300, 400, new Event("mouseup")); Tools.curTool.listeners.press(100, 200, new Event("mousedown"));
await sleep(80);
Tools.curTool.listeners.release(300, 400, new Event("mouseup"));
// A line with three points that form an "U" shape // A line with three points that form an "U" shape
await sleep(80); await sleep(80);
Tools.setColor('#abcdef'); Tools.setColor("#abcdef");
Tools.curTool.listeners.press(0, 0, new Event("mousedown")); Tools.curTool.listeners.press(0, 0, new Event("mousedown"));
await sleep(80); await sleep(80);
Tools.curTool.listeners.move(90, 120, new Event("mousemove")); Tools.curTool.listeners.move(90, 120, new Event("mousemove"));
await sleep(80); await sleep(80);
Tools.curTool.listeners.release(180, 0, new Event("mouseup")); Tools.curTool.listeners.release(180, 0, new Event("mouseup"));
done(); done();
}) })
.assert.visible("path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']") .assert.visible(
.assert.visible("path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']") "path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']",
.refresh() )
.waitForElementVisible("path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']") .assert.visible(
.assert.visible("path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']") "path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']",
.url(SERVER + '/preview/anonymous?' + tokenQuery) )
.waitForElementVisible("path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']") .refresh()
.assert.visible("path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']") .waitForElementVisible(
.back() "path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']",
)
.assert.visible(
"path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']",
)
.url(SERVER + "/preview/anonymous?" + tokenQuery)
.waitForElementVisible(
"path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']",
)
.assert.visible(
"path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']",
)
.back();
} }
function testCircle(browser) { function testCircle(browser) {
return browser return browser
.click('#toolID-Ellipse') .click("#toolID-Ellipse")
.executeAsync(function (done) { .executeAsync(function (done) {
Tools.setColor('#112233'); Tools.setColor("#112233");
Tools.curTool.listeners.press(200, 400, new Event("mousedown")); Tools.curTool.listeners.press(200, 400, new Event("mousedown"));
setTimeout(() => { setTimeout(() => {
const evt = new Event("mousemove"); const evt = new Event("mousemove");
evt.shiftKey = true; evt.shiftKey = true;
Tools.curTool.listeners.move(0, 0, evt); Tools.curTool.listeners.move(0, 0, evt);
done(); done();
}, 100); }, 100);
}) })
.assert.visible("ellipse[cx='0'][cy='200'][rx='200'][ry='200'][stroke='#112233']") .assert.visible(
.refresh() "ellipse[cx='0'][cy='200'][rx='200'][ry='200'][stroke='#112233']",
.waitForElementVisible("ellipse[cx='0'][cy='200'][rx='200'][ry='200'][stroke='#112233']", 15000) )
.click('#toolID-Ellipse') // Click the ellipse tool .refresh()
.click('#toolID-Ellipse') // Click again to toggle .waitForElementVisible(
.assert.containsText('#toolID-Ellipse .tool-name', 'Cercle') // Circle in french "ellipse[cx='0'][cy='200'][rx='200'][ry='200'][stroke='#112233']",
15000,
)
.click("#toolID-Ellipse") // Click the ellipse tool
.click("#toolID-Ellipse") // Click again to toggle
.assert.containsText("#toolID-Ellipse .tool-name", "Cercle"); // Circle in french
} }
function testCursor(browser) { function testCursor(browser) {
return browser return browser
.execute(function (done) { .execute(function (done) {
Tools.setColor('#456123'); // Move the cursor over the board Tools.setColor("#456123"); // Move the cursor over the board
var e = new Event("mousemove"); var e = new Event("mousemove");
e.pageX = 150; e.pageX = 150;
e.pageY = 200; e.pageY = 200;
Tools.board.dispatchEvent(e) Tools.board.dispatchEvent(e);
}) })
.assert.cssProperty("#cursor-me", "transform", "matrix(1, 0, 0, 1, 150, 200)") .assert.cssProperty(
.assert.attributeEquals("#cursor-me", "fill", "#456123") "#cursor-me",
"transform",
"matrix(1, 0, 0, 1, 150, 200)",
)
.assert.attributeEquals("#cursor-me", "fill", "#456123");
} }
function testImageUpload(browser) { function testImageUpload(browser) {
@ -132,38 +156,98 @@ function testImageUpload(browser) {
} }
function testBoard(browser) { function testBoard(browser) {
var page = browser.url(SERVER + '/boards/anonymous?lang=fr&' + tokenQuery) var page = browser
.waitForElementVisible('.tool[title ~= Crayon]') // pencil .url(SERVER + "/boards/anonymous?lang=fr&" + tokenQuery)
page = testImageUpload(page); .waitForElementVisible(".tool[title ~= Crayon]"); // pencil
page = testPencil(page); page = testImageUpload(page);
page = testCircle(page); page = testPencil(page);
page = testCursor(page); page = testCircle(page);
page = testCursor(page);
// test hideMenu // test hideMenu
browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=true&' + tokenQuery).waitForElementNotVisible('#menu'); browser
browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=false&' + tokenQuery).waitForElementVisible('#menu'); .url(SERVER + "/boards/anonymous?lang=fr&hideMenu=true&" + tokenQuery)
if(browser.globals.token) { .waitForElementNotVisible("#menu");
//has moderator jwt and no board name browser
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3IiXX0.PqYHmV0loeKwyLLYZ1a1eIXBCCaa3t5lYUTu_P_-i14').waitForElementVisible('#toolID-Clear'); .url(SERVER + "/boards/anonymous?lang=fr&hideMenu=false&" + tokenQuery)
//has moderator JWT and other board name .waitForElementVisible("#menu");
browser.url(SERVER + '/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3IiXX0.PqYHmV0loeKwyLLYZ1a1eIXBCCaa3t5lYUTu_P_-i14').waitForElementVisible('#toolID-Clear'); if (browser.globals.token) {
//has moderator JWT and board name match board name in url //has moderator jwt and no board name
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdGJvYXJkIl19.UVf6awGEChVxcWBbt6dYoNH0Scq7cVD_xfQn-U8A1lw').waitForElementVisible('#toolID-Clear'); browser
//has moderator JWT and board name NOT match board name in url .url(
browser.url(SERVER + '/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdGJvYXJkIl19.UVf6awGEChVxcWBbt6dYoNH0Scq7cVD_xfQn-U8A1lw').waitForElementNotPresent('#menu'); SERVER +
//has editor JWT and no boardname provided "/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3IiXX0.PqYHmV0loeKwyLLYZ1a1eIXBCCaa3t5lYUTu_P_-i14",
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0b3IiXX0.IJehwM8tPVQFzJ2fZMBHveii1DRChVtzo7PEnSmmFt8').waitForElementNotPresent('#toolID-Clear'); )
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0b3IiXX0.IJehwM8tPVQFzJ2fZMBHveii1DRChVtzo7PEnSmmFt8').waitForElementVisible('#menu') .waitForElementVisible("#toolID-Clear");
//has editor JWT and boardname provided and match to the board in the url //has moderator JWT and other board name
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo').waitForElementVisible('#menu'); browser
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo').waitForElementNotPresent('#toolID-Clear'); .url(
//has editor JWT and boardname provided and and not match to the board in the url SERVER +
browser.url(SERVER + '/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo').waitForElementNotPresent('#menu'); "/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3IiXX0.PqYHmV0loeKwyLLYZ1a1eIXBCCaa3t5lYUTu_P_-i14",
//is moderator and boardname contains ":" )
browser.url(SERVER + '/boards/test:board?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdDpib2FyZCJdfQ.LKYcDccheD2oXAMAemxSekDeowGsMl29CFkgJgwbkGE').waitForElementNotPresent('#menu'); .waitForElementVisible("#toolID-Clear");
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdDpib2FyZCJdfQ.LKYcDccheD2oXAMAemxSekDeowGsMl29CFkgJgwbkGE').waitForElementNotPresent('#menu'); //has moderator JWT and board name match board name in url
} browser
page.end(); .url(
SERVER +
"/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdGJvYXJkIl19.UVf6awGEChVxcWBbt6dYoNH0Scq7cVD_xfQn-U8A1lw",
)
.waitForElementVisible("#toolID-Clear");
//has moderator JWT and board name NOT match board name in url
browser
.url(
SERVER +
"/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdGJvYXJkIl19.UVf6awGEChVxcWBbt6dYoNH0Scq7cVD_xfQn-U8A1lw",
)
.waitForElementNotPresent("#menu");
//has editor JWT and no boardname provided
browser
.url(
SERVER +
"/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0b3IiXX0.IJehwM8tPVQFzJ2fZMBHveii1DRChVtzo7PEnSmmFt8",
)
.waitForElementNotPresent("#toolID-Clear");
browser
.url(
SERVER +
"/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0b3IiXX0.IJehwM8tPVQFzJ2fZMBHveii1DRChVtzo7PEnSmmFt8",
)
.waitForElementVisible("#menu");
//has editor JWT and boardname provided and match to the board in the url
browser
.url(
SERVER +
"/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo",
)
.waitForElementVisible("#menu");
browser
.url(
SERVER +
"/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo",
)
.waitForElementNotPresent("#toolID-Clear");
//has editor JWT and boardname provided and and not match to the board in the url
browser
.url(
SERVER +
"/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo",
)
.waitForElementNotPresent("#menu");
//is moderator and boardname contains ":"
browser
.url(
SERVER +
"/boards/test:board?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdDpib2FyZCJdfQ.LKYcDccheD2oXAMAemxSekDeowGsMl29CFkgJgwbkGE",
)
.waitForElementNotPresent("#menu");
browser
.url(
SERVER +
"/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdDpib2FyZCJdfQ.LKYcDccheD2oXAMAemxSekDeowGsMl29CFkgJgwbkGE",
)
.waitForElementNotPresent("#menu");
}
page.end();
} }
module.exports = { beforeEach, testBoard, afterEach }; module.exports = { beforeEach, testBoard, afterEach };