Skip to content
Commits on Source (137)
image: node:10
stages:
- test
- build
- tag
- publish
- release
cache:
paths:
- dist
- server
- lib
#test:
# stage: test
# only:
# - branches@MeldCE/first-draft
# except:
# - master@MeldCE/first-draft
build:
stage: build
before_script:
- yarn
script:
- npm run build:app
- npm run build:server
only:
- branches@MeldCE/first-draft
tag:
stage: tag
script:
- export VERSION=$(node -e "console.log(require('./package.json').version)")
- git status
- git remote -v
- git remote set-url origin ${GLT}
- git tag v$VERSION
- git push origin v$VERSION
only:
- master@MeldCE/first-draft
publish:
stage: publish
script:
- echo '//registry.npmjs.org/:_authToken=${NPMT}' > .npmrc
- npm publish
only:
- master@MeldCE/first-draft
branch_docker:
stage: publish
image: docker:stable
services:
- docker:stable-dind
variables:
IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull $IMAGE_NAME || docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build -f docker/prod/Dockerfile -t $IMAGE_NAME .
- docker push $IMAGE_NAME
only:
- branches@MeldCE/first-draft
except:
- master@MeldCE/first-draft
dev_docker:
stage: publish
image: docker:stable
services:
- docker:stable-dind
variables:
IMAGE_NAME: "$CI_REGISTRY_IMAGE/dev"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull $IMAGE_NAME || docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build -f docker/prod/Dockerfile -t $IMAGE_NAME .
- docker push $IMAGE_NAME
only:
- dev@MeldCE/first-draft
docker:
stage: publish
image: docker:stable
services:
- docker:stable-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build -f docker/prod/Dockerfile -t $CI_REGISTRY_IMAGE .
- docker push $IMAGE_NAME
only:
- master@MeldCE/first-draft
code_quality:
stage: release
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
services:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
paths: [gl-code-quality-report.json]
only:
- branches@MeldCE/first-draft
except:
- master@MeldCE/first-draft
dev_release:
stage: release
script:
- curl -H "Authorization:$KNOCK_ID" $(echo $KNOCK_URL= | base64 --decode)/first-draft-branch/deploy?branch=$CI_COMMIT_REF_NAME -s -w "%{http_code}" -o /dev/null
environment:
name: $CI_COMMIT_REF_NAME
url: https://$CI_COMMIT_REF_NAME.first-draft.xyz
on_stop: remove_dev_release
only:
- branches@MeldCE/first-draft
except:
- master@MeldCE/first-draft
remove_dev_release:
stage: release
script:
- curl -H "Authorization:$KNOCK_ID" $(echo $KNOCK_URL= | base64 --decode)/first-draft-branch/delete?branch=$CI_COMMIT_REF_NAME -s -w "%{http_code}" -o /dev/null
when: manual
environment:
name: $CI_COMMIT_REF_NAME
action: stop
only:
- branches@MeldCE/first-draft
except:
- dev@MeldCE/first-draft
- master@MeldCE/first-draft
......@@ -29,11 +29,11 @@
2019-03-06T07:16:38.027Z git:initial 0:30:26
2019-03-06T08:47:03.857Z git:initial 1:09:21
2019-03-06T09:02:05.322Z git:initial 0:09:13
2019-03-07T08:57:10.235Z git:initial Rearranging issues on gitlab
2019-03-07T08:57:10.235Z git:initial 0:56:27
2019-03-07T08:57:10.235Z git:initial 1:00:00
2019-03-07T08:57:10.235Z git:initial Got pen working
2019-03-07T08:57:10.235Z git:initial Rearranging issues on gitlab
2019-03-07T08:57:10.235Z git:initial Working on storing draft
2019-03-07T08:57:10.235Z git:initial 0:56:27
2019-03-07T13:57:46.032Z git:initial 0:49:12
2019-03-08T08:18:07.726Z git:initial Got saving of pen create working and emiting to room
2019-03-08T09:14:17.031Z git:initial 0:58:07
......@@ -46,6 +46,7 @@
2019-03-13T18:50:16.140Z git:19-pan-zoom-tool 1:20:30
2019-03-13T22:16:51.180Z git:19-pan-zoom-tool 0:37:57
2019-03-13T22:16:51.180Z git:19-pan-zoom-tool 0:37:57
2019-03-13T22:41:49.038Z git:28-form-generator 0:24:58
2019-03-14T19:00:04.901Z git:19-pan-zoom-tool 1:00:15
2019-03-14T23:10:04.901Z git:19-pan-zoom-tool 2:00:15
2019-03-14T23:40:31.031Z git:32-add-mobile-scaling-header 0:03:03
......@@ -61,8 +62,8 @@
2019-03-17T15:22:01.564Z git:19-pan-zoom-tool 0:11:42
2019-03-17T16:00:01.564Z git:19-pan-zoom-tool 0:20:42
2019-03-17T16:53:13.368Z git:19-pan-zoom-tool 0:16:25
2019-03-17T20:45:13.368Z git:master Looking at tab suspense issue
2019-03-17T20:45:13.368Z git:master 0:10:25
2019-03-17T20:45:13.368Z git:master Looking at tab suspense issue
2019-03-17T22:02:57.980Z git:18-upload-access-images 0:46:43
2019-03-18T08:27:21.159Z git:18-upload-access-images 0:08:56
2019-03-18T18:11:37.329Z git:18-upload-access-images 0:20:11
......@@ -133,8 +134,8 @@
2019-04-10T06:38:39.228Z git:70-no-media-loading 0:05:38
2019-04-10T09:10:01.074Z git:69-chain-action-removal 0:32:51
2019-04-11T17:03:36.318Z git:69-chain-action-removal 0:00:30
2019-04-11T18:02:42.294Z git:69-chain-action-removal fixed chain action removal
2019-04-11T18:02:42.070Z git:69-chain-action-removal 0:05:57
2019-04-11T18:02:42.294Z git:69-chain-action-removal fixed chain action removal
2019-04-11T18:02:42.294Z git:69-chain-action-removal fixed smoothing on click line
2019-04-12T07:41:20.070Z git:69-chain-action-removal 0:47:57
2019-04-12T07:41:20.070Z git:69-chain-action-removal 0:47:57
......@@ -160,8 +161,8 @@
2019-04-27T08:41:28.263Z git:13-give-a-name 0:12:42
2019-04-29T06:43:12.687Z git:13-give-a-name 0:24:35
2019-04-29T07:08:40.444Z git:85-text-dont-save 0:05:13
2019-04-29T07:08:40.444Z git:85-text-dont-save Fixing saving bug
2019-04-29T07:08:40.444Z git:85-text-dont-save 0:20:13
2019-04-29T07:08:40.444Z git:85-text-dont-save Fixing saving bug
2019-04-29T20:01:56.028Z git:85-text-dont-save 0:11:30
2019-04-29T20:09:00.688Z git:85-text-dont-save 0:06:12
2019-05-01T08:15:25.038Z git:13-give-a-name 0:42:53
......@@ -188,3 +189,78 @@
2019-05-24T16:39:13.118Z git:13-give-a-name 0:01:17
2019-05-24T17:20:12.642Z git:13-give-a-name 0:40:59
2019-05-24T17:35:53.738Z git:dev 0:00:54
2019-05-28T07:41:02.204Z git:28-form-generator 0:47:60
2019-05-28T08:47:48.024Z git:28-form-generator 0:09:20
2019-05-28T17:59:56.311Z git:28-form-generator 1:02:18
2019-05-29T07:02:40.032Z git:28-form-generator 0:42:43
2019-05-29T07:34:53.919Z git:28-form-generator 0:32:56
2019-05-29T19:50:31.032Z git:28-form-generator 0:31:31
2019-05-30T07:08:28.410Z git:28-form-generator 0:50:24
2019-05-30T07:20:21.676Z git:28-form-generator 0:11:41
2019-05-30T17:46:36.101Z git:28-form-generator 0:31:48
2019-05-30T17:46:36.101Z git:28-form-generator fixed issues
2019-05-31T06:44:13.476Z git:39-create-docker-image 0:06:06
2019-05-31T18:08:32.226Z git:39-create-docker-image 0:48:14
2019-06-03T07:00:48.776Z git:21-pen-configuration 0:30:59
2019-06-03T07:14:53.025Z git:21-pen-configuration Working on passing tool options to tool actions
2019-06-03T09:30:55.028Z git:39-create-docker-image 0:17:50
2019-06-03T17:35:17.565Z git:39-create-docker-image 0:33:03
2019-06-03T18:05:59.607Z git:21-pen-configuration 0:11:15
2019-06-05T07:33:57.068Z git:46-add-tests-and-typings 2:00:21
2019-06-05T07:34:21.301Z git:46-add-tests-and-typings Looking at testcafe api for interaction testing
2019-06-05T08:18:15.380Z git:21-pen-configuration 0:42:56
2019-06-05T19:24:44.657Z git:21-pen-configuration 2:56:08
2019-06-05T20:16:14.113Z git:21-pen-configuration 0:48:31
2019-06-05T20:52:26.598Z git:21-pen-configuration 0:20:14
2019-06-07T07:12:28.302Z git:101-images-on-spaces-draft 0:52:13
2019-06-07T07:17:46.966Z git:101-images-on-spaces-draft 0:05:17
2019-06-07T17:42:50.831Z git:21-pen-configuration 0:25:18
2019-06-07T17:42:50.831Z git:21-pen-configuration Fixed problemm with action call
2019-06-07T21:52:59.031Z git:101-images-on-spaces-draft 0:09:25
2019-06-13T06:39:59.757Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:00:29
2019-06-14T14:32:27.982Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:34:40
2019-06-14T17:03:46.723Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 1:04:42
2019-06-14T19:59:10.667Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:30:52
2019-06-17T16:18:41.922Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:30:27
2019-06-17T16:48:30.135Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:29:14
2019-06-18T09:42:06.532Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:01:06
2019-06-24T07:16:09.317Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 1:00:14
2019-06-24T17:44:05.547Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:29:31
2019-06-24T20:22:12.045Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:30:05
2019-06-25T07:20:16.982Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:22:08
2019-06-25T08:03:22.042Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:42:47
2019-06-25T17:16:14.759Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 1:06:29
2019-06-25T17:17:01.923Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 5Fixed panning with mouse
2019-06-26T22:00:39.005Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode Added global tool context
2019-06-26T22:06:56.306Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:27:17
2019-06-26T23:12:04.683Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:30:13
2019-06-27T08:03:44.148Z git:82-be-able-to-paste-text-into-text-node-while-in-edit-mode 0:20:23
2019-06-28T06:47:31.514Z git:101-images-on-spaces-draft 0:24:03
2019-06-28T08:02:09.037Z git:101-images-on-spaces-draft 0:30:57
2019-06-28T17:46:29.143Z git:101-images-on-spaces-draft 0:20:53
2019-07-01T06:22:06.433Z git:101-images-on-spaces-draft 0:07:14
2019-07-01T07:09:44.779Z git:12-click-action-pen-tool 0:37:54
2019-07-01T17:21:13.734Z git:12-click-action-pen-tool 0:12:35
2019-07-01T18:42:59.573Z git:24-image-insert-form 0:07:45
2019-07-02T06:28:58.567Z git:21-pen-configuration 0:16:16
2019-07-02T17:33:35.405Z git:21-pen-configuration 0:30:51
2019-07-03T06:44:32.724Z git:21-pen-configuration 0:21:52
2019-07-03T06:58:56.974Z git:21-pen-configuration 0:14:07
2019-07-03T17:10:19.494Z git:24-image-insert-form 00:19:13
2019-07-03T18:45:16.906Z git:24-image-insert-form 0:59:08
2019-07-04T08:02:51.038Z git:24-image-insert-form 1:01:58
2019-07-04T17:38:38.466Z git:24-image-insert-form 0:22:50
2019-07-04T17:53:38.466Z git:24-image-insert-form 0:10:50
2019-07-04T18:52:40.035Z git:24-image-insert-form 0:17:56
2019-07-05T07:04:49.210Z git:21-pen-configuration 0:38:59
2019-07-05T07:17:37.382Z git:24-image-insert-form 0:11:09
2019-07-05T07:17:37.382Z git:24-image-insert-form 0:02:09
2019-07-05T22:37:50.882Z git:21-pen-configuration 0:46:27
2019-07-08T08:13:35.414Z git:21-pen-configuration 0:15:12
2019-07-08T20:23:29.197Z git:21-pen-configuration Changed to use Proxy instead of getter for checking what values are used
2019-07-08T20:33:59.571Z git:21-pen-configuration 0:50:45
2019-07-08T20:34:18.024Z git:21-pen-configuration Got unselect button working
2019-07-08T20:42:24.154Z git:21-pen-configuration 0:08:18
2019-07-08T20:42:39.964Z git:21-pen-configuration Added styling for linkButton
2019-07-09T07:30:04.356Z git:24-image-insert-form 0:13:42
2019-07-10T06:35:07.075Z git:21-pen-configuration 0:20:55
......@@ -7,7 +7,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
## [0,2.1] - 2019-05-24
## [0.3.0] - 2019-07-12
### Added
- docker containers for containerised running and development
- automated development branch previewing for all
[merge requests](https://gitlab.com/MeldCE/first-draft/merge_requests)
- preview of the next segment for the click-aciton pen tool
- being able to paste text into a draft when the textbox tool is selected
- pen configuration that can change line properties for future and currently
selected lines
- insert images through either an image url or selecting them from your
computer
### Fixed
- Using special characters in draft ids
- Using special characters in images
## [0.2.1] - 2019-05-24
### Added
......@@ -42,12 +61,13 @@ Features:
- pen tool
- saving to JSON/folder
## 0.0.1 - 2019-03-02
## [0.0.1] - 2019-03-02
Initial placeholder release
[unreleased]: https://gitlab.com/MeldCE/first-draft/compare/v0,2.1...dev
[0,2.1]: https://gitlab.com/MeldCE/first-draft/compare/v0.1.3...v0,2.1
[unreleased]: https://gitlab.com/MeldCE/first-draft/compare/v0.3.0...dev
[0.3.0]: https://gitlab.com/MeldCE/first-draft/compare/v0.2.1...v0.3.0
[0.2.1]: https://gitlab.com/MeldCE/first-draft/compare/v0.1.3...v0.2.1
[0.1.3]: https://gitlab.com/MeldCE/first-draft/compare/v0.1.2...v0.1.3
[0.1.2]: https://gitlab.com/MeldCE/first-draft/compare/v0.0.1...v0.1.2
[0.0.1]: https://gitlab.com/MeldCE/first-draft/tree/v0.0.1
# first-draft 0.2.1
# first-draft 0.3.0
FOSS live drawing and annotation tool.
......@@ -88,6 +88,33 @@ first-draft
By default, the server listens on 127.0.0.1:7067. This can be changed by
setting the `HOST` and `PORT` environment variables.
### Docker
first-draft can also be run in container on any operating system using
[Docker](https://docker.com) or similar, and the docker images available from
[first-draft's container registry](https://gitlab.com/MeldCE/first-draft/container_registry).
The `first-draft` image can be used to run the latest version of first-draft.
first-draft will start on port 80 within the container and it will by
default use file storage look for drafts in the `/app/drafts` folder. It will
also look for a configuration file in `/app/config.js`.
You can run this image using docker run:
```
docker run -i -p 8080:80 -v $(pwd)/config.js:/app/config.js -v $(pwd)/drafts:/app/drafts registry.gitlab.com/meldce/first-draft
```
The `first-draft-dev` image can be used to develop first-draft.
Again, first-draft will start on port 80, but it will run development with
both server and client hot-reloading. It will look for drafts and a `config.js`
file in the same location, but also requires the `src` folder to be mapped
onto the container at `/app/src`:
```
docker run -i -p 8080:80 -v $(pwd)/config.js:/app/config.js -v $(pwd)/drafts:/app/drafts $(pwd)/src:/app/src registry.gitlab.com/meldce/first-draft-dev
```
### Configuration
A configuration file, `config.js` can be configure some basic settings in
......@@ -95,11 +122,13 @@ first-draft. The config file should be located where you start the first-draft
server from. Below is an example config file with a description of the
currently available parameters. More will come
```
```javascript
module.exports = {
// Password required to edit any drafts on the instance
// Default: no password
editPassword: 'notsosecretatthemoment',
// BaseURI for the drafts
// Default: "/d"
draftUri: '/d'
};
```
......@@ -239,7 +268,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
## [0,2.1] - 2019-05-24
## [0.3.0] - 2019-07-12
### Added
- docker containers for containerised running and development
- automated development branch previewing for all
[merge requests](https://gitlab.com/MeldCE/first-draft/merge_requests)
- preview of the next segment for the click-aciton pen tool
- being able to paste text into a draft when the textbox tool is selected
- pen configuration that can change line properties for future and currently
selected lines
- insert images through either an image url or selecting them from your
computer
### Fixed
- Using special characters in draft ids
- Using special characters in images
## [0.2.1] - 2019-05-24
### Added
......@@ -274,12 +322,13 @@ Features:
- pen tool
- saving to JSON/folder
## 0.0.1 - 2019-03-02
## [0.0.1] - 2019-03-02
Initial placeholder release
[unreleased]: https://gitlab.com/MeldCE/first-draft/compare/v0,2.1...dev
[0,2.1]: https://gitlab.com/MeldCE/first-draft/compare/v0.1.3...v0,2.1
[unreleased]: https://gitlab.com/MeldCE/first-draft/compare/v0.3.0...dev
[0.3.0]: https://gitlab.com/MeldCE/first-draft/compare/v0.2.1...v0.3.0
[0.2.1]: https://gitlab.com/MeldCE/first-draft/compare/v0.1.3...v0.2.1
[0.1.3]: https://gitlab.com/MeldCE/first-draft/compare/v0.1.2...v0.1.3
[0.1.2]: https://gitlab.com/MeldCE/first-draft/compare/v0.0.1...v0.1.2
[0.0.1]: https://gitlab.com/MeldCE/first-draft/tree/v0.0.1
module.exports = {
};
module.exports = {
// Password required to edit any drafts on the instance
// Default: no password
editPassword: 'notsosecretatthemoment',
// BaseURI for the drafts
// Default: "/d"
draftUri: '/d'
};
FROM node:10
RUN mkdir /app
WORKDIR /app
COPY package.json /app/
RUN npm install
ENV PORT 80
ENV HOST 0.0.0.0
EXPOSE 80
COPY LICENSE /app/
COPY README.md /app/
COPY CHANGELOG.md /app/
COPY config.example.js /app/
COPY config.blank.js /app/config.js
VOLUME [ "/app/src", "/app/drafts" ]
CMD npx ts-node-dev --prefer-ts /app/src/server/dev.ts
FROM node:10
ENV NODE_ENV production
RUN mkdir /app
WORKDIR /app
COPY package.json /app/
RUN npm install
LABEL maintainer="MeldCE Opensource"
ENV PORT 80
ENV HOST 0.0.0.0
EXPOSE 80
COPY LICENSE /app/
COPY README.md /app/
COPY CHANGELOG.md /app/
COPY config.blank.js /app/config.js
COPY lib /app/lib
COPY server /app/server
COPY dist /app/dist
VOLUME [ "/app/drafts" ]
CMD node /app/server/index.js
{
"name": "first-draft",
"version": "0.2.1",
"version": "0.3.0",
"description": "FOSS live drawing and annotation tool",
"scripts": {
"start": "server/index.js",
"build:app": "parcel build --detailed-report --no-source-maps src/app/index.html",
"build:server": "tsc",
"dev": "ts-node-dev src/server/dev.ts",
"dev": "ts-node-dev --prefer-ts src/server/dev.ts",
"readme": "utils/mdFileInclude src/README.md README.md",
"test": "echo \"Error: no test specified\" && exit 1",
"todo": "grep -B1 -A2 --exclude='*~' --exclude='.*.sw?' --color=always -rn TODO src | cut -c -300 | less -r"
"todo": "grep -B1 -A2 --exclude='*~' --exclude='.*.sw?' --color=always -rn TODO src | cut -c -300 | less -r",
"docker:prod": "docker build -f docker/prod/Dockerfile -t registry.gitlab.com/meldce/first-draft .",
"docker:dev": "docker build -f docker/dev/Dockerfile -t registry.gitlab.com/meldce/first-draft/dev .",
"docker:push": "docker push registry.gitlab.com/meldce/first-draft && docker push registry.gitlab.com/meldce/first-draft/dev"
},
"bin": {
"first-draft": "server/index.js"
......
......@@ -88,6 +88,33 @@ first-draft
By default, the server listens on 127.0.0.1:7067. This can be changed by
setting the `HOST` and `PORT` environment variables.
### Docker
first-draft can also be run in container on any operating system using
[Docker](https://docker.com) or similar, and the docker images available from
[first-draft's container registry](https://gitlab.com/MeldCE/first-draft/container_registry).
The `first-draft` image can be used to run the latest version of first-draft.
first-draft will start on port 80 within the container and it will by
default use file storage look for drafts in the `/app/drafts` folder. It will
also look for a configuration file in `/app/config.js`.
You can run this image using docker run:
```
docker run -i -p 8080:80 -v $(pwd)/config.js:/app/config.js -v $(pwd)/drafts:/app/drafts registry.gitlab.com/meldce/first-draft
```
The `first-draft-dev` image can be used to develop first-draft.
Again, first-draft will start on port 80, but it will run development with
both server and client hot-reloading. It will look for drafts and a `config.js`
file in the same location, but also requires the `src` folder to be mapped
onto the container at `/app/src`:
```
docker run -i -p 8080:80 -v $(pwd)/config.js:/app/config.js -v $(pwd)/drafts:/app/drafts $(pwd)/src:/app/src registry.gitlab.com/meldce/first-draft-dev
```
### Configuration
A configuration file, `config.js` can be configure some basic settings in
......@@ -95,13 +122,8 @@ first-draft. The config file should be located where you start the first-draft
server from. Below is an example config file with a description of the
currently available parameters. More will come
```
module.exports = {
// Password required to edit any drafts on the instance
editPassword: 'notsosecretatthemoment',
// BaseURI for the drafts
draftUri: '/d'
};
```javascript
<!=include config.example.js>
```
### Current Controls
......
......@@ -8,7 +8,7 @@
</head>
<body>
<div id="draft">
<div id="toolbar"></div>
<div id="toolbar" class="toolbar"></div>
<div id="canvasContainer">
<canvas id="canvas"></canvas>
<div id="canvasPosition"></div>
......
......@@ -2,6 +2,8 @@ import * as paper from 'paper/dist/paper-core';
import urlJoin from 'url-join';
import io from 'socket.io-client';
import { cleanId } from '../lib/id';
import { startDraft } from './modules/draft';
import { createStatus } from './modules/status';
import { createUser } from './modules/user';
......@@ -38,10 +40,21 @@ fetch('_config', {
}
} else {
id = match[1];
if (id !== cleanId(id)) {
id = cleanId(id);
history.replaceState(
{ id },
'',
urlJoin(config.baseUri, config.draftUri, id + '/')
);
}
}
const filteredSocket = {
emit: (event, data) => {
if (process.env.NODE_ENV === 'development') {
console.debug('%cReceived event', 'color: #090', event, data);
}
socket.emit(
event,
data
......@@ -54,6 +67,9 @@ fetch('_config', {
},
on: (event, handler) => {
socket.on(event, (data) => {
if (process.env.NODE_ENV === 'development') {
console.debug('%cReceived event', 'color: #090', event, data);
}
if (!data || data.senderId !== socket.id) {
handler(data);
}
......
......@@ -77,11 +77,15 @@ export const startDraft = (
console.error('error received', data);
});
app.socket.on('draft:current', (data: Messages.CurrentDrawing) => {
app.socket.on('draft:current', (data: Messages.DraftMessage) => {
if (data.draft) {
if (data.draft.name) {
document.title = `${data.draft.name} - first-draft`;
}
app.config = JSON.parse(JSON.stringify(config));
if (data.draft.config) {
Object.assign(data.draft.config);
}
}
if (initial) {
......
import { App } from '../../../typings/app';
import Paper from 'paper/dist/paper-core.js';
import * as DraftEvents from '../../../typings/Event';
import * as Draft from '../../../typings/Draft';
import * as DraftEvents from '../../../typings/Event';
import Paper from 'paper/dist/paper-core.js';
import ResizeObserver from 'resize-observer-polyfill';
import makeId from '../../../lib/id';
......@@ -37,6 +37,10 @@ export const createDrawing = (
draw: []
};
let lastPointerPosition = null;
let lastPointerCoordinates = null;
let lastPointerMovement = null;
const canvasCover = document.getElementById('canvasCover');
const rawEventHandlers = [];
......@@ -50,6 +54,44 @@ export const createDrawing = (
paper.settings.hitTolerance = 5;
canvas.addEventListener('mousemove', (event: Event) => {
const rect = canvas.getBoundingClientRect();
const newPointerPosition = new paper.Point(
event.clientX - rect.left,
event.clientY - rect.top
);
if (lastPointerPosition) {
lastPointerMovement = new paper.Point(
newPointerPosition.x - lastPointerPosition.x,
newPointerPosition.y - lastPointerPosition.y
);
}
lastPointerPosition = newPointerPosition;
lastPointerCoordinates = toDrawingCoordinates(event.clientX, event.clientY);
});
const touchPositionHandler = (event: Event) => {
if (event.touches.length) {
const rect = canvas.getBoundingClientRect();
const newPointerPosition = new paper.Point(
event.touches[0].clientX - rect.left,
event.touches[0].clientY - rect.top
);
if (lastPointerPosition) {
lastPointerMovement = new paper.Point(
newPointerPosition.x - lastPointerPosition.x,
newPointerPosition.y - lastPointerPosition.y
);
}
lastPointerPosition = newPointerPosition;
lastPointerCoordinates = toDrawingCoordinates(
event.touches[0].clientX,
event.touches[0].clientY
);
}
};
canvas.addEventListener('touchmove', touchPositionHandler);
canvas.addEventListener('touchstart', touchPositionHandler);
/**
* Update the center of the drawing based on the offset
*/
......@@ -74,9 +116,21 @@ export const createDrawing = (
updateCanvasPostion();
};
const updateZoom = (amount: number, point?: Point) => {
/**
* Update the zoom on the drawing to the given zoom, keeping the coordinate
* at the same pixel position if given.
*
* @param amount New zoom level
* @param point Coordinate to keep in same place when zooming
* @param Do not round to a minimum zoom value
*/
const updateZoom = (
amount: number,
point?: Point | null,
noMinLimit?: boolean
) => {
const currentZoom = paper.view.zoom;
const newZoom = Math.min(10, Math.max(0.05, amount));
const newZoom = Math.min(10, Math.max(noMinLimit ? 0 : 0.05, amount));
if (currentZoom !== newZoom) {
if (point) {
......@@ -215,7 +269,7 @@ export const createDrawing = (
viewSize.width / bounds.width,
viewSize.height / bounds.height
);
paper.view.zoom = scale;
updateZoom(scale, null, true);
offset = new paper.Point(-bounds.x, -bounds.y);
updateCenter();
}
......@@ -294,9 +348,10 @@ export const createDrawing = (
event.clientX,
event.clientY
);
drawingEvent.middlePixels = toDrawingCoordinates(
event.clientX,
event.clientY
const rect = canvas.getBoundingClientRect();
drawingEvent.middlePixels = new paper.Point(
event.clientX - rect.left,
event.clientY - rect.top
);
if (hitTestEvents.indexOf(event.type) !== -1) {
const hitResult = paper.project.hitTest(drawingEvent.middleCoordinates);
......@@ -336,6 +391,13 @@ export const createDrawing = (
}
}
if (event.type === 'mousemove' || event.type === 'touchmove') {
drawingEvent.movement = lastPointerMovement;
drawingEvent.coordinateMovement =
lastPointerMovement && lastPointerMovement.divide(paper.view.zoom);
}
drawingEvent.lastPointerPosition = lastPointerPosition;
// Select textbox group if textbox hit
if (
drawingEvent.item &&
......@@ -358,6 +420,28 @@ export const createDrawing = (
});
};
/**
* Convert an element into an object to be stored
*/
const makeObject = (element) => {
switch (element.type) {
case 'Path':
const json = element.exportJSON({ asString: false })[1];
const object = {
type: 'Path',
name: json.name,
segments: json.segments,
strokeColor: element.strokeColor.toCSS(true),
strokeCap: json.strokeCap,
strokeWidth: json.strokeWidth
};
if (json.opacity) {
object.opacity = json.opacity;
}
return object;
}
};
const emit = (type, event) => {
event.type = type;
// Used to identify the change
......@@ -384,6 +468,41 @@ export const createDrawing = (
}
};
const debounceElementsTimeouts = {};
const debounceElementsEmit = (action, elements) => {
if (!debounceElementsTimeouts[action]) {
debounceElementsTimeouts[action] = {
timeout: setTimeout(() => {
emit('draw', {
action: action,
elements: debounceElementsTimeouts[action].elements
});
delete debounceElementsTimeouts[action];
}, 100),
elements
};
} else {
for (let i = 0; i < elements.length; i++) {
// Find element in current elements
const index = debounceElementsTimeouts[action].elements.findIndex(
(element) =>
element.layerName === elements[i].layerName &&
element.element.name === elements[i].element.name
);
if (index !== -1) {
Object.assign(
debounceElementsTimeouts[action].elements[index],
elements[i]
);
} else {
debounceElementsTimeouts[action].elements.push(elements[i]);
}
}
}
};
const addImageLoader = (image) => {
if (imageLoadingStatus) {
const promise = new Promise((resolve, reject) => {
......@@ -532,6 +651,16 @@ export const createDrawing = (
textDiv.focus();
// Move caret to the end of the text
if (textbox.data.content) {
const range = document.createRange();
range.selectNodeContents(textDiv);
range.collapse(false);
const sel = getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
const inputHandler = (event) => {
// TODO Emit an edit event
emit('draw', {
......@@ -700,7 +829,11 @@ export const createDrawing = (
}
imagesLayer.activate();
const image = new paper.Raster(fileUrl);
// Encode URL
const encodedUrl = encodeURI(fileUrl);
const encodedUri = encodeURI(fileUri);
const image = new paper.Raster(encodedUrl);
image.name = id;
if (activeLayer) {
activeLayer.activate();
......@@ -713,7 +846,7 @@ export const createDrawing = (
asString: false
})[1];
image.selected = true;
emitObject.element.source = fileUri;
emitObject.element.source = encodedUri;
emitObject.element.matrix = rasterObject.matrix;
emit('draw', emitObject);
resolve(image);
......@@ -860,7 +993,11 @@ export const createDrawing = (
});
return;
} else if (data.action === 'deleteMultiple' && data.elements) {
} else if (data.action === 'deleteMultiple') {
if (!data.elements) {
return;
}
data.elements.forEach((element) => {
const layer = paper.project.layers.find(
(layer) => layer.name === element.layerName
......@@ -882,6 +1019,56 @@ export const createDrawing = (
item.remove();
});
return;
} else if (data.action === 'modifyMultiple') {
if (!data.elements) {
return;
}
data.elements.forEach((element) => {
const layer = paper.project.layers.find(
(layer) => layer.name === element.layerName
);
if (!layer) {
// TODO out of sync?
return;
}
// Try and find element if already in the layer
const index = layer.children.findIndex(
(child) => child.name === element.element.name
);
let item;
switch (element.element.type) {
case 'Textbox':
item = createTextbox(element.element);
break;
default:
item = layer.importJSON([element.element.type, element.element]);
}
console.log(
'layer',
layer,
'item',
item,
'index',
index,
'old item',
layer.children[index]
);
if (index === -1) {
if (typeof data.index === 'number') {
layer.insertChild(index, item);
} else {
layer.addChild(item);
}
} else {
layer.children[index].replaceWith(item);
}
});
return;
}
{
......@@ -1012,6 +1199,15 @@ export const createDrawing = (
}
}
},
get lastPointerPosition() {
return lastPointerPosition;
},
get lastPointerCoordinates() {
return lastPointerCoordinates;
},
get lastPointerMovement() {
return new paper.Point(lastPointerMovement);
},
get canvas() {
return canvas;
},
......@@ -1142,8 +1338,25 @@ export const createDrawing = (
active
});
},
setProperty: (items, property, value) => {
if (!Array.isArray(items)) {
items = [items];
}
const elements = [];
items.forEach((item) => {
item[property] = value;
elements.push({
layerName: item.parent.name,
element: makeObject(item)
});
});
debounceElementsEmit('modifyMultiple', elements);
},
createPath: (options = {}, share: boolean = true) => {
options = {
type: 'Path',
strokeColor: 'red',
strokeWidth: 2,
strokeCap: 'round',
......@@ -1152,7 +1365,11 @@ export const createDrawing = (
let timeout = null;
let lastActive = false;
paper.project.deselectAll();
const path = new paper.Path({ ...options, selected: true });
const path = new paper.Path({
...options,
selected:
typeof options.selected !== 'undefined' ? options.selected : true
});
const id = makeId();
path.name = id;
const layerName = paper.project.activeLayer.name;
......@@ -1231,9 +1448,12 @@ export const createDrawing = (
smooth: () => {
path.simplify(10);
},
finish: (noSimplify?: boolean) => {
if (!noSimplify) {
path.simplify(10);
finish: (simplify?: boolean | number | null) => {
if (simplify === null || simplify === true) {
simplify = 10;
}
if (simplify) {
path.simplify(simplify);
}
if (share) {
pathEmit();
......@@ -1345,10 +1565,7 @@ export const createDrawing = (
emit('draw', emitObject);
// Start editing the textBox if no text given
let edit = null;
if (!text) {
edit = editTextbox(textbox, true);
}
let edit = editTextbox(textbox, true);
// Reactivate current layer
if (annotationLayer) {
......
......@@ -36,7 +36,7 @@ export const createFileManager = (app: App) => {
return;
}
let id = makeId();
const id = makeId();
requests[id] = {
resolve,
......@@ -59,6 +59,7 @@ export const createFileManager = (app: App) => {
file: File,
replace: boolean = false
) => {
filename = encodeURIComponent(filename);
const url = urlJoin('/d', app.draftId, 'files', filename);
const fetchOptions = {
method: replace ? 'PATCH' : 'PUT',
......
import { Form, FormData } from '../../typings/Form';
export const createForm = (
form: Form,
container: HTMLElement,
originalValues?: FormData
) => {
let fields: {
input: HTMLInputElement;
[key: string]: HTMLElement;
} = {};
let submit = null;
let lastNeeded = {};
let submitLastNeeded = null;
let errorDiv = null;
let formElement = document.createElement('form');
formElement.className = 'form';
let values;
const createValueValidityCopies = () => {
let validities = {};
let needed = [];
const rejectFunction = (target, field) => {
if (process.env.NODE_ENV !== 'production') {
throw new Error('Cannot write properties on object directly');
}
};
const handlers = {
get: (target, field) => {
if (needed.indexOf(field) === -1) {
needed.push(field);
}
return target[field];
},
set: rejectFunction,
defineProperty: rejectFunction,
deleteProperty: rejectFunction
};
const currentValues = new Proxy(values, handlers);
Object.entries(fields).forEach(([field, inputs]) => {
validities[field] = inputs.input.validity;
});
const validity = new Proxy(validities, handlers);
return {
currentValues,
validity,
needed
};
};
const setAllowConfiguredButton = (name: string) => {
const element = form.elements.find((element) => element.name === name);
if (!element) {
throw new Error(`Could not find element ${name}`);
}
if (element.allowConfigured) {
if (values[name] !== null) {
fields[name].configured.innerText = `Use ${
element.allowConfigured
} value`;
fields[name].configured.disabled = false;
} else {
fields[name].configured.innerText = `Using ${
element.allowConfigured
} value`;
fields[name].configured.disabled = true;
}
}
};
const setAttribute = (field: string, attribute: string, value: any) => {
if (!fields[field]) {
return;
}
if (attribute === 'hidden') {
const el = fields[field].label || fields[field].input;
if (el) {
el.style.display = value ? 'none' : '';
}
return;
}
if (fields[field].input) {
fields[field].input.setAttribute(attribute, value);
}
};
const checkAttributeSetters = (field?: string) => {
// Create copy of values and validity
const { currentValues, validity, needed } = createValueValidityCopies();
form.elements.forEach((element) => {
if (!lastNeeded[element.name]) {
lastNeeded[element.name] = {};
}
if (
element.attributes &&
fields[element.name] &&
fields[element.name].input
) {
Object.entries(element.attributes).forEach(([attribute, value]) => {
if (typeof value === 'function') {
if (
!field ||
!lastNeeded[element.name][attribute] ||
lastNeeded[element.name][attribute].indexOf(field) !== -1
) {
setAttribute(
element.name,
attribute,
value(currentValues, validity)
);
lastNeeded[element.name][attribute] = needed.splice(0);
}
}
});
}
});
if (form.disabled && submit) {
if (
!field ||
!submitLastNeeded ||
submitLastNeeded.indexOf(field) !== -1
) {
submit.disabled = form.disabled(currentValues, validity);
submitLastNeeded = needed.splice(0);
}
}
};
const resetValues = () => {
if (!originalValues) {
values = {};
} else {
values = { ...originalValues };
}
form.elements.forEach((element) => {
if (!values.hasOwnProperty(element.name)) {
if (element.hasOwnProperty('default')) {
values[element.name] = element.default;
} else {
values[element.name] = null;
}
}
});
};
const formObject = {
reset: () => {
formElement.reset();
form.elements.forEach((element) => {
if (element.type === 'file' && fields[element.name]) {
fields[element.name].label.innerText = 'Choose a file';
}
});
resetValues();
},
get value() {
if (!formElement) {
throw new Error('Form removed');
}
return { ...values };
},
setValue: (name: string, value: any) => {
if (!formElement) {
throw new Error('Form removed');
}
values[name] = value;
checkAttributeSetters(name);
// Find field in form
const element = form.elements.find((element) => element.name === name);
if (!element) {
return;
}
// Set value
if (element.type === 'boolean') {
fields[name].input.checked = Boolean(value);
values[name] = Boolean(value);
} else {
fields[name].input.value = value;
values[name] = value;
}
},
remove: () => {
if (!formElement) {
throw new Error('Form removed');
}
formElement.remove();
formElement = null;
fields = null;
submit = null;
},
setMessage: (message: string, type?: string) => {
if (!formElement) {
throw new Error('Form removed');
}
errorDiv.innerText = message;
errorDiv.className = `message`;
if (type) {
errorDiv.className += ' ' + type;
}
},
clearMessage: () => {
if (!formElement) {
throw new Error('Form removed');
}
if (errorDiv.innerText) {
errorDiv.innerText = '';
}
}
};
formElement.addEventListener('submit', (event: Event) => {
event.preventDefault();
if (form.handler) {
const { currentValues, validity } = createValueValidityCopies();
form.handler(currentValues, validity, formObject);
}
});
const handleInput = (event: Event, clear: boolean) => {
let name;
if (event.target.type === 'range' && event.target.name.endsWith('-range')) {
name = event.target.name.slice(0, -6);
} else {
name = event.target.name;
}
if (!fields[name]) {
return;
}
// Find element
const element = form.elements.find((element) => element.name === name);
if (!element) {
return;
}
if (element.type === 'color' && clear) {
fields[name].input.value = null;
values[name] = null;
} else if (element.type === 'boolean') {
values[name] = fields[name].input.checked;
} else if (element.type === 'file') {
values[name] = fields[name].input.files;
} else if (
event.target.type === 'range' &&
event.target.name.endsWith('-range')
) {
values[name] = fields[name].range.value;
fields[name].input.value = values[name];
fields[name].input.setCustomValidity('');
} else {
if (element.type === 'number') {
let valid = true;
if (fields[name].input.validity.patternMismatch) {
fields[name].input.setCustomValidity('Please enter a number');
valid = false;
}
if (valid && element.attributes) {
const value = fields[name].input.value;
// Check validity of input as not checked natively
if (element.attributes.step) {
if (value % element.attributes.step !== 0) {
valid = false;
fields[name].input.setCustomValidity(
'Number must be a multiple of ' + element.attributes.step
);
}
}
const min =
typeof element.attributes.min === 'number'
? element.attributes.min
: null;
const max =
typeof element.attributes.max === 'number'
? element.attributes.max
: null;
if ((min !== null && value < min) || (max !== null && value > max)) {
valid = false;
let message = 'Number must be ';
if (min !== null && max !== null) {
message += `between ${min} and ${max}`;
} else if (min !== null) {
message += `greater than ${min}`;
} else {
message += `less than ${min}`;
}
fields[name].input.setCustomValidity(message);
} else {
fields[name].input.setCustomValidity('');
}
}
}
values[name] = fields[name].input.value;
if (element.type === 'number' && element.range) {
fields[name].range.value = values[name];
}
}
if (element.allowConfigured) {
setAllowConfiguredButton(name);
}
if (form.input) {
form.input(name, values[name], fields[name].input.validity);
}
// Run attribute mappers
checkAttributeSetters(name);
};
formElement.addEventListener('input', handleInput);
{
let el: HTMLElement, input: HTMLElement;
if (form.title) {
el = document.createElement('h1');
el.innerHTML = form.title;
formElement.appendChild(el);
}
if (form.description) {
const parts = form.description.split('\n\n');
parts.forEach((part) => {
el = document.createElement('p');
el.innerHTML = part;
formElement.appendChild(el);
});
}
// Create value
resetValues();
if (form.instructions) {
const parts = form.instructions.split(/\n+/g);
parts.forEach((part) => {
el = document.createElement('p');
el.innerHTML = part;
formElement.appendChild(el);
});
}
form.elements.forEach((element) => {
if (element.type === 'button') {
input = document.createElement('button');
input.type = 'button';
input.innerText = element.label;
fields[element.name] = { input };
if (element.attributes) {
for (let attribute in element.attributes) {
if (typeof value !== 'function') {
setAttribute(
element.name,
attribute,
element.attributes[attribute]
);
}
}
}
if (element.handler) {
input.addEventListener('click', element.handler);
}
formElement.appendChild(input);
return;
}
el = document.createElement('label');
if (element.tooltip) {
el.title = element.tooltip;
}
input = document.createElement('input');
fields[element.name] = {
input,
label: el
};
input.name = element.name;
if (element.type === 'boolean') {
input.type = 'checkbox';
el.appendChild(input);
if (element.label) {
el.appendChild(document.createTextNode(element.label));
}
if (values[element.name]) {
input.checked = true;
}
} else if (element.type === 'file') {
el.appendChild(input);
if (element.label) {
el.appendChild(document.createTextNode(element.label));
}
input.type = 'file';
el.className = 'file';
const fileLabel = document.createElement('span');
fields[element.name].label = fileLabel;
const fileInput = input;
el.appendChild(fileLabel);
fileLabel.innerText = 'Choose a file';
input.addEventListener('change', (event) => {
if (fileInput.files.length) {
fileLabel.innerText = fileInput.files[0].name;
if (fileInput.files.length > 2) {
fileLabel.innerText +=
' and ' + (fileInput.files.length - 1) + ' others';
} else if (fileInput.files.length === 2) {
fileLabel.innerText += ' and 1 other';
}
} else {
fileLabel.innerText = 'Choose a file';
}
});
} else {
switch (element.type) {
case 'number':
input.type = 'tel';
if (input.step && input.step >= 1) {
input.pattern = '[0-9]*';
} else {
input.pattern = '[0-9]*(.[0-9]+)?';
}
break;
case 'color':
case 'password':
case 'url':
input.type = element.type;
break;
case 'email':
input.type = 'email';
input.pattern =
"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$";
break;
}
if (element.attributes) {
Object.entries(element.attributes).forEach(([attribute, value]) => {
if (typeof value !== 'function') {
setAttribute(
element.name,
attribute,
element.attributes[attribute]
);
}
});
}
if (element.label) {
el.appendChild(document.createTextNode(element.label));
}
if (element.type === 'number' && element.range) {
const range = document.createElement('input');
range.type = 'range';
range.name = `${element.name}-range`;
if (element.attributes) {
if (element.attributes.step) {
range.step = element.attributes.step;
}
if (element.attributes.min) {
range.min = element.attributes.min;
}
if (element.attributes.max) {
range.max = element.attributes.max;
}
}
fields[element.name].range = range;
if (
typeof values[element.name] !== 'undefined' &&
values[element.name] !== null
) {
range.value = values[element.name];
}
el.appendChild(range);
}
el.appendChild(input);
if (
typeof values[element.name] !== 'undefined' &&
values[element.name] !== null
) {
input.value = values[element.name];
}
if (element.allowConfigured) {
input = document.createElement('button');
input.className = 'allowConfigured';
input.name = element.name;
input.addEventListener('click', (event) => {
handleInput(event, true);
});
fields[element.name].configured = input;
setAllowConfiguredButton(element.name);
el.appendChild(input);
}
}
if (element.attributes) {
for (let attribute in element.attributes) {
if (typeof element.attributes[attribute] !== 'function') {
setAttribute(
element.name,
attribute,
element.attributes[attribute]
);
}
}
}
el.appendChild(input);
formElement.appendChild(el);
if (element.description) {
el = document.createElement('div');
el.className = 'description';
el.innerHTML = element.description;
}
});
// Error message div
errorDiv = document.createElement('div');
errorDiv.className = 'message';
formElement.appendChild(errorDiv);
el = document.createElement('div');
el.className = 'buttons';
formElement.appendChild(el);
if (form.buttons) {
form.buttons.forEach((button) => {
if (button.type) {
input = document.createElement('input');
input.type = button.type;
} else {
input = document.createElement('button');
}
input.value = button.label;
el.appendChild(input);
});
} else if (form.handler) {
input = document.createElement('input');
input.type = 'submit';
input.value = 'Submit';
submit = input;
el.appendChild(input);
}
}
checkAttributeSetters();
container.appendChild(formElement);
return formObject;
};
export const createOverlay = (container: HTMLElement) => {
const overlay = document.createElement('div');
overlay.className = 'cover';
const overlayContainer = document.createElement('div');
overlayContainer.className = 'overlay';
overlay.appendChild(overlayContainer);
container.appendChild(overlay);
return {
close: () => {
overlay.remove();
},
get element() {
return overlayContainer;
}
};
};
This diff is collapsed.
......@@ -2,6 +2,8 @@ import createSha from '../../lib/sha';
import makeId from '../../lib/id';
import { arrayContainsValues } from '../../lib/utils';
import createEventer from '../../lib/eventer';
import { createForm } from './form';
import { createOverlay } from './overlay';
const userItem = 'user';
const hashItem = 'id';
......@@ -23,9 +25,7 @@ export const createUser = (app) => {
let user, nonce;
let waitingForChallenge;
const sha = createSha();
let loginListeners = null;
let loginDiv = null;
let errorDiv = null;
let login = null;
let challenge = null;
let authenticateId = null;
const containerElement = document.getElementById('canvasContainer');
......@@ -39,16 +39,10 @@ export const createUser = (app) => {
* Clear and close login screen
*/
const clearLogin = () => {
if (loginListeners) {
loginListeners.forEach(({ type, el, handler }) => {
el.removeEventListener(type, handler);
});
loginListeners = null;
}
if (loginDiv) {
loginDiv.remove();
loginDiv = null;
errorDiv = null;
if (login) {
login.login.remove();
login.overlay.close();
login = null;
}
};
......@@ -190,178 +184,114 @@ export const createUser = (app) => {
* cancelled authenticating
*/
const showLogin = (requirePassword?: boolean) => {
if (loginDiv) {
if (login) {
return;
}
loginListeners = [];
let listener;
if (requirePassword && !challenge) {
const challengeId = makeId();
app.socket.emit('draft:auth:challenge', { id: challengeId });
waitingForChallenge = { id: challengeId };
}
loginDiv = document.createElement('div');
loginDiv.className = 'cover';
const box = document.createElement('div');
box.className = 'login';
loginDiv.appendChild(box);
box.innerHTML = '<h1>Your Details</h1>';
let userInput;
let rememberMeInput;
const valid = () => {
submit.disabled = !(
(app.config.editPassword ||
app.config.hideUsername ||
userInput.value) &&
(!requirePassword || passwordInput.value)
);
};
if (!app.config.hideUsername) {
const userLabel = document.createElement('label');
box.appendChild(userLabel);
userLabel.innerText = 'Username';
userInput = document.createElement('input');
if (user) {
userInput.value = user.username || user.name || '';
}
userInput.addEventListener('input', valid);
loginListeners.push({ type: 'input', el: userInput, handler: valid });
userLabel.appendChild(userInput);
const rememberMeLabel = document.createElement('label');
box.appendChild(rememberMeLabel);
rememberMeLabel.appendChild(document.createTextNode('Remember Me'));
const rememberMeInput = document.createElement('input');
rememberMeLabel.className = 'remember';
rememberMeInput.type = 'checkbox';
rememberMeInput.checked = rememberMe;
rememberMeLabel.appendChild(rememberMeInput);
rememberMeInput.addEventListener('change', () => {
rememberMe = rememberMeInput.checked;
});
}
const passwordLabel = document.createElement('label');
box.appendChild(passwordLabel);
passwordLabel.innerText = 'Password';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.required = requirePassword;
passwordLabel.appendChild(passwordInput);
listener = (event) => {
if (errorDiv && errorDiv.innerText) {
errorDiv.innerText = '';
}
if (passwordInput.value) {
rememberPasswordInput.disabled = false;
} else {
rememberPasswordInput.disabled = true;
}
};
passwordInput.addEventListener('input', listener);
passwordInput.addEventListener('input', valid);
loginListeners.push({
type: 'input',
el: passwordInput,
handler: listener
});
loginListeners.push({ type: 'input', el: passwordInput, handler: valid });
const rememberPasswordLabel = document.createElement('label');
box.appendChild(rememberPasswordLabel);
rememberPasswordLabel.appendChild(
document.createTextNode('Remember Password')
);
rememberPasswordLabel.className = 'remember';
const rememberPasswordInput = document.createElement('input');
rememberPasswordInput.type = 'checkbox';
rememberPasswordInput.checked = rememberPassword;
rememberPasswordInput.disabled = true;
rememberPasswordInput.addEventListener('change', () => {
rememberPassword = rememberPasswordInput.checked;
if (rememberPassword) {
rememberMeInput.disabled = true;
rememberMeInput.checked = true;
} else {
rememberMeInput.disabled = false;
}
});
rememberPasswordLabel.appendChild(rememberPasswordInput);
errorDiv = document.createElement('div');
box.appendChild(errorDiv);
errorDiv.className = 'error';
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'buttons';
box.appendChild(buttonsDiv);
const submit = document.createElement('button');
submit.innerText = 'Submit';
listener = (event) => {
const username = userInput && userInput.value;
if (passwordInput.value) {
hash = {
value: sha.getSha(passwordInput.value)
};
if (challenge) {
const loginForm = {
title: 'Your Details',
elements: [
{
name: 'password',
type: 'password',
label: 'Password',
attributes: {
autocomplete: 'current-password'
}
},
{
name: 'rememberPassword',
type: 'boolean',
label: 'Remember Password',
attributes: {
disabled: (data) => {
return !data.password;
}
}
}
],
handler: (values, validity) => {
rememberMe = values.rememberMe;
rememberPassword = values.rememberPassword;
const username = values.username || null;
if (values.password) {
hash = {
value: sha.getSha(values.password)
};
if (challenge) {
authenticateId = makeId();
app.socket.emit('draft:auth:authenticate', {
id: authenticateId,
username,
challenge,
answer: sha.signChallenge(challenge, hash.value)
});
challenge = null;
} else {
const id = makeId();
waitingForChallenge = {
id,
handler: (data) => {
authenticateId = makeId();
app.socket.emit('draft:auth:authenticate', {
id: authenticateId,
username,
challenge: data.challenge,
answer: sha.signChallenge(data.challenge, hash.value)
});
}
};
app.socket.emit('draft:auth:challenge', {
id,
username
});
}
} else {
authenticateId = makeId();
app.socket.emit('draft:auth:authenticate', {
id: authenticateId,
username,
challenge,
answer: sha.signChallenge(challenge, hash.value)
});
challenge = null;
} else {
const id = makeId();
waitingForChallenge = {
id,
handler: (data) => {
authenticateId = makeId();
app.socket.emit('draft:auth:authenticate', {
id: authenticateId,
username,
challenge: data.challenge,
answer: sha.signChallenge(data.challenge, hash.value)
});
}
};
app.socket.emit('draft:auth:challenge', {
id,
username
});
}
} else {
authenticateId = makeId();
app.socket.emit('draft:auth:authenticate', {
id: authenticateId,
username
});
}
},
disabled: (data) =>
!(
(app.config.editPassword ||
app.config.hideUsername ||
data.username) &&
(!requirePassword || data.password)
)
};
submit.addEventListener('click', listener);
loginListeners.push({ type: 'click', el: submit, handler: listener });
submit.disabled = true;
buttonsDiv.appendChild(submit);
if (!requirePassword && !app.config.getUser) {
const cancel = document.createElement('button');
cancel.innerText = 'Cancel';
listener = (event) => {
clearLogin();
};
cancel.addEventListener('click', listener);
loginListeners.push({ type: 'click', el: cancel, handler: listener });
buttonsDiv.appendChild(cancel);
if (!app.config.hideUsername) {
loginForm.elements.unshift(
{
name: 'username',
label: 'Username'
},
{
name: 'rememberMe',
type: 'boolean',
label: 'Remember me',
attributes: {
disabled: (data) => {
return !data.username;
}
}
}
);
}
containerElement.appendChild(loginDiv);
const overlay = createOverlay(containerElement);
login = {
overlay,
login: createForm(loginForm, overlay.element, {
username: (user && (user.username || user.name)) || '',
rememberMe,
rememberPassword
})
};
let resolve, reject;
const promise = new Promise((promiseResolve, promiseReject) => {
......@@ -393,8 +323,8 @@ export const createUser = (app) => {
app.socket.on('draft:auth:authenticate', (data) => {
challenge = data.challenge;
if (data.goodUser && errorDiv) {
errorDiv.innerText = 'Password required';
if (data.goodUser && login) {
login.login.showMessage('Password required');
}
showLogin(data.requirePassword || data.goodUser);
});
......@@ -417,8 +347,8 @@ export const createUser = (app) => {
clearLogin();
} else {
clearHash();
if (errorDiv) {
errorDiv.innerText = 'Please try again';
if (login) {
login.login.setMessage('Please try again', 'error');
}
if (!data.noUser) {
showLogin(true);
......