start of complete rewrite on top of vue3
This introduces a new build structure and a new notification system.
|
@ -1,5 +0,0 @@
|
||||||
[*.{js,jsx,ts,tsx,vue}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Feature parity with RPG-Cards by Crobi
|
|
||||||
|
|
||||||
RPG-Cards by Crobi was the original inspiration to create this program.
|
|
||||||
|
|
||||||
see https://crobi.github.io/rpg-cards
|
|
||||||
|
|
||||||
## Card generation
|
|
||||||
|
|
||||||
[x] subtitle
|
|
||||||
[x] rule
|
|
||||||
[x] property
|
|
||||||
[x] description
|
|
||||||
[x] text
|
|
||||||
[x] subsection
|
|
||||||
[x] boxes
|
|
||||||
[x] dndstats
|
|
||||||
[x] fill
|
|
||||||
[x] bullet
|
|
||||||
[ ] picture: not supported and for now not planned
|
|
BIN
docs/scrot.jpg
Before Width: | Height: | Size: 63 KiB |
BIN
docs/scrot2.jpg
Before Width: | Height: | Size: 165 KiB |
21
html.config.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "vue3-app-starter",
|
||||||
|
"meta": {
|
||||||
|
"viewport": "width=device-width,initial-scale=1.0",
|
||||||
|
"description": "vue3 app starter with typescript support"
|
||||||
|
},
|
||||||
|
"logo": "./src/assets/logo.svg",
|
||||||
|
"favicons": {
|
||||||
|
"icons": {
|
||||||
|
"favicons": true,
|
||||||
|
"android": true,
|
||||||
|
"appleIcon": true,
|
||||||
|
"appleStartup": false,
|
||||||
|
"coast": false,
|
||||||
|
"firefox": false,
|
||||||
|
"yandex": false,
|
||||||
|
"windows": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"template": "./public/index.html"
|
||||||
|
}
|
81
package.json
|
@ -1,67 +1,36 @@
|
||||||
{
|
{
|
||||||
"name": "rpg-cards-ng",
|
"name": "rpg-cards-ng",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"description": "next gen rpg card app",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"author": "koehr <n@koehr.in>",
|
||||||
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "webpack-dev-server",
|
||||||
"build": "vue-cli-service build",
|
"build": "webpack --env.prod --progress"
|
||||||
"lint": "vue-cli-service lint"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.6.4",
|
"vue": "3.0.0-beta.15",
|
||||||
"register-service-worker": "^1.6.2",
|
"vue-router": "4.0.0-alpha.12"
|
||||||
"vue": "^2.6.11",
|
|
||||||
"vue-router": "^3.1.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@editorjs/editorjs": "^2.18.0",
|
"@vue/compiler-sfc": "3.0.0-beta.15",
|
||||||
"@editorjs/list": "^1.5.0",
|
"copy-webpack-plugin": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.2.0",
|
"css-loader": "^3.6.0",
|
||||||
"@typescript-eslint/parser": "^3.2.0",
|
"favicons-webpack-plugin": "^3.0.1",
|
||||||
"@vue/cli-plugin-babel": "^4.2.0",
|
"file-loader": "^6.0.0",
|
||||||
"@vue/cli-plugin-eslint": "^4.2.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"@vue/cli-plugin-pwa": "^4.2.0",
|
"raw-loader": "^4.0.1",
|
||||||
"@vue/cli-plugin-typescript": "^4.2.0",
|
"style-loader": "^1.2.0",
|
||||||
"@vue/cli-service": "^4.2.0",
|
"ts-loader": "^7.0.5",
|
||||||
"@vue/eslint-config-standard": "^5.1.2",
|
"typescript": "^3.9.5",
|
||||||
"@vue/eslint-config-typescript": "^5.0.2",
|
"url-loader": "^4.1.0",
|
||||||
"eslint": "^7.2.0",
|
"vue-loader": "16.0.0-beta.3",
|
||||||
"eslint-plugin-import": "^2.21.2",
|
"webpack": "^4.43.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"webpack-cli": "^3.3.11",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
"webpack-subresource-integrity": "^1.4.1",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"yarn": "^1.22.4"
|
||||||
"lint-staged": "^9.5.0",
|
|
||||||
"raw-loader": "^4.0.0",
|
|
||||||
"typescript": "~3.9.5",
|
|
||||||
"vue-property-decorator": "^8.5.0",
|
|
||||||
"vue-template-compiler": "^2.6.11"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:vue/essential",
|
|
||||||
"@vue/standard",
|
|
||||||
"@vue/typescript/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2020
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"browserslist": [
|
|
||||||
"> 1%",
|
|
||||||
"IE > 11"
|
|
||||||
],
|
|
||||||
"gitHooks": {
|
|
||||||
"pre-commit": "lint-staged"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.{js,jsx,vue,ts,tsx}": [
|
|
||||||
"vue-cli-service lint",
|
|
||||||
"git add"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.2 KiB |
|
@ -1,149 +0,0 @@
|
||||||
<?xml version="1.0" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
||||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
|
|
||||||
preserveAspectRatio="xMidYMid meet">
|
|
||||||
<metadata>
|
|
||||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
|
|
||||||
fill="#000000" stroke="none">
|
|
||||||
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
|
|
||||||
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
|
|
||||||
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
|
|
||||||
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
|
|
||||||
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
|
|
||||||
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
|
|
||||||
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
|
|
||||||
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
|
|
||||||
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
|
|
||||||
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
|
|
||||||
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
|
|
||||||
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
|
|
||||||
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
|
|
||||||
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
|
|
||||||
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
|
|
||||||
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
|
|
||||||
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
|
|
||||||
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
|
|
||||||
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
|
|
||||||
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
|
|
||||||
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
|
|
||||||
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
|
|
||||||
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
|
|
||||||
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
|
|
||||||
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
|
|
||||||
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
|
|
||||||
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
|
|
||||||
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
|
|
||||||
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
|
|
||||||
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
|
|
||||||
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
|
|
||||||
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
|
|
||||||
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
|
|
||||||
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
|
|
||||||
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
|
|
||||||
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
|
|
||||||
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
|
|
||||||
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
|
|
||||||
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
|
|
||||||
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
|
|
||||||
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
|
|
||||||
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
|
|
||||||
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
|
|
||||||
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
|
|
||||||
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
|
|
||||||
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
|
|
||||||
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
|
|
||||||
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
|
|
||||||
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
|
|
||||||
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
|
|
||||||
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
|
|
||||||
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
|
|
||||||
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
|
|
||||||
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
|
|
||||||
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
|
|
||||||
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
|
|
||||||
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
|
|
||||||
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
|
|
||||||
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
|
|
||||||
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
|
|
||||||
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
|
|
||||||
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
|
|
||||||
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
|
|
||||||
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
|
|
||||||
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
|
|
||||||
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
|
|
||||||
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
|
|
||||||
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
|
|
||||||
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
|
|
||||||
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
|
|
||||||
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
|
|
||||||
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
|
|
||||||
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
|
|
||||||
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
|
|
||||||
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
|
|
||||||
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
|
|
||||||
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
|
|
||||||
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
|
|
||||||
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
|
|
||||||
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
|
|
||||||
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
|
|
||||||
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
|
|
||||||
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
|
|
||||||
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
|
|
||||||
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
|
|
||||||
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
|
|
||||||
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
|
|
||||||
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
|
|
||||||
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
|
|
||||||
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
|
|
||||||
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
|
|
||||||
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
|
|
||||||
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
|
|
||||||
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
|
|
||||||
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
|
|
||||||
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
|
|
||||||
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
|
|
||||||
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
|
|
||||||
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
|
|
||||||
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
|
|
||||||
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
|
|
||||||
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
|
|
||||||
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
|
|
||||||
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
|
|
||||||
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
|
|
||||||
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
|
|
||||||
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
|
|
||||||
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
|
|
||||||
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
|
|
||||||
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
|
|
||||||
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
|
|
||||||
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
|
|
||||||
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
|
|
||||||
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
|
|
||||||
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
|
|
||||||
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
|
|
||||||
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
|
|
||||||
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
|
|
||||||
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
|
|
||||||
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
|
|
||||||
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
|
|
||||||
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
|
|
||||||
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
|
|
||||||
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
|
|
||||||
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
|
|
||||||
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
|
|
||||||
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
|
|
||||||
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
|
|
||||||
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
|
|
||||||
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
|
|
||||||
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
|
|
||||||
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
|
|
||||||
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
|
|
||||||
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
|
|
||||||
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
|
|
||||||
-9615 0 20 -32z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 10 KiB |
|
@ -3,15 +3,6 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon-32.png" sizes="32x32">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon-128.png" sizes="128x128">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon-192.png" sizes="192x192">
|
|
||||||
<link rel="shortcut icon" href="<%= BASE_URL %>favicon-196.png" sizes="196x196">
|
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>favicon-152.png" sizes="152x152">
|
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>favicon-180.png" sizes="180x180">
|
|
||||||
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -20,15 +11,5 @@
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
<script>
|
|
||||||
window.goatcounter = {no_onload: true}
|
|
||||||
window.addEventListener('hashchange', function(e) {
|
|
||||||
window.goatcounter.count({
|
|
||||||
path: location.pathname + location.search + location.hash
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<script data-goatcounter="https://rpg-cards-ng.goatcounter.com/count"
|
|
||||||
async src="//gc.zgo.at/count.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
58
src/App.vue
|
@ -1,20 +1,52 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<router-link class="home-link" to="/">
|
||||||
<router-link class="home-link" to="/">
|
<Logo />
|
||||||
<Logo />
|
</router-link>
|
||||||
</router-link>
|
|
||||||
|
<Notifications :notifications="notifications" @dismiss="dismissNotification" />
|
||||||
|
|
||||||
|
<main>
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang='ts'>
|
||||||
import { Component, Vue } from 'vue-property-decorator'
|
import { defineComponent } from 'vue'
|
||||||
import Logo from '@/components/logo.vue'
|
import { useState } from '@/state'
|
||||||
@Component({
|
import Logo from '@/components/Logo.vue'
|
||||||
components: { Logo }
|
import Notifications from '@/components/Notifications.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup () {
|
||||||
|
const { collection: notifications, actions } = useState('notifications')
|
||||||
|
return {
|
||||||
|
notifications,
|
||||||
|
addNotification: actions.add,
|
||||||
|
dismissNotification: actions.dismiss
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { Notifications, Logo },
|
||||||
|
watch: {
|
||||||
|
'$route' (newRoute) {
|
||||||
|
const bodyEl = document.body
|
||||||
|
bodyEl.className = "" // TODO: is this really the way to go here?
|
||||||
|
|
||||||
|
const bodyClass = newRoute.meta.bodyClass
|
||||||
|
if (bodyClass) bodyEl.classList.add(bodyClass)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.addNotification({
|
||||||
|
level: 'warning',
|
||||||
|
title: 'This is a pre-alpha version.',
|
||||||
|
content: 'Many features are still unstable or completely missing. Check out <a href="https://github.com/nkoehring/rpg-cards-ng/">the code repository</a> for more information.'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.addNotification({
|
||||||
|
content: 'Click the PLUS to create a new deck of cards.'
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
export default class App extends Vue {
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style src="@/assets/app.css" />
|
<style src='@/assets/app.css' />
|
||||||
|
|
|
@ -1,31 +1,45 @@
|
||||||
:root {
|
:root {
|
||||||
--card-height: 35rem;
|
--card-height: 35rem;
|
||||||
|
--background-color: #222;
|
||||||
|
--foreground-color: #CCC;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,body {
|
html,body {
|
||||||
display: block;
|
display: block;
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #222;
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #CCC;
|
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--foreground-color);
|
||||||
|
}
|
||||||
|
body.print {
|
||||||
|
color: black;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
display: block;
|
max-width: 90rem;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
max-width: 90rem;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app > .home-link {
|
#app > main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#app .home-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1vh;
|
top: 1rem;
|
||||||
left: 1vw;
|
left: 1rem;
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.15em;
|
||||||
|
border-bottom: 1px dotted white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
#logo {
|
||||||
|
@ -62,8 +76,17 @@ section[name=notifications] {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 70rem;
|
max-width: 70rem;
|
||||||
margin: 0 auto 1em;
|
margin: 0 auto 1em;
|
||||||
padding: 1rem 3rem;
|
}
|
||||||
border: .5em solid red;
|
section[name=notifications] > .note {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
background-color: #0006;
|
||||||
|
border: .5em solid #000;
|
||||||
|
}
|
||||||
|
section[name=notifications] > .warning {
|
||||||
|
border-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popup {
|
#popup {
|
||||||
|
|
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,8 +0,0 @@
|
||||||
import Component from 'vue-class-component'
|
|
||||||
|
|
||||||
// Register the router hooks with their names
|
|
||||||
Component.registerHooks([
|
|
||||||
'beforeRouteEnter',
|
|
||||||
'beforeRouteLeave',
|
|
||||||
'beforeRouteUpdate'
|
|
||||||
])
|
|
8
src/components/AlphaWarning.vue
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<p class="note warning">
|
||||||
|
<strong>This is a pre-alpha version.</strong>
|
||||||
|
Many features are still unstable or completely missing.
|
||||||
|
<br />
|
||||||
|
Check out <a href="https://github.com/nkoehring/rpg-cards-ng/">the code repository</a> for more information.
|
||||||
|
</p>
|
||||||
|
</template>
|
24
src/components/CardBack.vue
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<img :src="iconPath" alt="card icon" />
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed } from 'vue'
|
||||||
|
import iconPath from '@/lib/iconPath'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CardBack',
|
||||||
|
props: {
|
||||||
|
icon: String,
|
||||||
|
color: String
|
||||||
|
},
|
||||||
|
setup (props) {
|
||||||
|
return {
|
||||||
|
iconPath: computed(() => iconPath(props.icon || 'plus'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -37,11 +37,3 @@
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from 'vue-property-decorator'
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class DeckCard extends Vue {
|
|
||||||
}
|
|
||||||
</script>
|
|
27
src/components/Notifications.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<section name="notifications">
|
||||||
|
<p class="note" :class="note.level" v-for="note in notDismissedNotes">
|
||||||
|
<strong>{{ note.title }}</strong>
|
||||||
|
<div v-html="note.content" />
|
||||||
|
<button @click="$emit('dismiss', note)">dismiss</button>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { Notification } from '@/types'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Notifications',
|
||||||
|
props: {
|
||||||
|
notifications: Array
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
notDismissedNotes (): Notification[] {
|
||||||
|
const notes = this.notifications as Notification[]
|
||||||
|
return notes.filter(note => !note.dismissed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -1,49 +0,0 @@
|
||||||
<template>
|
|
||||||
<main ref="cardEl" class="card-content"></main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
|
||||||
import { Card } from '@/types'
|
|
||||||
|
|
||||||
import Editor from '@editorjs/editorjs'
|
|
||||||
import List from '@editorjs/list'
|
|
||||||
import { Heading, Delimiter, Charges, DnDStats } from '@/editor'
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class DeckCardEditor extends Vue {
|
|
||||||
@Prop() public readonly cardId!: string
|
|
||||||
@Prop() public readonly active!: boolean
|
|
||||||
@Prop() public readonly content!: Card['content']
|
|
||||||
|
|
||||||
private editor!: Editor
|
|
||||||
|
|
||||||
private get id () {
|
|
||||||
return `${this.cardId}-editor`
|
|
||||||
}
|
|
||||||
|
|
||||||
private mounted () {
|
|
||||||
this.editor = new Editor({
|
|
||||||
holder: this.$refs.cardEl as HTMLElement,
|
|
||||||
autofocus: false,
|
|
||||||
tools: {
|
|
||||||
list: { class: List, inlineToolbar: true },
|
|
||||||
heading: { class: Heading, inlineToolbar: true },
|
|
||||||
delimiter: { class: Delimiter, inlineToolbar: false },
|
|
||||||
charges: { class: Charges, inlineToolbar: false },
|
|
||||||
dndstats: { class: DnDStats, inlineToolbar: false }
|
|
||||||
},
|
|
||||||
data: this.content,
|
|
||||||
placeholder: 'Click here to write your card.',
|
|
||||||
onChange: () => {
|
|
||||||
console.log('editor change, saving')
|
|
||||||
this.editor.save().then(value => {
|
|
||||||
this.$emit('change', { field: 'content', value })
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('error saving data', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,187 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:id="card.id"
|
|
||||||
class="flip-card card"
|
|
||||||
:class="{ active: isSelection }"
|
|
||||||
:style="containerStyle"
|
|
||||||
@click="clickUnlessSelected">
|
|
||||||
<div class="active-background" @click.self.stop="$emit('close')" />
|
|
||||||
<button class="action-close" @click.self.stop="$emit('close')" v-if="isSelection" />
|
|
||||||
<section name="card-front" class="card-front">
|
|
||||||
<header>
|
|
||||||
<h1 :contenteditable="isSelection"
|
|
||||||
@blur="editField('name', $event)"
|
|
||||||
@keypress.enter.prevent="editField('name', $event)">
|
|
||||||
{{ card.name }}
|
|
||||||
</h1>
|
|
||||||
<img :src="icon" />
|
|
||||||
</header>
|
|
||||||
<deck-card-editor
|
|
||||||
:card-id="card.id"
|
|
||||||
:active="isSelection"
|
|
||||||
:content="card.content"
|
|
||||||
@change="$emit('edit', $event)"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section name="card-back" class="card-back">
|
|
||||||
<div class="icon-wrapper">
|
|
||||||
<img :src="backIcon" />
|
|
||||||
</div>
|
|
||||||
<button @click="$emit('click')">edit card</button>
|
|
||||||
<button @click="$emit('delete')">delete card</button>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
|
||||||
import { Deck, Card } from '@/types'
|
|
||||||
import { cardSizeToStyle, iconPath } from '@/lib'
|
|
||||||
import DeckCardEditor from '@/components/deck-card-editor.vue'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { DeckCardEditor }
|
|
||||||
})
|
|
||||||
export default class DeckCard extends Vue {
|
|
||||||
@Prop() public readonly card!: Card
|
|
||||||
@Prop() public readonly deck!: Deck
|
|
||||||
@Prop() public readonly isSelection!: boolean
|
|
||||||
|
|
||||||
private editHeadline = false;
|
|
||||||
private editFieldIndex: number | null = null;
|
|
||||||
|
|
||||||
private clickUnlessSelected () {
|
|
||||||
if (this.isSelection) return
|
|
||||||
this.$emit('click')
|
|
||||||
}
|
|
||||||
|
|
||||||
private editField (field: string, event: Event) {
|
|
||||||
if (event.target === null) return
|
|
||||||
const target = event.target as HTMLElement
|
|
||||||
const payload = { field, value: target.innerText }
|
|
||||||
this.$emit('edit', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get icon () {
|
|
||||||
const icon = this.card.icon || this.deck.icon
|
|
||||||
return iconPath(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get backIcon () {
|
|
||||||
const icon = this.card.backIcon || this.deck.icon
|
|
||||||
return iconPath(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get containerStyle () {
|
|
||||||
const style = {
|
|
||||||
'--highlight-color': this.card.color || this.deck.color,
|
|
||||||
...cardSizeToStyle(this.deck.cardSize),
|
|
||||||
transform: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const selected = this.isSelection
|
|
||||||
const hasElement = this.$el
|
|
||||||
|
|
||||||
if (selected && hasElement) {
|
|
||||||
const el = this.$el.getBoundingClientRect()
|
|
||||||
const wWidth = window.innerWidth
|
|
||||||
const wHeight = window.innerHeight
|
|
||||||
let scale = Math.min(2, wWidth / el.width)
|
|
||||||
|
|
||||||
const dH = wHeight / el.height
|
|
||||||
if (dH < scale) {
|
|
||||||
// leave some space if scaled card would otherwise fill top to bottom
|
|
||||||
// so that we can fit controls
|
|
||||||
scale = dH - dH * 0.1
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('scale', scale)
|
|
||||||
|
|
||||||
const dx = Math.round(wWidth / 2.0 - el.x - el.width / 2.0)
|
|
||||||
const dy = Math.round(wHeight / 2.0 - el.y - el.height / 2.0)
|
|
||||||
|
|
||||||
style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`
|
|
||||||
}
|
|
||||||
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style src="@/assets/card.css" />
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.flip-card {
|
|
||||||
position: relative;
|
|
||||||
perspective: 600px;
|
|
||||||
transition: transform .2s ease-out .4s;
|
|
||||||
}
|
|
||||||
.flip-card > .active-background {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: -100vh;
|
|
||||||
left: -100vw;
|
|
||||||
width: 200vw;
|
|
||||||
height: 200vh;
|
|
||||||
background-color: #0008;
|
|
||||||
}
|
|
||||||
.flip-card.active {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.flip-card.active > .active-background {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-front, .card-back {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--highlight-color);
|
|
||||||
transform: rotateX(0) rotateY(0);
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
transition: transform .4s ease-out;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.flip-card:not(.active):hover > .card-front {
|
|
||||||
transform: rotateX(0) rotateY(179deg);
|
|
||||||
}
|
|
||||||
.flip-card:not(.active):hover > .card-back {
|
|
||||||
z-index: 2;
|
|
||||||
transform: rotateX(0) rotateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-front {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.card-front > header > h1[contenteditable="true"] { text-decoration: underline dotted; }
|
|
||||||
.card-front > header > h1[contenteditable="true"]:focus { text-decoration: none; }
|
|
||||||
|
|
||||||
.card-back {
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 2;
|
|
||||||
transform: rotateX(0) rotateY(-179deg);
|
|
||||||
}
|
|
||||||
.card-back > button {
|
|
||||||
width: 80%;
|
|
||||||
margin: .1rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
margin-top: -3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (orientation:landscape) {
|
|
||||||
.action-close {
|
|
||||||
top: 3rem;
|
|
||||||
right: -3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,70 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:id="deck.id" class="card deck-cover" :style="style"
|
|
||||||
@click="$emit('click')"
|
|
||||||
>
|
|
||||||
<div class="icon-wrapper">
|
|
||||||
<img :src="icon" />
|
|
||||||
</div>
|
|
||||||
<footer>{{ deck.name }} ({{ deck.cards.length }})</footer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
|
||||||
import { Deck } from '@/types'
|
|
||||||
import { cardSizeToStyle, iconPath, defaultDeck } from '@/lib'
|
|
||||||
|
|
||||||
const emptyDeck: Deck = {
|
|
||||||
...defaultDeck(),
|
|
||||||
id: '_add_deck',
|
|
||||||
name: 'create new deck',
|
|
||||||
color: 'transparent',
|
|
||||||
icon: 'plus'
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class DeckCover extends Vue {
|
|
||||||
@Prop({ default () { return emptyDeck } }) public readonly deck!: Deck
|
|
||||||
|
|
||||||
private get icon () {
|
|
||||||
const icon = this.deck.icon || 'default'
|
|
||||||
return iconPath(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get style () {
|
|
||||||
return {
|
|
||||||
backgroundColor: this.deck.color,
|
|
||||||
...cardSizeToStyle(this.deck.cardSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.deck-cover {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 4rem;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
.deck-cover > footer {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
#_add_deck.deck-cover {
|
|
||||||
height: var(--card-height);
|
|
||||||
width: 25rem;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
#_add_deck.deck-cover > footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.deck-cover > .icon-wrapper {
|
|
||||||
width: 90%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,76 +0,0 @@
|
||||||
<template>
|
|
||||||
<form class="options-form" @submit.prevent="saveDeck">
|
|
||||||
<div class="deck-form-fields">
|
|
||||||
<select v-model="icon">
|
|
||||||
<option :key="iconName" :value="iconName" v-for="iconName in icons">{{ iconName }}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input v-model="name" title="deck name" placeholder="give it a name" />
|
|
||||||
<input v-model="description" title="deck description" placeholder="the most awesome deck of cards" />
|
|
||||||
|
|
||||||
<p>Pick a colour: <input type="color" v-model="color" /></p>
|
|
||||||
|
|
||||||
<select v-model="cardSize">
|
|
||||||
<option :key="size.value" :value="size.value" v-for="size in sizes">{{ size.title }}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit">Save deck</button>
|
|
||||||
<button class="cancel" @click.prevent="$emit('close')">cancel</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DeckCover :deck="newDeck" />
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Emit, Vue } from 'vue-property-decorator'
|
|
||||||
import { Deck, CardSize } from '@/types'
|
|
||||||
import { cardSizeOptions } from '@/consts'
|
|
||||||
import DeckCover from '@/components/deck-cover.vue'
|
|
||||||
import { iconPath } from '../lib'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { DeckCover }
|
|
||||||
})
|
|
||||||
export default class DeckForm extends Vue {
|
|
||||||
@Prop() public readonly deck!: Deck
|
|
||||||
|
|
||||||
private icons = ['mouth-watering', 'robe', 'thorny-triskelion']
|
|
||||||
private sizes = cardSizeOptions
|
|
||||||
|
|
||||||
private icon: string
|
|
||||||
private name: string
|
|
||||||
private description: string
|
|
||||||
private color: string
|
|
||||||
private cardSize: CardSize
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this.icon = this.deck.icon
|
|
||||||
this.name = this.deck.name
|
|
||||||
this.description = this.deck.description
|
|
||||||
this.color = this.deck.color
|
|
||||||
this.cardSize = this.deck.cardSize
|
|
||||||
}
|
|
||||||
|
|
||||||
private get iconPath () {
|
|
||||||
return iconPath(this.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get newDeck (): Deck {
|
|
||||||
return {
|
|
||||||
...this.deck,
|
|
||||||
name: this.name,
|
|
||||||
description: this.description,
|
|
||||||
color: this.color,
|
|
||||||
icon: this.icon,
|
|
||||||
cardSize: this.cardSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit('save')
|
|
||||||
private saveDeck (): Deck {
|
|
||||||
return this.newDeck
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,24 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="edit-deck-form" class="deck">
|
|
||||||
<header>Deck Config</header>
|
|
||||||
<DeckForm :deck="deck" @save="saveDeck" @close="$emit('close')" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Emit, Vue } from 'vue-property-decorator'
|
|
||||||
import { Deck } from '@/types'
|
|
||||||
import DeckForm from './deck-form.vue'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { DeckForm }
|
|
||||||
})
|
|
||||||
export default class EditDeckForm extends Vue {
|
|
||||||
@Prop() public readonly deck!: Deck
|
|
||||||
|
|
||||||
@Emit('save')
|
|
||||||
private saveDeck (deck: Deck) {
|
|
||||||
this.$storage.saveDeck(deck)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="switch">
|
|
||||||
<input :id="id" class="checkbox" type="checkbox" :checked="value" @change="$emit('input', !value)" />
|
|
||||||
<label :for="id">
|
|
||||||
<div class="switch-label-text">{{ label }}</div>
|
|
||||||
<div class="switch-elements-wrapper">
|
|
||||||
<div class="switch-elements">
|
|
||||||
<div class="switch-element off"><slot name="off">NO</slot></div>
|
|
||||||
<div class="switch-element btn"></div>
|
|
||||||
<div class="switch-element on"><slot name="on">YES</slot></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class FlipSwitch extends Vue {
|
|
||||||
@Prop() public readonly id!: string
|
|
||||||
@Prop() public readonly value!: boolean
|
|
||||||
@Prop() public readonly label!: string
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.switch > input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.switch .switch-elements-wrapper {
|
|
||||||
height: 2em;
|
|
||||||
width: 4em;
|
|
||||||
border: 4px solid black;
|
|
||||||
border-radius: 2em;
|
|
||||||
background-color: black;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.switch .switch-elements {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
transition: transform .2s ease-in;
|
|
||||||
}
|
|
||||||
.switch .switch-element {
|
|
||||||
height: 1.8em;
|
|
||||||
width: 1.8em;
|
|
||||||
margin: .1em;
|
|
||||||
line-height: 1.8em;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
.switch .btn {
|
|
||||||
background-color: gray;
|
|
||||||
border-radius: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.checkbox:checked + label .switch-elements-wrapper > .switch-elements {
|
|
||||||
transform: translate(-2em, 0);
|
|
||||||
}
|
|
||||||
input.checkbox:checked + label .switch-elements-wrapper {
|
|
||||||
box-shadow: 0 0 15px 2px green;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,58 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="new-deck-form" class="deck">
|
|
||||||
<header>Create a new deck of cards</header>
|
|
||||||
<DeckForm :deck="newDeck" @save="saveDeck" @close="$emit('close')" />
|
|
||||||
<footer class="centered">You can also <button @click="importDeck">import</button> an existing set.</footer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Emit, Vue } from 'vue-property-decorator'
|
|
||||||
import { Deck } from '@/types'
|
|
||||||
import DeckForm from './deck-form.vue'
|
|
||||||
import { defaultDeck, randomId, isValidDeck } from '../lib'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { DeckForm }
|
|
||||||
})
|
|
||||||
export default class NewDeckForm extends Vue {
|
|
||||||
private newDeck: Deck = defaultDeck()
|
|
||||||
|
|
||||||
private importDeck () {
|
|
||||||
const newFileSelector = document.createElement('input')
|
|
||||||
newFileSelector.setAttribute('type', 'file')
|
|
||||||
|
|
||||||
newFileSelector.onchange = event => {
|
|
||||||
if (event === null) return
|
|
||||||
const fileList = (event.target as HTMLInputElement).files
|
|
||||||
if (fileList === null || fileList.length < 1) return
|
|
||||||
const file = fileList[0]
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
const seemsToBeJSON = file.type === 'application/json'
|
|
||||||
// TODO: more checks?
|
|
||||||
let fileOk = seemsToBeJSON
|
|
||||||
|
|
||||||
if (!seemsToBeJSON) {
|
|
||||||
fileOk = window.confirm(`This seems to be wrong file type (${file.type}). Should be JSON. Import anyway?`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileOk) return
|
|
||||||
|
|
||||||
file.text().then((text: string) => {
|
|
||||||
const json = JSON.parse(text)
|
|
||||||
if (!isValidDeck(json)) window.alert('Sorry, that did\'t seem to be a valid deck.')
|
|
||||||
else this.$emit('save', this.$storage.saveDeck(json))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
newFileSelector.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit('save')
|
|
||||||
private saveDeck (deck: Deck) {
|
|
||||||
deck.id = randomId() // just to make sure
|
|
||||||
this.$storage.saveDeck(deck)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,96 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="print-options-form">
|
|
||||||
<header>Print Deck</header>
|
|
||||||
|
|
||||||
<form @submit.prevent="printDeck">
|
|
||||||
<div class="deck-form-fields">
|
|
||||||
<label for="print-option-page-size">
|
|
||||||
Page Size
|
|
||||||
<select class="print-option-select" id="print-option-page-size" v-model="pageSize">
|
|
||||||
<option :key="size.value" :value="size.value" v-for="size in pageSizes">{{ size.title }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label for="print-option-card-size">
|
|
||||||
Card Size
|
|
||||||
<select class="print-option-select" id="print-option-card-size" v-model="cardSize">
|
|
||||||
<option :key="size.value" :value="size.value" v-for="size in cardSizes">{{ size.title }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label for="print-option-arrangement">
|
|
||||||
Arrangement
|
|
||||||
<select class="print-option-select" id="print-option-arrangement" v-model="arrangement">
|
|
||||||
<option :key="arrangement.value" :value="arrangement.value" v-for="arrangement in arrangements">{{ arrangement.title }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<FlipSwitch id="print-option-rounded-corners" label="Rounded Corners" v-model="roundedCorners">
|
|
||||||
</FlipSwitch>
|
|
||||||
|
|
||||||
<button type="submit">Print deck</button>
|
|
||||||
<button class="cancel" @click.prevent="$emit('close')">cancel</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
|
||||||
import { Deck } from '@/types'
|
|
||||||
import {
|
|
||||||
cardSizeOptions,
|
|
||||||
pageSizeOptions,
|
|
||||||
arrangementOptions,
|
|
||||||
defaultCardSize,
|
|
||||||
defaultPageSize,
|
|
||||||
defaultArrangement
|
|
||||||
} from '@/consts'
|
|
||||||
import FlipSwitch from '@/components/flip-switch.vue'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { FlipSwitch }
|
|
||||||
})
|
|
||||||
export default class EditDeckForm extends Vue {
|
|
||||||
@Prop() public readonly deck!: Deck
|
|
||||||
|
|
||||||
private pageSizes = pageSizeOptions
|
|
||||||
private cardSizes = cardSizeOptions
|
|
||||||
private arrangements = arrangementOptions
|
|
||||||
|
|
||||||
private pageSize = defaultPageSize
|
|
||||||
private cardSize = defaultCardSize
|
|
||||||
private arrangement = defaultArrangement
|
|
||||||
private roundedCorners = true
|
|
||||||
|
|
||||||
private mounted () {
|
|
||||||
this.cardSize = this.deck.cardSize
|
|
||||||
this.pageSize = this.deck.pageSize
|
|
||||||
this.arrangement = this.deck.arrangement
|
|
||||||
this.roundedCorners = this.deck.roundedCorners
|
|
||||||
}
|
|
||||||
|
|
||||||
private printDeck () {
|
|
||||||
this.$storage.saveDeck({
|
|
||||||
...this.deck,
|
|
||||||
arrangement: this.arrangement,
|
|
||||||
pageSize: this.pageSize,
|
|
||||||
cardSize: this.cardSize,
|
|
||||||
roundedCorners: this.roundedCorners
|
|
||||||
})
|
|
||||||
console.log('would print on', this.pageSize, `(${this.arrangement})`, this.deck.cards.length, 'cards of size', this.cardSize, this.roundedCorners ? 'with rounded corners' : '')
|
|
||||||
window.open(`/print/${this.deck.id}`, '_blank')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.print-option-select {
|
|
||||||
width: 55%;
|
|
||||||
}
|
|
||||||
.deck-form-fields {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 20em;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,89 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="card" :style="containerStyle">
|
|
||||||
<div class="card-front" v-if="showFront">
|
|
||||||
<header>
|
|
||||||
<h1>{{ card.name }}</h1>
|
|
||||||
<img :src="icon" />
|
|
||||||
</header>
|
|
||||||
<main ref="cardEl" class="card-content">
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<div class="card-back" v-if="showBack">BACK</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
|
||||||
import { Deck, Card } from '@/types'
|
|
||||||
import { iconPath } from '@/lib'
|
|
||||||
|
|
||||||
import Editor from '@editorjs/editorjs'
|
|
||||||
import List from '@editorjs/list'
|
|
||||||
import { Heading, Delimiter, Charges, DnDStats } from '@/editor'
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class StaticCard extends Vue {
|
|
||||||
@Prop() public readonly card!: Card
|
|
||||||
@Prop() public readonly deck!: Deck
|
|
||||||
@Prop({ default: false }) public readonly showFront!: boolean
|
|
||||||
@Prop({ default: false }) public readonly showBack!: boolean
|
|
||||||
|
|
||||||
private editor!: Editor
|
|
||||||
|
|
||||||
private mounted () {
|
|
||||||
this.editor = new Editor({
|
|
||||||
holder: this.$refs.cardEl as HTMLElement,
|
|
||||||
autofocus: false,
|
|
||||||
hideToolbar: true,
|
|
||||||
tools: {
|
|
||||||
list: { class: List, inlineToolbar: false },
|
|
||||||
heading: { class: Heading, inlineToolbar: false },
|
|
||||||
delimiter: { class: Delimiter, inlineToolbar: false },
|
|
||||||
charges: { class: Charges, inlineToolbar: false },
|
|
||||||
dndstats: { class: DnDStats, inlineToolbar: false }
|
|
||||||
},
|
|
||||||
data: this.card.content,
|
|
||||||
onReady: () => {
|
|
||||||
console.log('editor is ready, what to do?')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private get icon () {
|
|
||||||
const icon = this.card.icon || this.deck.icon
|
|
||||||
return iconPath(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get backIcon () {
|
|
||||||
const icon = this.card.backIcon || this.deck.icon
|
|
||||||
return iconPath(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get containerStyle () {
|
|
||||||
const color = (this.deck && this.deck.color) || this.card.color
|
|
||||||
|
|
||||||
return {
|
|
||||||
'--highlight-color': color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style src="@/assets/card.css" />
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.card {
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
background-color: var(--highlight-color);
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
margin: 0;
|
|
||||||
cursor: default;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.card-front, .card-back {
|
|
||||||
width: var(--card-width);
|
|
||||||
height: var(--card-height);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { CardSize, PageSize, Arrangement } from './types'
|
|
||||||
|
|
||||||
export const cardSizeOptions = [
|
|
||||||
{ title: '88x62 (Poker)', value: CardSize.Poker },
|
|
||||||
{ title: '88x56 (Bridge)', value: CardSize.Bridge }
|
|
||||||
]
|
|
||||||
|
|
||||||
export const pageSizeOptions = [
|
|
||||||
{ title: 'A4', value: PageSize.A4 }, // 210mm × 297mm
|
|
||||||
{ title: 'US Letter', value: PageSize.USLetter }, // 8.5in × 11in
|
|
||||||
{ title: 'JIS-B4', value: PageSize.JISB4 }, // 182mm × 257mm
|
|
||||||
{ title: 'A3', value: PageSize.A3 }, // 297mm × 420mm
|
|
||||||
{ title: 'A5', value: PageSize.A5 }, // 148mm × 210mm
|
|
||||||
{ title: 'US Legal', value: PageSize.USLegal }, // 8.5in × 14in
|
|
||||||
{ title: 'US Ledger', value: PageSize.USLedger }, // 11in × 17in
|
|
||||||
{ title: 'JIS-B5', value: PageSize.JISB5 } // 257mm × 364mm
|
|
||||||
]
|
|
||||||
|
|
||||||
export const arrangementOptions = [
|
|
||||||
{ title: 'Double Sided', value: Arrangement.DoubleSided },
|
|
||||||
{ title: 'Only Front Sides', value: Arrangement.FrontOnly },
|
|
||||||
{ title: 'Side by Side', value: Arrangement.SideBySide }
|
|
||||||
]
|
|
||||||
|
|
||||||
export const defaultPageSize = pageSizeOptions[0].value
|
|
||||||
export const defaultCardSize = cardSizeOptions[0].value
|
|
||||||
export const defaultArrangement = arrangementOptions[0].value
|
|
|
@ -1,39 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import { randomId } from './lib'
|
|
||||||
|
|
||||||
const eventHandlers: { [key: string]: () => void } = {}
|
|
||||||
|
|
||||||
Vue.directive('editable', (el, { value, arg }, vnode) => {
|
|
||||||
const keypressHandler = (event: KeyboardEvent) => {
|
|
||||||
// allow line break via Shift + Enter
|
|
||||||
if (event.keyCode === 13 && !event.shiftKey) {
|
|
||||||
event.preventDefault()
|
|
||||||
console.log('edit event on enter', el.innerText)
|
|
||||||
if (!vnode.context) return
|
|
||||||
vnode.context.$emit('edit', { param: arg, value: el.innerText })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blurHandler = () => {
|
|
||||||
console.log('edit event on blur', el.innerText)
|
|
||||||
if (!vnode.context) return
|
|
||||||
vnode.context.$emit('edit', { param: arg, value: el.innerText })
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove old event listeners
|
|
||||||
if (el.dataset.__evtid) {
|
|
||||||
eventHandlers[el.dataset.__evtid]()
|
|
||||||
}
|
|
||||||
|
|
||||||
el.contentEditable = value ? 'true' : 'false'
|
|
||||||
el.addEventListener('keypress', keypressHandler)
|
|
||||||
el.addEventListener('blur', blurHandler)
|
|
||||||
|
|
||||||
// TODO: is there a better way to avoid multiple event handlers?
|
|
||||||
const id = randomId()
|
|
||||||
el.dataset.__evtid = id
|
|
||||||
eventHandlers[id] = () => {
|
|
||||||
el.removeEventListener('keypress', keypressHandler)
|
|
||||||
el.removeEventListener('blur', blurHandler)
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,134 +0,0 @@
|
||||||
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
|
|
||||||
import icon from '../assets/editor/charges.svg.txt'
|
|
||||||
import iconCircle from '../assets/editor/charges-circle.svg.txt'
|
|
||||||
|
|
||||||
const title = 'Charges'
|
|
||||||
|
|
||||||
interface ChargesData {
|
|
||||||
variant: string;
|
|
||||||
amount: number;
|
|
||||||
size: number;
|
|
||||||
stretch: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Charges extends ContentlessBlock {
|
|
||||||
static MIN_SIZE = 1
|
|
||||||
static MAX_SIZE = 5
|
|
||||||
private _variant: string
|
|
||||||
private _amount: number
|
|
||||||
private _size: number
|
|
||||||
private _stretch: boolean
|
|
||||||
|
|
||||||
constructor (args: BlockToolArgs) {
|
|
||||||
super(args)
|
|
||||||
this._settingButtons = [
|
|
||||||
{ name: 'box', icon, action: (name: string) => this.setVariant(name) },
|
|
||||||
{ name: 'more', icon: icon, action: () => this.increaseAmount() },
|
|
||||||
{ name: 'bigger', icon: icon, action: () => this.increaseSize() },
|
|
||||||
{ name: 'circle', icon: iconCircle, action: (name: string) => this.setVariant(name) },
|
|
||||||
{ name: 'less', icon: icon, action: () => this.decreaseAmount() },
|
|
||||||
{ name: 'smaller', icon: icon, action: () => this.decreaseSize() },
|
|
||||||
{ name: 'toggle-stretch', icon: icon, action: () => this.toggleStretch() }
|
|
||||||
]
|
|
||||||
const { variant, amount, size, stretch } = (args.data || {}) as ChargesData
|
|
||||||
|
|
||||||
this._variant = variant || 'box'
|
|
||||||
this._amount = amount || 5
|
|
||||||
this._size = size || 1
|
|
||||||
this._stretch = !(stretch === false)
|
|
||||||
|
|
||||||
this._element = this._render()
|
|
||||||
}
|
|
||||||
|
|
||||||
private setVariant (variant: string) {
|
|
||||||
if (this._variant === variant) return
|
|
||||||
|
|
||||||
const charges = Array.from(this._element.children)
|
|
||||||
|
|
||||||
charges.forEach(charge => {
|
|
||||||
charge.classList.remove(`card-charge-${this._variant}`)
|
|
||||||
charge.classList.add(`card-charge-${variant}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
this._variant = variant
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleStretch () {
|
|
||||||
if (this._stretch) this._element.classList.remove('card-charges-stretch')
|
|
||||||
else this._element.classList.add('card-charges-stretch')
|
|
||||||
this._stretch = !this._stretch
|
|
||||||
}
|
|
||||||
|
|
||||||
private createCharge (): HTMLElement {
|
|
||||||
const charge = document.createElement('DIV')
|
|
||||||
charge.classList.add('card-charge', `card-charge-${this._variant}`, `card-charge-size-${this._size}`)
|
|
||||||
return charge
|
|
||||||
}
|
|
||||||
|
|
||||||
private increaseAmount () {
|
|
||||||
this._element.appendChild(this.createCharge())
|
|
||||||
this._amount++
|
|
||||||
}
|
|
||||||
|
|
||||||
private decreaseAmount () {
|
|
||||||
const child = this._element.lastElementChild
|
|
||||||
if (child) {
|
|
||||||
this._element.removeChild(child)
|
|
||||||
this._amount--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private increaseSize () {
|
|
||||||
if (this._size >= Charges.MAX_SIZE) return
|
|
||||||
|
|
||||||
const charges = Array.from(this._element.children)
|
|
||||||
|
|
||||||
charges.forEach(charge => {
|
|
||||||
charge.classList.remove(`card-charge-size-${this._size}`)
|
|
||||||
charge.classList.add(`card-charge-size-${this._size + 1}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
this._size++
|
|
||||||
}
|
|
||||||
|
|
||||||
private decreaseSize () {
|
|
||||||
if (this._size <= Charges.MIN_SIZE) return
|
|
||||||
|
|
||||||
const charges = Array.from(this._element.children)
|
|
||||||
|
|
||||||
charges.forEach(charge => {
|
|
||||||
charge.classList.remove(`card-charge-size-${this._size}`)
|
|
||||||
charge.classList.add(`card-charge-size-${this._size - 1}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
this._size--
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _render (): HTMLElement {
|
|
||||||
const el = document.createElement('DIV')
|
|
||||||
el.classList.add('card-charges-wrapper', this._CSS.block)
|
|
||||||
|
|
||||||
if (this._stretch) el.classList.add('card-charges-stretch')
|
|
||||||
|
|
||||||
for (let i = 0; i < this._amount; i++) {
|
|
||||||
el.appendChild(this.createCharge())
|
|
||||||
}
|
|
||||||
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
public save (): ChargesData {
|
|
||||||
return {
|
|
||||||
variant: this._variant,
|
|
||||||
amount: this._amount,
|
|
||||||
size: this._size,
|
|
||||||
stretch: this._stretch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get toolbox () {
|
|
||||||
return { icon, title }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Charges
|
|
|
@ -1,222 +0,0 @@
|
||||||
import {
|
|
||||||
BlockTool,
|
|
||||||
BlockToolData,
|
|
||||||
ToolboxConfig,
|
|
||||||
API,
|
|
||||||
HTMLPasteEvent,
|
|
||||||
ToolSettings,
|
|
||||||
SanitizerConfig
|
|
||||||
} from '@editorjs/editorjs'
|
|
||||||
|
|
||||||
export { HTMLPasteEvent } from '@editorjs/editorjs'
|
|
||||||
|
|
||||||
interface PasteConfig {
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContentBlockConfig extends ToolSettings {
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContentBlockSettingButton {
|
|
||||||
name: string;
|
|
||||||
icon: string;
|
|
||||||
action: (name: string, event?: MouseEvent) => void; // action triggered by button
|
|
||||||
isActive?: (name: string) => boolean; // determine if current button is active
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ContentBlockSettings = ContentBlockSettingButton[]
|
|
||||||
|
|
||||||
export interface ContentBlockArgs {
|
|
||||||
api: API;
|
|
||||||
config?: ContentBlockConfig;
|
|
||||||
data?: BlockToolData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CSSClasses {
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContentBlockData extends BlockToolData {
|
|
||||||
text?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type importFunction = (str: string) => ContentBlockData
|
|
||||||
type exportFunction = (data: ContentBlockData) => string
|
|
||||||
|
|
||||||
export interface ConversionConfig {
|
|
||||||
import: string | importFunction;
|
|
||||||
export: string | exportFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ContentBlock implements BlockTool {
|
|
||||||
// Default placeholder for Paragraph Tool
|
|
||||||
static get DEFAULT_PLACEHOLDER (): string {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
static _supportedTags: string[] = []
|
|
||||||
|
|
||||||
static _toolboxConfig: ToolboxConfig = {
|
|
||||||
icon: '<svg></svg>',
|
|
||||||
title: 'UnnamedContentPlugin'
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _defaultPlaceholder (): string {
|
|
||||||
return ContentBlock.DEFAULT_PLACEHOLDER
|
|
||||||
}
|
|
||||||
|
|
||||||
protected api: API
|
|
||||||
protected _element: HTMLElement
|
|
||||||
protected _data: ContentBlockData
|
|
||||||
protected _config: ContentBlockConfig
|
|
||||||
protected _placeholder: string
|
|
||||||
protected _CSS: CSSClasses = {}
|
|
||||||
protected onKeyUp: (event: KeyboardEvent) => void
|
|
||||||
protected _settingButtons: ContentBlockSettings = []
|
|
||||||
|
|
||||||
constructor ({ data, config, api }: ContentBlockArgs) {
|
|
||||||
this.api = api
|
|
||||||
this._config = config as ContentBlockConfig
|
|
||||||
this._CSS.block = this.api.styles.block
|
|
||||||
|
|
||||||
this.onKeyUp = (event: KeyboardEvent) => this._onKeyUp(event)
|
|
||||||
|
|
||||||
// Placeholder it is first Block
|
|
||||||
this._placeholder = config?.placeholder ? config.placeholder : this._defaultPlaceholder()
|
|
||||||
this._data = data as ContentBlockData
|
|
||||||
this._element = this._render()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if text content is empty and set empty string to inner html.
|
|
||||||
// We need this because some browsers (e.g. Safari) insert <br> into empty contenteditanle elements
|
|
||||||
_onKeyUp (event: KeyboardEvent) {
|
|
||||||
if (event.code !== 'Backspace' && event.code !== 'Delete') return
|
|
||||||
|
|
||||||
if (this._element.textContent === '') {
|
|
||||||
this._element.innerHTML = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render tool view
|
|
||||||
// whenever a redraw is needed the result is saved in this._element
|
|
||||||
protected _render (): HTMLElement {
|
|
||||||
const el = document.createElement('DIV')
|
|
||||||
el.classList.add(this._CSS.block)
|
|
||||||
el.dataset.placeholder = this._placeholder
|
|
||||||
el.addEventListener('keyup', this.onKeyUp)
|
|
||||||
el.innerHTML = this.data.text || ''
|
|
||||||
el.contentEditable = 'true'
|
|
||||||
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return Tool's view
|
|
||||||
public render (): HTMLElement {
|
|
||||||
return this._element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method that specified how to merge two Text blocks.
|
|
||||||
// Called by Editor.js by backspace at the beginning of the Block
|
|
||||||
public merge (data: ContentBlockData) {
|
|
||||||
this.data = {
|
|
||||||
text: (this.data.text || '') + data.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate Paragraph block data (by default checks for emptiness)
|
|
||||||
public validate (savedData: ContentBlockData): boolean {
|
|
||||||
if (!savedData.text) return false
|
|
||||||
return savedData.text.trim() !== ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract Tool's data from the view
|
|
||||||
public save (toolsContent: HTMLElement): ContentBlockData {
|
|
||||||
return {
|
|
||||||
text: toolsContent.innerHTML
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get CSS (): CSSClasses {
|
|
||||||
return this._CSS
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable Conversion Toolbar. Paragraph can be converted to/from other tools
|
|
||||||
*/
|
|
||||||
static get conversionConfig (): ConversionConfig {
|
|
||||||
return {
|
|
||||||
export: 'text', // to convert Paragraph to other block, use 'text' property of saved data
|
|
||||||
import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitizer rules
|
|
||||||
static get sanitize (): SanitizerConfig {
|
|
||||||
return {
|
|
||||||
text: { br: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get data (): ContentBlockData {
|
|
||||||
const text = this._element?.innerHTML
|
|
||||||
if (text !== undefined) this._data.text = text
|
|
||||||
if (this._data.text === undefined) this._data.text = ''
|
|
||||||
return this._data
|
|
||||||
}
|
|
||||||
|
|
||||||
set data (data: ContentBlockData) {
|
|
||||||
this._data = data || {}
|
|
||||||
this._element.innerHTML = this._data.text || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderSettings (): HTMLElement {
|
|
||||||
const wrapper = document.createElement('DIV')
|
|
||||||
|
|
||||||
this._settingButtons.forEach(tune => {
|
|
||||||
// make sure the settings button does something
|
|
||||||
if (!tune.icon || typeof tune.action !== 'function') return
|
|
||||||
|
|
||||||
const { name, icon, action, isActive } = tune
|
|
||||||
|
|
||||||
const btn = document.createElement('SPAN')
|
|
||||||
btn.classList.add(this.api.styles.settingsButton)
|
|
||||||
|
|
||||||
if (typeof isActive === 'function' && isActive(name)) {
|
|
||||||
btn.classList.add(this.api.styles.settingsButtonActive)
|
|
||||||
}
|
|
||||||
btn.innerHTML = icon
|
|
||||||
btn.addEventListener('click', event => action(name, event))
|
|
||||||
|
|
||||||
wrapper.appendChild(btn)
|
|
||||||
})
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used by Editor.js paste handling API.
|
|
||||||
// Provides configuration to handle the tools tags.
|
|
||||||
static get pasteConfig (): PasteConfig {
|
|
||||||
return {
|
|
||||||
tags: this._supportedTags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// overwrite this if you need special handling of paste data
|
|
||||||
protected pasteHandler (element: HTMLElement): ContentBlockData {
|
|
||||||
return { text: element.innerText }
|
|
||||||
}
|
|
||||||
|
|
||||||
// On paste callback fired from Editor.
|
|
||||||
public onPaste (event: HTMLPasteEvent) {
|
|
||||||
const element = event.detail.data
|
|
||||||
this.data = this.pasteHandler(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon and title for displaying at the Toolbox
|
|
||||||
static get toolbox (): ToolboxConfig {
|
|
||||||
return this._toolboxConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContentBlock
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { BlockTool, BlockToolData, ToolSettings, ToolboxConfig, API } from '@editorjs/editorjs'
|
|
||||||
import { ContentBlockSettings, CSSClasses } from './content-block'
|
|
||||||
|
|
||||||
export interface BlockToolArgs {
|
|
||||||
api: API;
|
|
||||||
config?: ToolSettings;
|
|
||||||
data?: BlockToolData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ContentlessBlock implements BlockTool {
|
|
||||||
static get contentless () {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
protected api: API
|
|
||||||
protected _element: HTMLElement
|
|
||||||
protected _data: object
|
|
||||||
protected _config: ToolSettings
|
|
||||||
protected _CSS: CSSClasses = {}
|
|
||||||
protected _settingButtons: ContentBlockSettings = []
|
|
||||||
|
|
||||||
constructor ({ data, config, api }: BlockToolArgs) {
|
|
||||||
this.api = api
|
|
||||||
this._config = config as ToolSettings
|
|
||||||
this._data = data || {}
|
|
||||||
this._CSS.block = this.api.styles.block
|
|
||||||
this._element = this._render()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _render (): HTMLElement {
|
|
||||||
const el = document.createElement('DIV')
|
|
||||||
el.classList.add(this._CSS.block)
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
public render (): HTMLElement {
|
|
||||||
return this._element
|
|
||||||
}
|
|
||||||
|
|
||||||
public save (_toolsContent: HTMLElement): object {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderSettings (): HTMLElement {
|
|
||||||
const wrapper = document.createElement('DIV')
|
|
||||||
|
|
||||||
this._settingButtons.forEach(tune => {
|
|
||||||
// make sure the settings button does something
|
|
||||||
if (!tune.icon || typeof tune.action !== 'function') return
|
|
||||||
|
|
||||||
const { name, icon, action, isActive } = tune
|
|
||||||
|
|
||||||
const btn = document.createElement('SPAN')
|
|
||||||
btn.classList.add(this.api.styles.settingsButton)
|
|
||||||
|
|
||||||
if (typeof isActive === 'function' && isActive(name)) {
|
|
||||||
btn.classList.add(this.api.styles.settingsButtonActive)
|
|
||||||
}
|
|
||||||
btn.innerHTML = icon
|
|
||||||
btn.addEventListener('click', event => action(name, event))
|
|
||||||
|
|
||||||
wrapper.appendChild(btn)
|
|
||||||
})
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
static get toolbox (): ToolboxConfig {
|
|
||||||
return { icon: '<svg></svg>', title: 'UnnamedPlugin' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContentlessBlock
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
|
|
||||||
import icon from '../assets/editor/delimiter.svg.txt'
|
|
||||||
import iconR from '../assets/editor/delimiter_r.svg.txt'
|
|
||||||
import iconL from '../assets/editor/delimiter_l.svg.txt'
|
|
||||||
const title = 'Delimiter'
|
|
||||||
|
|
||||||
interface DelimiterData {
|
|
||||||
variant: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Delimiter extends ContentlessBlock {
|
|
||||||
private _variant = 'none'
|
|
||||||
|
|
||||||
constructor (args: BlockToolArgs) {
|
|
||||||
super(args)
|
|
||||||
this._settingButtons = [
|
|
||||||
{ name: 'straight', icon, action: (name: string) => this.setDelimiterType(name) },
|
|
||||||
{ name: 'pointing-left', icon: iconL, action: (name: string) => this.setDelimiterType(name) },
|
|
||||||
{ name: 'pointing-right', icon: iconR, action: (name: string) => this.setDelimiterType(name) }
|
|
||||||
]
|
|
||||||
const { variant } = (args.data || {}) as DelimiterData
|
|
||||||
if (variant) this.setDelimiterType(variant)
|
|
||||||
}
|
|
||||||
|
|
||||||
private setDelimiterType (name: string) {
|
|
||||||
this._element.classList.remove('pointing-left')
|
|
||||||
this._element.classList.remove('pointing-right')
|
|
||||||
this._variant = 'none'
|
|
||||||
|
|
||||||
if (name === 'pointing-left' || name === 'pointing-right') {
|
|
||||||
this._variant = name
|
|
||||||
this._element.classList.add(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _render (): HTMLElement {
|
|
||||||
const el = document.createElement('HR')
|
|
||||||
el.classList.add('card-delimiter', this._CSS.block)
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
public save (): DelimiterData {
|
|
||||||
return {
|
|
||||||
variant: this._variant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get toolbox () {
|
|
||||||
return { icon, title }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Delimiter
|
|
|
@ -1,106 +0,0 @@
|
||||||
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
|
|
||||||
import icon from '../assets/editor/charges-circle.svg.txt'
|
|
||||||
|
|
||||||
const title = 'DnDStats'
|
|
||||||
|
|
||||||
interface DnDStatsData {
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DnDStats extends ContentlessBlock {
|
|
||||||
static _toolboxConfig = { icon, title }
|
|
||||||
private _stats = [10, 10, 10, 10, 10, 10]
|
|
||||||
|
|
||||||
constructor (args: BlockToolArgs) {
|
|
||||||
super(args)
|
|
||||||
this.data = args.data as DnDStatsData
|
|
||||||
this._element = this._render()
|
|
||||||
}
|
|
||||||
|
|
||||||
public get data () {
|
|
||||||
return {
|
|
||||||
text: this._stats.join(',')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public set data (data: DnDStatsData) {
|
|
||||||
if (data.text === undefined) data.text = ''
|
|
||||||
|
|
||||||
const newStats = data.text.split(',')
|
|
||||||
.map(x => parseInt(x, 10))
|
|
||||||
.filter(x => !Number.isNaN(x))
|
|
||||||
|
|
||||||
while (newStats.length < 6) newStats.push(10) // fill missing stats
|
|
||||||
|
|
||||||
this._stats = newStats
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates a random four character long id
|
|
||||||
private randomId (): string {
|
|
||||||
const min = 46656 // '1000'
|
|
||||||
const max = 1679615 /* 'zzzz' */ - 46656 /* '1000' */
|
|
||||||
return (min + Math.floor(max * Math.random())).toString(36)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderStatMod (value: number): string {
|
|
||||||
const mod = Math.floor((value - 10) / 2.0)
|
|
||||||
const sign = mod < 0 ? '' : '+'
|
|
||||||
return ` (${sign}${mod})`
|
|
||||||
}
|
|
||||||
|
|
||||||
private createStatBlock (title: string, value: number, changeHandler: (newValue: number) => void): HTMLElement {
|
|
||||||
const id = `dnd-stat-${title}-${this.randomId()}`
|
|
||||||
|
|
||||||
const labelWrapper = document.createElement('label')
|
|
||||||
const titleEl = document.createElement('span')
|
|
||||||
const statInputEl = document.createElement('input')
|
|
||||||
const statModEl = document.createElement('span')
|
|
||||||
|
|
||||||
// should allow focussing block with tab
|
|
||||||
labelWrapper.setAttribute('z-index', '1')
|
|
||||||
labelWrapper.classList.add('dnd-stat-block')
|
|
||||||
labelWrapper.setAttribute('for', id)
|
|
||||||
|
|
||||||
titleEl.classList.add('dnd-stat-title')
|
|
||||||
titleEl.innerText = title
|
|
||||||
|
|
||||||
statInputEl.id = id
|
|
||||||
statInputEl.value = `${value}`
|
|
||||||
statInputEl.addEventListener('input', () => {
|
|
||||||
const value = parseInt(statInputEl.value, 10)
|
|
||||||
statModEl.innerText = this.renderStatMod(value)
|
|
||||||
changeHandler(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
statModEl.innerText = this.renderStatMod(value)
|
|
||||||
|
|
||||||
labelWrapper.appendChild(titleEl)
|
|
||||||
labelWrapper.appendChild(statInputEl)
|
|
||||||
labelWrapper.appendChild(statModEl)
|
|
||||||
|
|
||||||
return labelWrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _render (): HTMLElement {
|
|
||||||
const el = document.createElement('div')
|
|
||||||
el.classList.add('card-dnd-stats')
|
|
||||||
const stats = this._stats || [10, 10, 10, 10, 10, 10]
|
|
||||||
const titles = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
|
||||||
|
|
||||||
stats.forEach((stat, i) => {
|
|
||||||
const title = titles[i]
|
|
||||||
const block = this.createStatBlock(title, stat, newValue => {
|
|
||||||
this._stats[i] = newValue
|
|
||||||
})
|
|
||||||
el.appendChild(block)
|
|
||||||
})
|
|
||||||
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
public save (): DnDStatsData {
|
|
||||||
return this.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DnDStats
|
|
|
@ -1,159 +0,0 @@
|
||||||
import {
|
|
||||||
ContentBlock,
|
|
||||||
ContentBlockArgs,
|
|
||||||
ContentBlockConfig,
|
|
||||||
ContentBlockData
|
|
||||||
} from './content-block'
|
|
||||||
|
|
||||||
import icon from '../assets/editor/header.svg.txt'
|
|
||||||
import icon1 from '../assets/editor/header1.svg.txt'
|
|
||||||
import icon2 from '../assets/editor/header2.svg.txt'
|
|
||||||
import icon3 from '../assets/editor/header3.svg.txt'
|
|
||||||
import icon4 from '../assets/editor/header4.svg.txt'
|
|
||||||
import icon5 from '../assets/editor/header5.svg.txt'
|
|
||||||
import icon6 from '../assets/editor/header6.svg.txt'
|
|
||||||
|
|
||||||
const title = 'Heading'
|
|
||||||
|
|
||||||
enum HeadingLevel {
|
|
||||||
One = 1,
|
|
||||||
Two = 2,
|
|
||||||
Three = 3,
|
|
||||||
Four = 4,
|
|
||||||
Five = 5,
|
|
||||||
Six = 6
|
|
||||||
}
|
|
||||||
|
|
||||||
const icons = [null, icon1, icon2, icon3, icon4, icon5, icon6]
|
|
||||||
|
|
||||||
interface HeadingConfig extends ContentBlockConfig {
|
|
||||||
placeholder?: string;
|
|
||||||
levels?: HeadingLevel[];
|
|
||||||
defaultLevel?: HeadingLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeadingData extends ContentBlockData {
|
|
||||||
text: string;
|
|
||||||
level?: HeadingLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Heading extends ContentBlock {
|
|
||||||
static _supportedTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
|
|
||||||
static _toolboxConfig = { icon, title }
|
|
||||||
|
|
||||||
protected _config: HeadingConfig
|
|
||||||
private defaultLevel: HeadingLevel
|
|
||||||
private currentLevel: HeadingLevel
|
|
||||||
|
|
||||||
constructor (args: ContentBlockArgs) {
|
|
||||||
super(args)
|
|
||||||
this._config = args.config as HeadingConfig
|
|
||||||
|
|
||||||
if (this._config.levels === undefined) {
|
|
||||||
this._config.levels = [HeadingLevel.Two, HeadingLevel.Three]
|
|
||||||
}
|
|
||||||
if (this._config.defaultLevel === undefined) {
|
|
||||||
this._config.defaultLevel = HeadingLevel.Two
|
|
||||||
}
|
|
||||||
if (this._config.levels.indexOf(this._config.defaultLevel) === -1) {
|
|
||||||
console.warn('(ง\'̀-\'́)ง Heading Tool: the default level specified was not found in available levels')
|
|
||||||
}
|
|
||||||
this.defaultLevel = this._config.defaultLevel
|
|
||||||
this.currentLevel = this.defaultLevel
|
|
||||||
|
|
||||||
// setting data will rerender the element with the right settings
|
|
||||||
this.data = {
|
|
||||||
level: this.currentLevel,
|
|
||||||
text: (args.data as HeadingData).text || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
this._settingButtons = this._config.levels.map(level => {
|
|
||||||
return {
|
|
||||||
name: `H${level}`,
|
|
||||||
icon: icons[level] || icon,
|
|
||||||
action: (name: string) => this.setLevel(name),
|
|
||||||
isActive: (name: string): boolean => this.isCurrentLevel(name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public get data (): HeadingData {
|
|
||||||
return this._data as HeadingData
|
|
||||||
}
|
|
||||||
|
|
||||||
public set data (data: HeadingData) {
|
|
||||||
const currentData = this._data as HeadingData
|
|
||||||
|
|
||||||
if (data.level === undefined) data.level = currentData.level || this.defaultLevel
|
|
||||||
if (data.text === undefined) data.text = currentData.text || ''
|
|
||||||
|
|
||||||
this._data = data
|
|
||||||
this.currentLevel = data.level
|
|
||||||
|
|
||||||
const newHeader = this._render()
|
|
||||||
if (this._element.parentNode) {
|
|
||||||
this._element.parentNode.replaceChild(newHeader, this._element)
|
|
||||||
}
|
|
||||||
this._element = newHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
private isCurrentLevel (name: string): boolean {
|
|
||||||
const currentLevel = `H${this.currentLevel}`
|
|
||||||
return name === currentLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
private setLevel (name: string) {
|
|
||||||
const level = parseInt(name[1], 10)
|
|
||||||
this.data = { level, text: this._element.innerHTML }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _render (): HTMLElement {
|
|
||||||
const el = document.createElement(`H${this.currentLevel}`)
|
|
||||||
el.innerHTML = this.data.text || ''
|
|
||||||
el.classList.add(this._CSS.block)
|
|
||||||
el.contentEditable = 'true'
|
|
||||||
el.dataset.placeholder = this._config.placeholder || ''
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle pasted H1-H6 tags to substitute with header tool
|
|
||||||
protected pasteHandler (element: HTMLHeadingElement): HeadingData {
|
|
||||||
const text = element.innerHTML
|
|
||||||
let level = this.defaultLevel
|
|
||||||
|
|
||||||
const tagMatch = element.tagName.match(/H(\d)/)
|
|
||||||
if (tagMatch) level = parseInt(tagMatch[1], 10)
|
|
||||||
|
|
||||||
// Fallback to nearest level when specified not available
|
|
||||||
if (this._config.levels) {
|
|
||||||
level = this._config.levels.reduce((prevLevel, currLevel) => {
|
|
||||||
return Math.abs(currLevel - level) < Math.abs(prevLevel - level) ? currLevel : prevLevel
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { level, text }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method that specified how to merge two Text blocks.
|
|
||||||
// Called by Editor.js by backspace at the beginning of the Block
|
|
||||||
public merge (data: HeadingData) {
|
|
||||||
this.data = {
|
|
||||||
text: this.data.text + (data.text || ''),
|
|
||||||
level: this.data.level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract tools data from view
|
|
||||||
public save (toolsContent: HTMLElement): HeadingData {
|
|
||||||
return {
|
|
||||||
text: toolsContent.innerHTML,
|
|
||||||
level: this.currentLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get sanitize () {
|
|
||||||
return { level: {} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Heading
|
|
|
@ -1,4 +0,0 @@
|
||||||
export { default as Delimiter } from './delimiter'
|
|
||||||
export { default as Heading } from './heading'
|
|
||||||
export { default as Charges } from './charges'
|
|
||||||
export { default as DnDStats } from './dnd-stats'
|
|
66
src/lib.ts
|
@ -1,66 +0,0 @@
|
||||||
import { CardSize, PageSize, Arrangement, Deck, Card } from './types'
|
|
||||||
|
|
||||||
export function randomId (): string {
|
|
||||||
const now = Date.now()
|
|
||||||
const rnd = Math.round(10000000 + Math.random() * 10000000).toString(36)
|
|
||||||
|
|
||||||
return `${now}.${rnd}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cardWHFromSize (size: CardSize): number[] {
|
|
||||||
return size.split('x').map(v => parseFloat(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function iconPath (icon: string): string {
|
|
||||||
return `/img/${icon}.svg`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cardSizeToStyle (size: CardSize): object {
|
|
||||||
const [w, h] = cardWHFromSize(size)
|
|
||||||
const ratio = w / h
|
|
||||||
|
|
||||||
return {
|
|
||||||
width: `calc(var(--card-height) * ${ratio})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultDeck (): Deck {
|
|
||||||
return {
|
|
||||||
id: randomId(),
|
|
||||||
icon: 'robe',
|
|
||||||
name: 'the nameless',
|
|
||||||
description: '',
|
|
||||||
color: '#3C1C00',
|
|
||||||
cards: [],
|
|
||||||
cardSize: CardSize.Poker,
|
|
||||||
pageSize: PageSize.A4,
|
|
||||||
arrangement: Arrangement.DoubleSided,
|
|
||||||
roundedCorners: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultCard (): Card {
|
|
||||||
return {
|
|
||||||
id: `c${randomId()}`,
|
|
||||||
name: 'no title yet',
|
|
||||||
count: 1,
|
|
||||||
tags: [],
|
|
||||||
icon: 'robe',
|
|
||||||
content: {
|
|
||||||
time: Date.now(),
|
|
||||||
blocks: [],
|
|
||||||
version: '2.17.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidDeck (deck: any): boolean {
|
|
||||||
const example = defaultDeck() as { [key: string]: any }
|
|
||||||
|
|
||||||
for (const key in example) {
|
|
||||||
const type = typeof example[key]
|
|
||||||
return typeof deck[key] === type
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
30
src/lib/card.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import randomId from './randomId'
|
||||||
|
import { Card, CardSize } from '../types'
|
||||||
|
|
||||||
|
export function defaultCard (): Card {
|
||||||
|
return {
|
||||||
|
id: `c${randomId()}`,
|
||||||
|
name: 'no title yet',
|
||||||
|
count: 1,
|
||||||
|
tags: [],
|
||||||
|
icon: 'robe',
|
||||||
|
content: {
|
||||||
|
time: Date.now(),
|
||||||
|
blocks: [],
|
||||||
|
version: '2.17.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cardWHFromSize (size: CardSize): number[] {
|
||||||
|
return size.split('x').map(v => parseFloat(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cardSizeToStyle (size: CardSize): { width: string } {
|
||||||
|
const [w, h] = cardWHFromSize(size)
|
||||||
|
const ratio = w / h
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: `calc(var(--card-height) * ${ratio})`
|
||||||
|
}
|
||||||
|
}
|
28
src/lib/deck.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import randomId from './randomId'
|
||||||
|
import { Deck, CardSize, PageSize, Arrangement } from '../types'
|
||||||
|
|
||||||
|
export function defaultDeck (): Deck {
|
||||||
|
return {
|
||||||
|
id: randomId(),
|
||||||
|
icon: 'robe',
|
||||||
|
name: 'the nameless',
|
||||||
|
description: '',
|
||||||
|
color: '#3C1C00',
|
||||||
|
cards: [],
|
||||||
|
cardSize: CardSize.Poker,
|
||||||
|
pageSize: PageSize.A4,
|
||||||
|
arrangement: Arrangement.DoubleSided,
|
||||||
|
roundedCorners: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidDeck (deck: any): boolean {
|
||||||
|
const example = defaultDeck() as { [key: string]: any }
|
||||||
|
|
||||||
|
for (const key in example) {
|
||||||
|
const type = typeof example[key]
|
||||||
|
return typeof deck[key] === type
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
3
src/lib/iconPath.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function iconPath (icon: string): string {
|
||||||
|
return `/img/${icon}.svg`
|
||||||
|
}
|
6
src/lib/randomId.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default function randomId (): string {
|
||||||
|
const now = Date.now()
|
||||||
|
const rnd = Math.round(10000000 + Math.random() * 10000000).toString(36)
|
||||||
|
|
||||||
|
return `${now}.${rnd}`
|
||||||
|
}
|
25
src/main.ts
|
@ -1,22 +1,11 @@
|
||||||
import './class-component-hooks'
|
import { createApp } from 'vue'
|
||||||
import Vue from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
// import './registerServiceWorker'
|
import state from './state'
|
||||||
import './directives'
|
|
||||||
|
|
||||||
import StorageHandler from './storage'
|
import App from './App.vue'
|
||||||
|
|
||||||
declare module 'vue/types/vue' {
|
const app = createApp(App)
|
||||||
interface Vue {
|
app.provide('state', state)
|
||||||
$storage: StorageHandler;
|
app.use(router)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
app.mount('#app')
|
||||||
Vue.prototype.$storage = new StorageHandler()
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
router,
|
|
||||||
render: h => h(App)
|
|
||||||
}).$mount('#app')
|
|
||||||
|
|
12
src/modules.d.ts
vendored
|
@ -1,12 +0,0 @@
|
||||||
declare module '*.vue' {
|
|
||||||
import Vue from 'vue'
|
|
||||||
export default Vue
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.txt' {
|
|
||||||
const content: string
|
|
||||||
export default content
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@editorjs/paragraph'
|
|
||||||
declare module '@editorjs/list'
|
|
|
@ -1,32 +0,0 @@
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import { register } from 'register-service-worker'
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
|
||||||
ready () {
|
|
||||||
console.log(
|
|
||||||
'App is being served from cache by a service worker.\n' +
|
|
||||||
'For more details, visit https://goo.gl/AFskqB'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
registered () {
|
|
||||||
console.log('Service worker has been registered.')
|
|
||||||
},
|
|
||||||
cached () {
|
|
||||||
console.log('Content has been cached for offline use.')
|
|
||||||
},
|
|
||||||
updatefound () {
|
|
||||||
console.log('New content is downloading.')
|
|
||||||
},
|
|
||||||
updated () {
|
|
||||||
console.log('New content is available; please refresh.')
|
|
||||||
},
|
|
||||||
offline () {
|
|
||||||
console.log('No internet connection found. App is running in offline mode.')
|
|
||||||
},
|
|
||||||
error (error) {
|
|
||||||
console.error('Error during service worker registration:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,34 +1,20 @@
|
||||||
import Vue from 'vue'
|
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
|
||||||
import VueRouter from 'vue-router'
|
import Home from '@/views/Home.vue'
|
||||||
import Home from './views/Home.vue'
|
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
// const AsyncDeck = () => import(/* webpackChunkName: "deck", webpackPrefetch: 10 */'./views/Deck.vue')
|
||||||
|
// const AsyncPrint = () => import(/* webpackChunkName: "print", webpackPrefetch: 1 */'./views/Print.vue')
|
||||||
|
const AsyncDeck = () => import(/* webpackChunkName: "deck" */'./views/Deck.vue')
|
||||||
|
const AsyncPrint = () => import(/* webpackChunkName: "print" */'./views/Print.vue')
|
||||||
|
|
||||||
const routes = [{
|
const isServer = typeof window === 'undefined'
|
||||||
path: '/',
|
const history = isServer ? createMemoryHistory() : createWebHistory()
|
||||||
name: 'Home',
|
|
||||||
component: Home
|
|
||||||
}, {
|
|
||||||
path: '/deck/:id',
|
|
||||||
name: 'Deck',
|
|
||||||
component: () => import(/* webpackChunkName "deck" */ './views/Deck.vue')
|
|
||||||
}, {
|
|
||||||
path: '/print/:id',
|
|
||||||
name: 'Print',
|
|
||||||
component: () => import(/* webpackChunkName "print" */ './views/Print.vue')
|
|
||||||
}, {
|
|
||||||
path: '/about',
|
|
||||||
name: 'About',
|
|
||||||
// route level code-splitting
|
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
|
||||||
// which is lazy-loaded when the route is visited.
|
|
||||||
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
|
|
||||||
}]
|
|
||||||
|
|
||||||
const router = new VueRouter({
|
export default createRouter({
|
||||||
mode: 'history',
|
history,
|
||||||
base: process.env.BASE_URL,
|
strict: true,
|
||||||
routes
|
routes: [
|
||||||
|
{ path: '/', name: 'Home', component: Home },
|
||||||
|
{ path: '/deck/:id', name: 'Deck', component: AsyncDeck },
|
||||||
|
{ path: '/print/:id', name: 'Print', component: AsyncPrint, meta: { bodyClass: 'print' } },
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
|
||||||
|
|
13
src/shims-tsx.d.ts
vendored
|
@ -1,13 +0,0 @@
|
||||||
import Vue, { VNode } from 'vue'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace JSX {
|
|
||||||
// tslint:disable no-empty-interface
|
|
||||||
interface Element extends VNode {}
|
|
||||||
// tslint:disable no-empty-interface
|
|
||||||
interface ElementClass extends Vue {}
|
|
||||||
interface IntrinsicElements {
|
|
||||||
[elem: string]: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
5
src/shims.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
declare module "*.vue" {
|
||||||
|
import { defineComponent } from "vue"
|
||||||
|
const Component: ReturnType<typeof defineComponent>
|
||||||
|
export default Component
|
||||||
|
}
|
51
src/state.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { reactive, ref, Ref } from 'vue'
|
||||||
|
import { State, Notification, Deck } from './types'
|
||||||
|
import { defaultDeck } from './lib/deck'
|
||||||
|
|
||||||
|
interface Payload {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state: State = {
|
||||||
|
settings: ref({}),
|
||||||
|
decks: ref([]),
|
||||||
|
notifications: ref([])
|
||||||
|
}
|
||||||
|
|
||||||
|
// { level: 'warning', title: 'This is a pre-alpha version.', content: 'Many features are still unstable or completely missing. Check out <a href="https://github.com/nkoehring/rpg-cards-ng/">the code repository</a> for more information.', dismissed: false },
|
||||||
|
// { level: 'info', title: '', content: 'Click the PLUS to create a new deck of cards.', dismissed: false },
|
||||||
|
|
||||||
|
/// actions are called like action['sub/foo'](state.sub, payload)
|
||||||
|
export const stateActions = {
|
||||||
|
'notifications/add' (notifications: Ref<Notification[]>, payload: Payload) {
|
||||||
|
notifications.value.push({
|
||||||
|
level: 'info',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
dismissed: false,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'notifications/dismiss' (notifications: Ref<Notification[]>, notification: Notification) {
|
||||||
|
notification.dismissed = true
|
||||||
|
notifications.value = notifications.value.filter(note => !note.dismissed)
|
||||||
|
},
|
||||||
|
'decks/new' (): Deck {
|
||||||
|
return defaultDeck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useState (field: string): { [key: string]: any } {
|
||||||
|
const collection = ref(state[field])
|
||||||
|
const actions = Object.keys(stateActions).reduce((acc, key) => {
|
||||||
|
if (key.startsWith(`${field}/`)) {
|
||||||
|
const newKey = key.split('/')[1]
|
||||||
|
acc[newKey] = (payload: Payload) => stateActions[key](collection, payload)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return { collection, actions }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reactive(state)
|
|
@ -1,48 +0,0 @@
|
||||||
import { Deck, StoredStuff } from './types'
|
|
||||||
const KEY = 'rpg-cards-ng'
|
|
||||||
|
|
||||||
export default class StorageHandler {
|
|
||||||
private cache: StoredStuff = {
|
|
||||||
decks: [],
|
|
||||||
defaults: {
|
|
||||||
color: '#3C1C00'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
if (localStorage.getItem(KEY) === undefined) this.persist()
|
|
||||||
|
|
||||||
const stored = localStorage.getItem(KEY)
|
|
||||||
if (stored !== null) this.cache = JSON.parse(stored)
|
|
||||||
}
|
|
||||||
|
|
||||||
get decks (): Deck[] {
|
|
||||||
return this.cache.decks
|
|
||||||
}
|
|
||||||
|
|
||||||
set decks (decks: Deck[]) {
|
|
||||||
this.cache.decks = decks
|
|
||||||
this.persist()
|
|
||||||
}
|
|
||||||
|
|
||||||
saveDeck (newDeck: Deck) {
|
|
||||||
const decks = this.cache.decks
|
|
||||||
const index = decks.findIndex(deck => deck.id === newDeck.id)
|
|
||||||
|
|
||||||
if (index >= 0) decks[index] = newDeck
|
|
||||||
else decks.push(newDeck)
|
|
||||||
|
|
||||||
this.persist()
|
|
||||||
}
|
|
||||||
|
|
||||||
findDeck (id: string): Deck | null {
|
|
||||||
for (const deck of this.cache.decks) {
|
|
||||||
if (deck.id === id) return deck
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
persist () {
|
|
||||||
localStorage.setItem(KEY, JSON.stringify(this.cache))
|
|
||||||
}
|
|
||||||
}
|
|
19
src/types.ts
|
@ -1,3 +1,5 @@
|
||||||
|
import { Ref } from 'vue'
|
||||||
|
|
||||||
// page width x page height
|
// page width x page height
|
||||||
export const enum PageSize {
|
export const enum PageSize {
|
||||||
A4 = '210mm 297mm',
|
A4 = '210mm 297mm',
|
||||||
|
@ -62,10 +64,23 @@ export interface Deck {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
color: string;
|
/* no global settings, yet */
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredStuff {
|
export interface StoredSettings {
|
||||||
decks: Deck[];
|
decks: Deck[];
|
||||||
defaults: Settings;
|
defaults: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
level: 'warning' | 'error' | 'info';
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
dismissed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
settings: Ref<Settings>;
|
||||||
|
decks: Ref<Deck[]>;
|
||||||
|
notifications: Ref<Notification[]>;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,172 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<main name="deck" class="loading" v-if="loading">
|
<h1>Welcome {{ name }}</h1>
|
||||||
<header>...wait for it...</header>
|
<p>This is a placeholder view.</p>
|
||||||
<router-link to="/">« or go back if you're impatient</router-link>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<main name="deck" class="not-found" v-else-if="notFound">
|
|
||||||
<header>Deck not found</header>
|
|
||||||
<router-link to="/">« lets go back home</router-link>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<main name="deck" :class="{ popup }" v-else>
|
|
||||||
<div class="deck-bg">
|
|
||||||
<img :src="deckIcon" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<span>{{ deck.name }}</span>
|
|
||||||
<button class="edit-button" @click="popup = 'edit'">edit</button>
|
|
||||||
<button class="print-button" @click="popup = 'print'">print</button>
|
|
||||||
|
|
||||||
<p>{{ deck.description }}</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section name="deck-cards" class="cards" :class="{ centered: !deck.cards.length }">
|
|
||||||
<deck-card v-for="(card, i) in deck.cards"
|
|
||||||
:key="card.id"
|
|
||||||
:card="card"
|
|
||||||
:deck="deck"
|
|
||||||
:is-selection="card === selection"
|
|
||||||
@click="selection = card"
|
|
||||||
@close="selection = null"
|
|
||||||
@edit="editCard(card, $event.field, $event.value)"
|
|
||||||
@delete="removeCard(i)"
|
|
||||||
/>
|
|
||||||
<deck-cover @click="newCard" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div id="popup" v-if="popup === 'edit'">
|
|
||||||
<div class="popup-content">
|
|
||||||
<EditDeckForm
|
|
||||||
:deck="deck"
|
|
||||||
@save="closeAndReload"
|
|
||||||
@close="popup = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="popup" v-else-if="popup === 'print'">
|
|
||||||
<div class="popup-content">
|
|
||||||
<PrintDeckForm
|
|
||||||
:deck="deck"
|
|
||||||
@save="closeAndReload"
|
|
||||||
@close="popup = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator'
|
import { defineComponent } from "vue"
|
||||||
import { Deck, Card } from '../types'
|
|
||||||
import DeckCover from '@/components/deck-cover.vue'
|
|
||||||
import DeckCard from '@/components/deck-card.vue'
|
|
||||||
import EditDeckForm from '@/components/edit-deck-form.vue'
|
|
||||||
import PrintDeckForm from '@/components/print-deck-form.vue'
|
|
||||||
import { iconPath, defaultCard } from '@/lib'
|
|
||||||
|
|
||||||
@Component({
|
const name = 'Deck'
|
||||||
components: { DeckCover, DeckCard, EditDeckForm, PrintDeckForm }
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
return { name }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
export default class DeckView extends Vue {
|
|
||||||
private popup = false
|
|
||||||
private notFound = false
|
|
||||||
private loading = true
|
|
||||||
private deck: Deck | null = null
|
|
||||||
private selection: Card | null = null
|
|
||||||
|
|
||||||
private mounted () {
|
|
||||||
const currentDeckId = this.$route.params.id
|
|
||||||
this.deck = this.$storage.findDeck(currentDeckId)
|
|
||||||
if (this.deck === null) this.notFound = true
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private get deckIcon () {
|
|
||||||
if (this.deck === null) return ''
|
|
||||||
return iconPath(this.deck.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private closeAndReload () {
|
|
||||||
this.deck = this.$storage.findDeck(this.deck?.id || '')
|
|
||||||
this.selection = null
|
|
||||||
this.popup = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private newCard () {
|
|
||||||
if (this.deck === null) return
|
|
||||||
|
|
||||||
const newCard = defaultCard()
|
|
||||||
newCard.content = {
|
|
||||||
time: Date.now(),
|
|
||||||
blocks: [{
|
|
||||||
type: 'heading',
|
|
||||||
data: {
|
|
||||||
text: 'Next Level RPG Card',
|
|
||||||
level: 2
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
type: 'delimiter',
|
|
||||||
data: { variant: 'pointing-left' }
|
|
||||||
}, {
|
|
||||||
type: 'paragraph',
|
|
||||||
data: { text: 'This card is a rich text editor so you can basically do whatever you want.' }
|
|
||||||
}, {
|
|
||||||
type: 'paragraph',
|
|
||||||
data: { text: ' ' }
|
|
||||||
}, {
|
|
||||||
type: 'paragraph',
|
|
||||||
data: { text: 'You see that delimiter over there? It seems to be wrong, or maybe you like it that way. In any way you can change it by clicking on it and then on the little tool button on the right.' }
|
|
||||||
}],
|
|
||||||
version: '2.17.0'
|
|
||||||
}
|
|
||||||
this.deck.cards.push(newCard)
|
|
||||||
this.$storage.persist()
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.selection = newCard
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private editCard<Card, K extends keyof Card> (card: Card, field: K, value: Card[K]) {
|
|
||||||
card[field] = value
|
|
||||||
this.$storage.persist()
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeCard (index: number) {
|
|
||||||
if (this.deck === null) return
|
|
||||||
if (this.deck.cards.length - 1 < index) return
|
|
||||||
|
|
||||||
const userIsSure = confirm('Are you sure you want to permanently delete this card?')
|
|
||||||
if (!userIsSure) return
|
|
||||||
|
|
||||||
this.deck.cards.splice(index, 1)
|
|
||||||
this.$storage.persist()
|
|
||||||
this.closeAndReload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.edit-button, .print-button {
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
.edit-button {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
.deck-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
max-height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.deck-bg > img {
|
|
||||||
filter: saturate(0%) blur(5px) opacity(8%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,54 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<main name="home" :class="{ popup }">
|
<header>RPG Cards for y'all</header>
|
||||||
<header>RPG Cards for y'all</header>
|
|
||||||
<section name="notifications">
|
<section name="deck-covers" class="cards" :class="{ centered: !decks.length }">
|
||||||
<p class="warning">
|
<router-link :to="{ name: 'Deck', params: { id: deck.id } }" :key="deck.id" v-for="deck in decks">
|
||||||
<strong>This is a pre-alpha version.</strong>
|
<CardBack :icon="deck.icon" :color="deck.color" :size="deck.cardSize">
|
||||||
Many features are still unstable or completely missing.
|
{{ deck.name }} ({{ deck.cards.length }})
|
||||||
<br />
|
</CardBack>
|
||||||
Check out <a href="https://github.com/nkoehring/rpg-cards-ng/">the code repository</a> for more information.
|
</router-link>
|
||||||
</p>
|
<CardBack @click="newDeck" icon="plus" />
|
||||||
</section>
|
</section>
|
||||||
<section name="deck-covers" class="cards" :class="{ centered: !savedDecks.length }">
|
|
||||||
<router-link :to="{ name: 'Deck', params: { id: deck.id } }" :key="deck.id" v-for="deck in savedDecks">
|
|
||||||
<deck-cover :deck="deck" />
|
|
||||||
</router-link>
|
|
||||||
<deck-cover @click="newDeck" />
|
|
||||||
</section>
|
|
||||||
<p class="info" v-if="savedDecks.length === 0">Click the PLUS to create a new deck of cards.</p>
|
|
||||||
|
|
||||||
<div id="popup" v-show="popup">
|
|
||||||
<div class="popup-content">
|
|
||||||
<NewDeckForm @save="popup = false" @close="popup = false" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator'
|
import { defineComponent } from 'vue'
|
||||||
import { Deck } from '../types'
|
import { useState } from '@/state'
|
||||||
import DeckCover from '@/components/deck-cover.vue'
|
|
||||||
import NewDeckForm from '@/components/new-deck-form.vue'
|
|
||||||
|
|
||||||
@Component({
|
import CardBack from '@/components/CardBack.vue'
|
||||||
components: { NewDeckForm, DeckCover }
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Home',
|
||||||
|
components: { CardBack },
|
||||||
|
setup () {
|
||||||
|
const { collection: decks, actions } = useState('decks')
|
||||||
|
return {
|
||||||
|
decks,
|
||||||
|
newDeck: actions['decks/new']
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
export default class Home extends Vue {
|
|
||||||
private popup = false
|
|
||||||
private savedDecks: Deck[];
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this.savedDecks = this.$storage.decks
|
|
||||||
}
|
|
||||||
|
|
||||||
private editDeck (deck: Deck) {
|
|
||||||
console.log('would edit deck', deck.name, 'now')
|
|
||||||
}
|
|
||||||
|
|
||||||
private newDeck () {
|
|
||||||
this.popup = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,122 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<main id="print-view" name="print-view" :class="{ loading, 'not-found': notFound }" :style="pageSizeCSS">
|
<h1>Welcome {{ name }}</h1>
|
||||||
<div class="loading" v-if="loading">— loading —</div>
|
<p>This is a placeholder view.</p>
|
||||||
<div class="not-found" v-else-if="notFound">Deck not found :(</div>
|
|
||||||
<template v-else>
|
|
||||||
<div class="page">
|
|
||||||
<Card :key="card.id" v-for="card in deck.cards"
|
|
||||||
:card="card"
|
|
||||||
:deck="deck"
|
|
||||||
:show-front="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="page">
|
|
||||||
<header>Page 2</header>
|
|
||||||
<p>foo bar baz</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</main>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator'
|
import { defineComponent } from "vue"
|
||||||
import { Deck } from '../types'
|
|
||||||
import { defaultCardSize, defaultPageSize } from '../consts'
|
|
||||||
import Card from '../components/static-card.vue'
|
|
||||||
import { iconPath } from '../lib'
|
|
||||||
|
|
||||||
interface Dimensions {
|
const name = 'Print'
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
components: { Card }
|
setup() {
|
||||||
|
return { name }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
export default class PrintDeck extends Vue {
|
|
||||||
private loading = true
|
|
||||||
private notFound = false
|
|
||||||
private deck: Deck | null = null
|
|
||||||
|
|
||||||
private landscape = false // TODO: not yet implemented
|
|
||||||
|
|
||||||
private mounted () {
|
|
||||||
const currentDeckId = this.$route.params.id
|
|
||||||
this.deck = this.$storage.findDeck(currentDeckId)
|
|
||||||
if (this.deck === null) this.notFound = true
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private get pageSize (): Dimensions {
|
|
||||||
const pageSize = this.deck === null ? defaultPageSize : this.deck.pageSize
|
|
||||||
const [width, height] = pageSize.split(' ').map(x => parseFloat(x))
|
|
||||||
return { width, height }
|
|
||||||
}
|
|
||||||
|
|
||||||
private get cardSize (): Dimensions {
|
|
||||||
const cardSize = this.deck === null ? defaultCardSize : this.deck.cardSize
|
|
||||||
const [height, width] = cardSize.split('x').map(x => parseFloat(x))
|
|
||||||
return { width, height }
|
|
||||||
}
|
|
||||||
|
|
||||||
private get cardsPerPage (): number {
|
|
||||||
if (this.deck === null || this.deck.cards.length === 0) return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private get deckIcon () {
|
|
||||||
if (this.deck === null) return ''
|
|
||||||
return iconPath(this.deck.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get pageSizeCSS () {
|
|
||||||
const cardHeight = `${this.cardSize.height}mm`
|
|
||||||
const cardWidth = `${this.cardSize.width}mm`
|
|
||||||
|
|
||||||
const pageHeight = `${this.pageSize.height}mm`
|
|
||||||
const pageWidth = `${this.pageSize.width}mm`
|
|
||||||
|
|
||||||
console.log(cardHeight, cardWidth, pageHeight, pageWidth)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'--width': pageWidth,
|
|
||||||
'--height': pageHeight,
|
|
||||||
'--card-width': cardWidth,
|
|
||||||
'--card-height': cardHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
@page {
|
|
||||||
margin: 0;
|
|
||||||
size: var(--size);
|
|
||||||
}
|
|
||||||
#print-view > .page {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-content: flex-start;
|
|
||||||
page-break-after: always;
|
|
||||||
width: var(--width);
|
|
||||||
height: var(--height);
|
|
||||||
margin: 5mm auto;
|
|
||||||
padding: 1cm 9mm;
|
|
||||||
background-color: white;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
html,body {
|
|
||||||
background-color: gray;
|
|
||||||
}
|
|
||||||
#app > .home-link {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#print-view > .page {
|
|
||||||
margin: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,39 +1,26 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"allowJs": true,
|
||||||
"module": "esnext",
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"importHelpers": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"declaration": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noLib": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"suppressImplicitAnyIndexErrors": true,
|
||||||
|
"target": "es2015",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"types": [
|
|
||||||
"webpack-env"
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.tsx",
|
|
||||||
"src/**/*.vue",
|
|
||||||
"tests/**/*.ts",
|
|
||||||
"tests/**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"./node_modules"
|
||||||
]
|
],
|
||||||
|
"include": [
|
||||||
|
"./src/**/*.ts",
|
||||||
|
"./src/**/*.vue",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
chainWebpack: config => {
|
|
||||||
config.module
|
|
||||||
.rule('raw')
|
|
||||||
.test(/\.txt$/)
|
|
||||||
.use('raw-loader')
|
|
||||||
.loader('raw-loader')
|
|
||||||
.end()
|
|
||||||
}
|
|
||||||
}
|
|
96
webpack.config.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const pkg = require('./package.json')
|
||||||
|
const { resolve } = require('path')
|
||||||
|
const { VueLoaderPlugin } = require('vue-loader')
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const FaviconsWebpackPlugin = require('favicons-webpack-plugin')
|
||||||
|
const SriPlugin = require('webpack-subresource-integrity')
|
||||||
|
|
||||||
|
const htmlConfig = require('./html.config.json') || {}
|
||||||
|
const outputPath = resolve(__dirname, './dist')
|
||||||
|
const publicPath = resolve(__dirname, './public')
|
||||||
|
|
||||||
|
module.exports = (env = {}) => ({
|
||||||
|
mode: env.prod ? 'production' : 'development',
|
||||||
|
devtool: env.prod ? false : 'eval-source-map',
|
||||||
|
entry: resolve(__dirname, './src/main.ts'),
|
||||||
|
output: {
|
||||||
|
path: outputPath,
|
||||||
|
crossOriginLoading: 'anonymous'
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'async',
|
||||||
|
minSize: 32000,
|
||||||
|
maxSize: 48000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.vue$/i,
|
||||||
|
use: 'vue-loader'
|
||||||
|
}, {
|
||||||
|
test: /\.ts$/i,
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: { appendTsSuffixTo: [/\.vue$/] }
|
||||||
|
}, {
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: ['style-loader', 'css-loader']
|
||||||
|
}, {
|
||||||
|
test: /\.(png|jpg|gif)$/i,
|
||||||
|
loader: 'url-loader',
|
||||||
|
options: { limit: 8192 }
|
||||||
|
}, {
|
||||||
|
test: /\.(png|jpg|gif|svg)$/i,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name (/*resourcePath, resourceQuery*/) {
|
||||||
|
// see https://github.com/webpack-contrib/file-loader
|
||||||
|
// `resourcePath` - `/absolute/path/to/file.js`
|
||||||
|
// `resourceQuery` - `?foo=bar`
|
||||||
|
return env.prod ? '[contenthash].[ext]' : '[path][name].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
test: /\.(txt|raw)$/i,
|
||||||
|
use: 'raw-loader'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js', '.vue', '.json'],
|
||||||
|
alias: {
|
||||||
|
'vue': '@vue/runtime-dom',
|
||||||
|
'@': resolve(__dirname, './src/'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new VueLoaderPlugin(),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [{ from: publicPath, to: outputPath }]
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
title: htmlConfig.title || pkg.name,
|
||||||
|
meta: htmlConfig.meta || {},
|
||||||
|
// TODO: not setting template option kinda breaks the build
|
||||||
|
template: resolve(__dirname, htmlConfig.template || './index.html'),
|
||||||
|
scriptLoading: htmlConfig.scriptLoading || 'defer',
|
||||||
|
hash: true
|
||||||
|
}),
|
||||||
|
new FaviconsWebpackPlugin({
|
||||||
|
logo: htmlConfig.logo || './logo.png',
|
||||||
|
// see https://github.com/itgalaxy/favicons#usage
|
||||||
|
favicons: htmlConfig.favicons || {}
|
||||||
|
}),
|
||||||
|
new SriPlugin({
|
||||||
|
hashFuncNames: ['sha512'],
|
||||||
|
enabled: env.prod
|
||||||
|
})
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
inline: true,
|
||||||
|
hot: true,
|
||||||
|
stats: 'minimal',
|
||||||
|
contentBase: resolve(__dirname, 'dist'),
|
||||||
|
overlay: true
|
||||||
|
}
|
||||||
|
})
|