diff options
author | Jenny Yu <jennyyu212@outlook.com> | 2022-09-12 21:57:15 -0400 |
---|---|---|
committer | Jenny Yu <jennyyu212@outlook.com> | 2022-09-12 21:57:15 -0400 |
commit | 7da791491a588f2a2a177a4eb144311ba49f5782 (patch) | |
tree | ec715830946f7e1329135ff12be2c1d66ac65149 | |
parent | 7f0eacf3fc0b54ceb4d574a719208861789581d3 (diff) | |
parent | 4315a0378bc54ae9eaa684d416839f635c38e865 (diff) |
Merge branch 'master' into sharing-jenny (break)
155 files changed, 11844 insertions, 8085 deletions
diff --git a/.gitignore b/.gitignore index 7a3c82ee6..7d3a4d214 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ src/server/session_manager/logs/**/*.log debug.log .vscodeignore Dockerfile +.vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json index a7c30ca1e..ce9f50f67 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,9 +12,7 @@ "breakOnLoad": true, "url": "http://localhost:1050/login", "webRoot": "${workspaceFolder}", - "runtimeArgs": [ - "--experimental-modules" - ] + "runtimeArgs": ["--experimental-modules"] }, { "type": "chrome", @@ -25,18 +23,21 @@ "url": "http://localhost:1050/login", "webRoot": "${workspaceFolder}", "runtimeExecutable": "/usr/bin/chromium", - "runtimeArgs": [ - "--experimental-modules" - ] + "runtimeArgs": ["--experimental-modules"] }, { "type": "firefox", "request": "launch", "name": "Launch Firefox against localhost", - //"sourceMaps": "client", "reAttach": true, "url": "http://localhost:1050/login", "webRoot": "${workspaceFolder}", + "pathMappings": [ + { + "url": "webpack://dash/src", + "path": "${workspaceFolder}/src" + } + ] }, { "type": "chrome", @@ -45,8 +46,7 @@ "sourceMaps": true, "breakOnLoad": true, "url": "https://browndash.com/login", - //"url": "http://dash-web.eastus2.cloudapp.azure.com:1050/login", - "webRoot": "${workspaceFolder}", + "webRoot": "${workspaceFolder}" }, { "type": "node", @@ -62,13 +62,7 @@ "request": "launch", "name": "Current TS File", "runtimeExecutable": "npx", - "runtimeArgs": [ - "ts-node-dev", - "--nolazy", - "--inspect", - "--", - "${relativeFile}" - ], + "runtimeArgs": ["ts-node-dev", "--nolazy", "--inspect", "--", "${relativeFile}"], "port": 9229 }, { @@ -76,14 +70,7 @@ "request": "launch", "name": "Mocha Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": [ - "-r", - "ts-node/register", - "--timeout", - "999999", - "--colors", - "${workspaceFolder}/test/**/*.ts" - ], + "args": ["-r", "ts-node/register", "--timeout", "999999", "--colors", "${workspaceFolder}/test/**/*.ts"], "console": "integratedTerminal", "internalConsoleOptions": "openOnSessionStart", "protocol": "inspector" @@ -93,17 +80,10 @@ "request": "launch", "name": "Mocha Current File", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": [ - "-r", - "ts-node/register", - "--timeout", - "999999", - "--colors", - "${file}" - ], + "args": ["-r", "ts-node/register", "--timeout", "999999", "--colors", "${file}"], "console": "integratedTerminal", "internalConsoleOptions": "openOnSessionStart", "protocol": "inspector" - }, + } ] -}
\ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 3b9cda8bb..5c186f43f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -926,6 +926,106 @@ "react-is": "^16.8.0 || ^17.0.0" } }, + "@octokit/auth-token": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.0.tgz", + "integrity": "sha512-MDNFUBcJIptB9At7HiV7VCvU3NcL4GnfCQaP8C5lrxWrRPMJBnemYtehaKSOlaM7AYxeRyj9etenu8LVpSpVaQ==", + "requires": { + "@octokit/types": "^6.0.3" + } + }, + "@octokit/core": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.4.tgz", + "integrity": "sha512-sUpR/hc4Gc7K34o60bWC7WUH6Q7T6ftZ2dUmepSyJr9PRF76/qqkWjE2SOEzCqLA5W83SaISymwKtxks+96hPQ==", + "requires": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.0.tgz", + "integrity": "sha512-Kz/mIkOTjs9rV50hf/JK9pIDl4aGwAtT8pry6Rpy+hVXkAPhXanNQRxMoq6AeRgDCZR6t/A1zKniY2V1YhrzlQ==", + "requires": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, + "@octokit/graphql": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.0.tgz", + "integrity": "sha512-1ZZ8tX4lUEcLPvHagfIVu5S2xpHYXAmgN0+95eAOPoaVPzCfUXJtA5vASafcpWcO86ze0Pzn30TAx72aB2aguQ==", + "requires": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "@octokit/request": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.0.tgz", + "integrity": "sha512-7IAmHnaezZrgUqtRShMlByJK33MT9ZDnMRgZjnRrRV9a/jzzFwKGz0vxhFU6i7VMLraYcQ1qmcAOin37Kryq+Q==", + "requires": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, + "@octokit/request-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.0.tgz", + "integrity": "sha512-WBtpzm9lR8z4IHIMtOqr6XwfkGvMOOILNLxsWvDwtzm/n7f5AWuqJTXQXdDtOvPfTDrH4TPhEvW2qMlR4JFA2w==", + "requires": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "requires": { + "@octokit/openapi-types": "^12.11.0" + } + }, "@react-google-maps/api": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.12.0.tgz", @@ -1038,7 +1138,7 @@ "@types/bcrypt-nodejs": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@types/bcrypt-nodejs/-/bcrypt-nodejs-0.0.30.tgz", - "integrity": "sha1-TN2WtJKTs5MhIuS34pVD415rrlg=", + "integrity": "sha512-gSWCu7EOXhcM0FYM8tanV0deaX1VM07sgBS8dSUmgnAQ5/3Xn6k+mWF+ZfE+c/OCW14IVnNdTOKcHgvL78oDFQ==", "dev": true }, "@types/bezier-js": { @@ -1204,6 +1304,14 @@ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz", "integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg==" }, + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "requires": { + "@types/ms": "*" + } + }, "@types/dom-speech-recognition": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz", @@ -1377,6 +1485,14 @@ "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.49.1.tgz", "integrity": "sha512-Sbl5anucT7LUcUxXsRxkCozHdXIkUiY+Tyru+OVl5rot0+VIZuuulmABC7X+nF7rL7BRTAguSBSAD/e/AfIkkA==" }, + "@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "requires": { + "@types/unist": "*" + } + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -1491,6 +1607,19 @@ "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", "dev": true }, + "@types/mdast": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", + "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "requires": { + "@types/unist": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -1543,6 +1672,11 @@ "mongoose": "*" } }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, "@types/node": { "version": "10.17.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", @@ -2109,7 +2243,7 @@ "@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I=", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", "dev": true }, "@types/strip-json-comments": { @@ -2145,7 +2279,7 @@ "@types/typescript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/typescript/-/typescript-2.0.0.tgz", - "integrity": "sha1-xDNTnJi64oaCswfqp6D9IRW4PCg=", + "integrity": "sha512-WMEWfMISiJ2QKyk5/dSdgL0ZwP//PZj0jmDU0hMh51FmLq4WIYzjlngsUQZXejQL+QtkXJUOGjb3G3UCvgZuSQ==", "dev": true, "requires": { "typescript": "*" @@ -2168,6 +2302,11 @@ } } }, + "@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, "@types/uuid": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.10.tgz", @@ -2455,7 +2594,7 @@ "textarea-caret": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.0.2.tgz", - "integrity": "sha512-gRzeti2YS4did7UJnPQ47wrjD+vp+CJIe9zbsu0bJ987d8QVLvLNG9757rqiQTIy4hGIeFauTTJt5Xkn51UkXg==" + "integrity": "sha1-82DEhpmqGr9xhoCkOjGoUGZcLK8=" } } }, @@ -2474,7 +2613,7 @@ "Base64": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", - "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" + "integrity": "sha512-reGEWshDmTDQDsCec/HduOO9Wyj6yMOupMfhIf3ugN1TDlK2NQW4DDJSqNNtp380SNcvRfXtO8HSCQot0d0SMw==" }, "D": { "version": "1.0.0", @@ -2517,7 +2656,7 @@ "acorn-globals": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", - "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "integrity": "sha512-uWttZCk96+7itPxK8xCzY86PnxKTMrReKDqrHzv42VQY0K30PUO8WY13WMOuI+cOdX4EIdzdvQ8k6jkuGRFMYw==", "requires": { "acorn": "^4.0.4" }, @@ -2525,7 +2664,7 @@ "acorn": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + "integrity": "sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==" } } }, @@ -2554,7 +2693,7 @@ "after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==" + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, "agent-base": { "version": "6.0.2", @@ -2629,7 +2768,7 @@ "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -2644,7 +2783,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -2654,12 +2793,12 @@ "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==" }, "ansi-align": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "integrity": "sha512-TdlOggdA/zURfMYa7ABC66j+oqfMew58KpJMbUlH3bcZP1b+cBHIHDDn5uH9INsxrHBPjsqM0tDB4jPTF/vgJA==", "requires": { "string-width": "^2.0.0" } @@ -2723,7 +2862,7 @@ "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "anymatch": { "version": "2.0.0", @@ -2737,7 +2876,7 @@ "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "requires": { "remove-trailing-separator": "^1.0.1" } @@ -2832,7 +2971,7 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==" }, "arr-flatten": { "version": "1.1.0", @@ -2842,7 +2981,7 @@ "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==" }, "array-back": { "version": "2.0.0", @@ -2933,18 +3072,18 @@ "array-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "integrity": "sha512-H3LU5RLiSsGXPhN+Nipar0iR0IofH+8r89G2y1tBKxQ/agagKyAjhkAFDRBfodP2caPrNKHpAWNIM/c9yeL7uA==", "dev": true }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==" }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "array-includes": { "version": "3.1.5", @@ -2962,7 +3101,7 @@ "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", "dev": true, "requires": { "array-uniq": "^1.0.1" @@ -2971,13 +3110,13 @@ "array-uniq": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "dev": true }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==" }, "array.prototype.flat": { "version": "1.3.0", @@ -3028,7 +3167,7 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "asn1": { "version": "0.2.6", @@ -3041,7 +3180,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, "assertion-error": { "version": "1.1.0", @@ -3051,7 +3190,7 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==" }, "ast-types-flow": { "version": "0.0.7", @@ -3081,7 +3220,7 @@ "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" + "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==" }, "async-limiter": { "version": "1.0.1", @@ -3092,7 +3231,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "atob": { "version": "2.1.2", @@ -3142,7 +3281,7 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { "version": "1.11.0", @@ -3172,12 +3311,12 @@ "babel": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel/-/babel-6.23.0.tgz", - "integrity": "sha1-0NHn2APpdHZb7qMjLU4VPA77kPQ=" + "integrity": "sha512-ZDcCaI8Vlct8PJ3DvmyqUz+5X2Ylz3ZuuItBe/74yXosk2dwyVo/aN7MCJ8HJzhnnJ+6yP4o+lDgG9MBe91DLA==" }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", "dev": true, "requires": { "chalk": "^1.1.3", @@ -3188,19 +3327,19 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "dev": true }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "dev": true, "requires": { "ansi-styles": "^2.2.1", @@ -3213,7 +3352,7 @@ "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", "dev": true }, "strip-ansi": { @@ -3297,12 +3436,12 @@ "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -3323,7 +3462,7 @@ "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", "requires": { "babel-runtime": "^6.26.0", "esutils": "^2.0.2", @@ -3346,7 +3485,12 @@ "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==" + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==" }, "balanced-match": { "version": "1.0.2", @@ -3370,7 +3514,7 @@ "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "requires": { "is-descriptor": "^1.0.0" } @@ -3406,12 +3550,12 @@ "base16": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", - "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=" + "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==" }, "base64-arraybuffer": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==" + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" }, "base64-js": { "version": "1.5.1", @@ -3431,26 +3575,31 @@ "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, "bcrypt-nodejs": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/bcrypt-nodejs/-/bcrypt-nodejs-0.0.3.tgz", - "integrity": "sha1-xgkX8m3CNWYVZsaBBhwwPCsohCs=" + "integrity": "sha512-NmTbLm867btBHCBZ222FQXkQKzecB0KG6pTXFa6NeTVZaSnLfCsx7EK2PL3J+kX8xJThUquEBbhimRCKKZX9zA==" }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "requires": { "tweetnacl": "^0.14.3" } }, + "before-after-hook": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + }, "bezier-curve": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/bezier-curve/-/bezier-curve-1.0.0.tgz", - "integrity": "sha1-o9+v6rEqlMRicw1QeYxSqEBdc3k=" + "integrity": "sha512-h6uZJ6qdFfswS1rIRericgouhTeiVi/MnH10OKtCu2IZzXa+ZcjaxRLHY4u/evRsJcxYbbiNkgWQj2Z4UIcpEQ==" }, "bezier-js": { "version": "4.1.1", @@ -3499,7 +3648,7 @@ "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "integrity": "sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==", "requires": { "inherits": "~2.0.0" } @@ -3562,7 +3711,7 @@ "bonjour": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==", "dev": true, "requires": { "array-flatten": "^2.1.0", @@ -3584,7 +3733,7 @@ "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "bootstrap": { "version": "4.6.1", @@ -3613,7 +3762,7 @@ "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==" } } }, @@ -3646,7 +3795,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "requires": { "is-extendable": "^0.1.0" } @@ -3656,7 +3805,7 @@ "browser-assert": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", - "integrity": "sha1-mqpaKox0aFwq4Fv+Ru/WBvBowgA=" + "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==" }, "browser-process-hrtime": { "version": "1.0.0", @@ -3701,12 +3850,12 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "buffer-from": { "version": "1.1.2", @@ -3722,12 +3871,12 @@ "buffer-shims": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + "integrity": "sha512-Zy8ZXMyxIT6RMTeY7OP/bDndfj6bwCan7SS98CEndS6deHwWPpseeHlwarNcBim+etXnF9HBc1non5JgDaJU1g==" }, "built-in-math-eval": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/built-in-math-eval/-/built-in-math-eval-0.3.0.tgz", - "integrity": "sha1-JA3CHLOJQ5WIxhxGDrAHZJfvxBw=", + "integrity": "sha512-5XD5cujru60ooKJ4sGZqoH5v2Xvgw7ezV54gJX/OnPkgDKoH3BnlMEi8xW6hl8xaEjxKHebgrsawroeZnGwIMA==", "requires": { "math-codegen": "^0.3.5" } @@ -3858,7 +4007,7 @@ "caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", "requires": { "callsites": "^2.0.0" }, @@ -3866,14 +4015,14 @@ "callsites": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==" } } }, "caller-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", "requires": { "caller-callsite": "^2.0.0" } @@ -3900,7 +4049,7 @@ "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", "requires": { "camelcase": "^2.0.0", "map-obj": "^1.0.0" @@ -3909,14 +4058,14 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==" } } }, "camelize": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + "integrity": "sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg==" }, "caniuse-lite": { "version": "1.0.30001351", @@ -3941,12 +4090,17 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", "requires": { "align-text": "^0.1.3", "lazy-cache": "^1.0.3" @@ -3984,12 +4138,17 @@ "change-emitter": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", - "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" + "integrity": "sha512-YXzt1cQ4a2jqazhcuSWEOc1K2q8g9H6eWNsyZgi640LDzRWVQ2eDe+Y/kVdftH+vYdPF2rgDb3dLdpxE1jvAxw==" + }, + "character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==" }, "character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", "requires": { "is-regex": "^1.0.3" } @@ -4003,12 +4162,12 @@ "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==" }, "child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha1-sffn/HPSXn/R1FWtyU4UODAYK1o=" + "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" }, "chokidar": { "version": "2.1.8", @@ -4037,7 +4196,7 @@ "chrome": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chrome/-/chrome-0.1.0.tgz", - "integrity": "sha1-9h2beS/v6MGUxwVt3BAscmqGQyk=", + "integrity": "sha512-6KYl20U4Taj6YipylsWr2etUvp9AElJKfGNSBmyGTymYmancnOb041ZNadolEZi2nboLXH7jMSqUmm4kpuTzfg==", "requires": { "exeq": "^2.2.0", "plist": "^1.1.0" @@ -4057,7 +4216,7 @@ "clamp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", - "integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ=" + "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==" }, "class-transformer": { "version": "0.2.3", @@ -4078,7 +4237,7 @@ "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "requires": { "is-descriptor": "^0.1.0" } @@ -4108,7 +4267,7 @@ "cli-boxes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + "integrity": "sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==" }, "cli-cursor": { "version": "3.1.0", @@ -4128,7 +4287,7 @@ "clipboard": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", - "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", + "integrity": "sha512-smkaRaIQsrnKN1F3wd1/vY9Q+DeR4L8ZCXKeHCFC2j8RZuSBbuImcLdnIO4GTxmzJxQuDGNKkyfpGoPW7Ua5bQ==", "requires": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -4153,7 +4312,7 @@ "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==" }, "yargs-parser": { "version": "7.0.0", @@ -4203,7 +4362,7 @@ "clj-fuzzy": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/clj-fuzzy/-/clj-fuzzy-0.3.3.tgz", - "integrity": "sha1-seU0MJHFIC28UlMoY+HEp4RX8D0=" + "integrity": "sha512-9cyh9A8+OphDZeKIG21MgyDHWDkWxTvagwvFLVjtdi6eToFENF7iDLlKwhHrnBQRSQwprKNhazG053nE/UgwfQ==" }, "clone-deep": { "version": "4.0.1", @@ -4218,7 +4377,7 @@ "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "requires": { "mimic-response": "^1.0.0" }, @@ -4238,12 +4397,12 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -4269,7 +4428,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "color-string": { "version": "1.9.1", @@ -4303,6 +4462,11 @@ "delayed-stream": "~1.0.0" } }, + "comma-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz", + "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==" + }, "command-exists": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.6.tgz", @@ -4327,13 +4491,13 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==" + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" }, "component-emitter": { "version": "1.3.0", @@ -4343,7 +4507,7 @@ "component-inherit": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==" + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "compress-brotli": { "version": "1.3.8", @@ -4408,7 +4572,7 @@ "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true }, "debug": { @@ -4423,7 +4587,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } @@ -4431,7 +4595,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "concat-stream": { "version": "1.6.2", @@ -4497,7 +4661,7 @@ "connect-flash": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", - "integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA=" + "integrity": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==" }, "connect-history-api-fallback": { "version": "1.6.0", @@ -4526,12 +4690,12 @@ "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + "integrity": "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==" }, "readable-stream": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", - "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", + "integrity": "sha512-a6ibcfWFhgihuTw/chl+u3fB5ykBZFmnvpyZHebY0MCQE4vvYcsCLpCeaQ1BkH7HdJYavNSqF0WDLeo4IPHQaQ==", "requires": { "buffer-shims": "~1.0.0", "core-util-is": "~1.0.0", @@ -4555,7 +4719,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "constantinople": { "version": "3.1.2", @@ -4639,7 +4803,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "cookies": { "version": "0.8.0", @@ -4684,7 +4848,7 @@ "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==" }, "copy-webpack-plugin": { "version": "4.6.0", @@ -4714,7 +4878,7 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true }, "serialize-javascript": { @@ -4728,7 +4892,7 @@ "core-js": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + "integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==" }, "core-js-pure": { "version": "3.22.8", @@ -4792,7 +4956,7 @@ "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "integrity": "sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==", "requires": { "capture-stack-trace": "^1.0.0" } @@ -4857,7 +5021,7 @@ "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "integrity": "sha512-eZ+m1WNhSZutOa/uRblAc9Ut5MQfukFrFMtPSm3bZCA888NmMd5AWXWdgRZ80zd+pTk1P2JrGjg9pUPTvl2PWQ==", "requires": { "lru-cache": "^4.0.1", "which": "^1.2.9" @@ -4887,7 +5051,7 @@ "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==" }, "css-box-model": { "version": "1.2.1", @@ -4900,7 +5064,7 @@ "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" }, "css-in-js-utils": { "version": "2.0.1", @@ -5043,7 +5207,7 @@ "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", "requires": { "array-find-index": "^1.0.1" } @@ -5051,14 +5215,24 @@ "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==" + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==", "dev": true }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "d3-array": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", @@ -5193,7 +5367,7 @@ "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "requires": { "assert-plus": "^1.0.0" } @@ -5245,7 +5419,7 @@ "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=" + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==" }, "debounce": { "version": "1.2.1", @@ -5263,12 +5437,20 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "requires": { + "character-entities": "^2.0.0" + } }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" }, "decompress-response": { "version": "4.2.1", @@ -5433,7 +5615,7 @@ "globby": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", "dev": true, "requires": { "array-union": "^1.0.1", @@ -5446,7 +5628,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -5471,7 +5653,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "delegate": { "version": "3.2.0", @@ -5481,7 +5663,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, "denque": { "version": "1.5.1", @@ -5584,7 +5766,7 @@ "import-fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", "requires": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" @@ -5619,7 +5801,7 @@ "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "requires": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -5633,7 +5815,7 @@ "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==" }, "string-width": { "version": "4.2.3", @@ -5697,10 +5879,20 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "deps-regex": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.1.4.tgz", - "integrity": "sha1-UYZnt2kUYKXn4KNBvnbrfOgJAYQ=" + "integrity": "sha512-3tzwGYogSJi8HoG93R5x9NrdefZQOXgHgGih/7eivloOq6yC6O+yoFxZnkgP661twvfILONfoKRdF9GQOGx2RA==" + }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, "destroy": { "version": "1.2.0", @@ -5749,7 +5941,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true } } @@ -5757,7 +5949,7 @@ "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", "dev": true }, "dns-packet": { @@ -5773,7 +5965,7 @@ "dns-txt": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==", "dev": true, "requires": { "buffer-indexof": "^1.0.0" @@ -5791,7 +5983,7 @@ "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" }, "dom-converter": { "version": "0.2.0", @@ -5900,12 +6092,12 @@ "double-bits": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/double-bits/-/double-bits-1.1.1.tgz", - "integrity": "sha1-WKu6RUlNpND6Nrc60RoobJGEscY=" + "integrity": "sha512-BCLEIBq0O/DWoA7BsCu/R+RP0ZXiowP8BhtJT3qeuuQEBpnS8LK/Wo6UTJQv6v8mK1fj8n90YziHLwGdM5whSg==" }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "integrity": "sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==" }, "duplexify": { "version": "3.7.1", @@ -5939,7 +6131,7 @@ "dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", - "integrity": "sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE=", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", "dev": true, "requires": { "xtend": "^4.0.0" @@ -5948,7 +6140,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -5965,7 +6157,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { "version": "1.4.149", @@ -5994,7 +6186,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "encoding": { "version": "0.1.13", @@ -6069,7 +6261,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "ws": { "version": "7.4.6", @@ -6208,10 +6400,32 @@ "is-symbol": "^1.0.2" } }, + "es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "dev": true, + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "es6-promise": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", - "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + "integrity": "sha512-oj4jOSXvWglTsc3wrw86iom3LDPOx1nbipQk+jaG3dy+sMRM6ReSgVr/VlmBuF6lXUrflN9DCcQHeSbAwGUl4g==" }, "es6-symbol": { "version": "3.1.3", @@ -6219,6 +6433,7 @@ "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { + "d": "^1.0.1", "ext": "^1.1.2" } }, @@ -6230,12 +6445,12 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "escodegen": { "version": "1.14.3", @@ -7461,7 +7676,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "event-target-shim": { "version": "5.0.1", @@ -7489,7 +7704,7 @@ "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -7503,7 +7718,7 @@ "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -7534,7 +7749,7 @@ "exeq": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/exeq/-/exeq-2.4.0.tgz", - "integrity": "sha1-Td8qaEZIxCeteZNJzzO9dTWPiEo=", + "integrity": "sha512-B648qbDS00nQZv9UQGLT5RbZm/5dNBX10F8oWeXcgpFHSLm1249u95t/3sn2wXdQjLhlF+edAECdshFtSr1K0Q==", "requires": { "bluebird": "^3.0.3", "native-or-bluebird": "^1.2.0" @@ -7543,7 +7758,7 @@ "exif": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/exif/-/exif-0.6.0.tgz", - "integrity": "sha1-YKYmaAdlQst+T1cZnUrG830sX0o=", + "integrity": "sha512-gEwM4uanNMfLnDNKclZ7jPEA99E3rpy4ntoS6QW8u6murZjl1o8qRaPdMoC46Syg3d9/QaET0bYKhWlTwJCPgg==", "requires": { "debug": "^2.2" }, @@ -7559,7 +7774,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -7571,7 +7786,7 @@ "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -7593,7 +7808,7 @@ "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "requires": { "is-descriptor": "^0.1.0" } @@ -7601,7 +7816,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "requires": { "is-extendable": "^0.1.0" } @@ -7609,7 +7824,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -7692,7 +7907,7 @@ "express-flash": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/express-flash/-/express-flash-0.0.2.tgz", - "integrity": "sha1-I9GovPP5DXB5KOSJ+Whp7K0KzaI=", + "integrity": "sha512-QVUR0ZZRCaa8+iPHoUQaQJrQWcQuK/Q+19M7IUIdIEtvwhrA/ifHT7y1CVJI41YfGiOQnbGtn3uvd2vOdgu58A==", "requires": { "connect-flash": "0.1.x" } @@ -7749,7 +7964,7 @@ "expressjs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/expressjs/-/expressjs-1.0.1.tgz", - "integrity": "sha1-IgMoRpoY31rWFeK3oM6ZXxf7ru8=" + "integrity": "sha512-eFnQ5bMJxTZ29XwRJPV8ee/OURBBMS6Fm+b5rvMMEyz6u2IxPEh2SRzMZt9WvgnV+SMLmnzkALE1DnGG1HxJCw==" }, "ext": { "version": "1.6.0", @@ -7776,7 +7991,7 @@ "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -7832,7 +8047,7 @@ "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "requires": { "is-descriptor": "^1.0.0" } @@ -7840,7 +8055,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "requires": { "is-extendable": "^0.1.0" } @@ -7910,7 +8125,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "fast-deep-equal": { "version": "3.1.3", @@ -7931,7 +8146,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "fast-text-encoding": { @@ -7970,7 +8185,7 @@ "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "requires": { "pend": "~1.2.0" } @@ -7978,7 +8193,7 @@ "ffmpeg": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/ffmpeg/-/ffmpeg-0.0.4.tgz", - "integrity": "sha1-HEYN+OfaUSf2LO70v6BsWciWMMs=", + "integrity": "sha512-3TgWUJJlZGQn+crJFyhsO/oNeRRnGTy6GhgS98oUCIfZrOW5haPPV7DUfOm3xJcHr5q3TJpjk2GudPutrNisRA==", "requires": { "when": ">= 0.0.1" } @@ -8025,7 +8240,7 @@ "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -8036,7 +8251,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "requires": { "is-extendable": "^0.1.0" } @@ -8046,7 +8261,7 @@ "filter-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=" + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==" }, "finalhandler": { "version": "1.2.0", @@ -8080,7 +8295,7 @@ "find": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/find/-/find-0.1.7.tgz", - "integrity": "sha1-yGyHrxqxjyIrvjjeyGy8dg0Wpvs=", + "integrity": "sha512-jPrupTOe/pO//3a9Ty2o4NqQCp0L46UG+swUnfFtdmtQVN8pEltKpAqR7Nuf6vWn0GBXx5w+R1MyZzqwjEIqdA==", "requires": { "traverse-chain": "~0.1.0" } @@ -8088,7 +8303,7 @@ "find-cache-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "integrity": "sha512-46TFiBOzX7xq/PcSWfFwkyjpemdRnMe31UQF+os0y+1W3k95f6R4SEt02Hj4p3X0Mir9gfrkmOtshFidS0VPUg==", "dev": true, "requires": { "commondir": "^1.0.1", @@ -8229,7 +8444,7 @@ "fluent-ffmpeg": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", - "integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", "requires": { "async": ">=0.2.9", "which": "^1.1.1" @@ -8296,7 +8511,7 @@ "for-each-property": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/for-each-property/-/for-each-property-0.0.4.tgz", - "integrity": "sha1-z6hXrsFCLh0Sb/CHhPz2Jim8g/Y=", + "integrity": "sha512-xYs28PM0CKXETFzuGC6ZooH0voZlsSDZwidJcy92flQJi3PK7i3gZx23xHXCPOaD4zmet3bDo+wS7E7SujrlCw==", "requires": { "get-prototype-chain": "^1.0.1" } @@ -8304,7 +8519,7 @@ "for-each-property-deep": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/for-each-property-deep/-/for-each-property-deep-0.0.3.tgz", - "integrity": "sha1-MTCaSvw4qcygbxsiP1PWSm0IP60=", + "integrity": "sha512-qzP8QkODWVVRPpWiBZacSbBl67cTTWoBfxMG0wE46AsS1yl7qv05sGN+dHvD4s4tnvl/goe6Sp4qBI+rlVBgNg==", "requires": { "for-each-property": "0.0.4" } @@ -8312,12 +8527,12 @@ "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==" }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "fork-ts-checker-webpack-plugin": { "version": "1.6.0", @@ -8371,7 +8586,7 @@ "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", "requires": { "map-cache": "^0.2.2" } @@ -8379,12 +8594,12 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, "requires": { "inherits": "^2.0.1", @@ -8416,7 +8631,7 @@ "fs-extra": { "version": "0.26.7", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", + "integrity": "sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q==", "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^2.1.0", @@ -8451,7 +8666,7 @@ "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -8480,7 +8695,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { "version": "1.2.13", @@ -8661,7 +8876,7 @@ "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==" }, "get-intrinsic": { "version": "1.1.2", @@ -8681,12 +8896,12 @@ "get-prototype-chain": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-prototype-chain/-/get-prototype-chain-1.0.1.tgz", - "integrity": "sha1-oXGhFeoeSQbG7ThDofABwYUQQW8=" + "integrity": "sha512-2m7WZ0jveIg/dAbCbpUxEToaJ8Dmti5EkgDP8YM3UpHUT6SAORjE2odP8XQGNVGXMHi8q8cCCoy3HTByTaTVTw==" }, "get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" + "integrity": "sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==" }, "get-stream": { "version": "6.0.1", @@ -8705,12 +8920,12 @@ "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==" }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "requires": { "assert-plus": "^1.0.0" } @@ -8718,7 +8933,7 @@ "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "glob": { "version": "7.2.3", @@ -8736,7 +8951,7 @@ "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "requires": { "is-glob": "^3.1.0", "path-dirname": "^1.0.0" @@ -8745,7 +8960,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "requires": { "is-extglob": "^2.1.0" } @@ -8761,7 +8976,7 @@ "global-dirs": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", "requires": { "ini": "^1.3.4" } @@ -8774,7 +8989,7 @@ "globby": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "integrity": "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==", "dev": true, "requires": { "array-union": "^1.0.1", @@ -8788,7 +9003,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true } } @@ -8829,7 +9044,7 @@ "golden-layout": { "version": "1.5.9", "resolved": "https://registry.npmjs.org/golden-layout/-/golden-layout-1.5.9.tgz", - "integrity": "sha1-o5vB9qZ+b4hreXwBbdkk6UJrp38=", + "integrity": "sha512-iBXDQCXOTgUEQJo96zPbjDoy5bRIk9XW5l+q+pDgLnIyReqaa1aiQctNud4epsskyLt952BG521dew5Z1liSxA==", "requires": { "jquery": "*" } @@ -8837,7 +9052,7 @@ "good-listener": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", "requires": { "delegate": "^3.1.2" } @@ -9004,7 +9219,7 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" }, "har-validator": { "version": "5.1.5", @@ -9026,7 +9241,7 @@ "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "requires": { "ansi-regex": "^2.0.0" }, @@ -9034,7 +9249,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" } } }, @@ -9054,19 +9269,19 @@ "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" } } }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==" + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { "version": "1.0.0", @@ -9092,12 +9307,12 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -9107,7 +9322,7 @@ "has-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -9121,13 +9336,105 @@ "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", "requires": { "is-buffer": "^1.1.5" } } } }, + "hast-to-hyperscript": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz", + "integrity": "sha512-dhIVGoKCQVewFi+vz3Vt567E4ejMppS1haBRL6TEmeLeJVB1i/FJIIg/e6s1Bwn0g5qtYojHEKvyGA+OZuyifw==", + "requires": { + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.3.0", + "unist-util-is": "^5.0.0", + "web-namespaces": "^2.0.0" + } + }, + "hast-util-from-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz", + "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==", + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + } + }, + "hast-util-parse-selector": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz", + "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==", + "requires": { + "@types/hast": "^2.0.0" + } + }, + "hast-util-raw": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.2.tgz", + "integrity": "sha512-0x3BhhdlBcqRIKyc095lBSDvmQNMY3Eulj2PLsT5XCyKYrxssI5yr3P4Kv/PBo1s/DMkZy2voGkMXECnFCZRLQ==", + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "dependencies": { + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + } + } + }, + "hast-util-to-parse5": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.0.0.tgz", + "integrity": "sha512-YHiS6aTaZ3N0Q3nxaY/Tj98D6kM8QX5Q8xqgg8G45zR7PvWnPGPP0vcKCgb/moIydEJ/QWczVrX0JODCVeoV7A==", + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-to-hyperscript": "^10.0.0", + "property-information": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + } + }, + "hast-util-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", + "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==" + }, + "hastscript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.0.2.tgz", + "integrity": "sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==", + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -9154,7 +9461,7 @@ "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, "requires": { "inherits": "^2.0.1", @@ -9183,7 +9490,7 @@ "html": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/html/-/html-1.0.0.tgz", - "integrity": "sha1-pUT6nqVJK/s6LMqCEKEL57WvH2E=", + "integrity": "sha512-lw/7YsdKiP3kk5PnR1INY17iJuzdAtJewxr14ozKJWbbR97znovZ0mh+WEMZ8rjc3lgTK+ID/htTjuyGKB52Kw==", "requires": { "concat-stream": "^1.4.7" } @@ -9233,6 +9540,11 @@ "minimist": "^1.2.0" } }, + "html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==" + }, "html-webpack-plugin": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", @@ -9261,7 +9573,7 @@ "http-browserify": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz", - "integrity": "sha1-M3la3nLfiKz7/TZ3PO/tp2RzWyA=", + "integrity": "sha512-Irf/LJXmE3cBzU1eaR4+NEX6bmVLqt1wkmDiA7kBwH7zmb0D8kBAXsDmQ88hhj/qv9iEZKlyGx/hrMcFi8sOHw==", "requires": { "Base64": "~0.2.0", "inherits": "~2.0.1" @@ -9275,7 +9587,7 @@ "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", "dev": true }, "http-errors": { @@ -9322,7 +9634,7 @@ "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -9341,12 +9653,12 @@ "https": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=" + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, "https-proxy-agent": { "version": "5.0.1", @@ -9398,7 +9710,7 @@ "icss-replace-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", "dev": true }, "icss-utils": { @@ -9418,7 +9730,7 @@ "iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", "dev": true }, "ignore": { @@ -9430,7 +9742,7 @@ "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, "iink-js": { "version": "1.5.4", @@ -9474,7 +9786,7 @@ "image-size-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz", - "integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=", + "integrity": "sha512-N505B5FSy2Xf5l/Haef+99TwfJqTu40hnU560+rC0Cm6cxtwVz2yRFh9WpOk1YEjfv3dI0PgVYAH0hmXQmjDcw==", "requires": { "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1", "readable-stream": "^1.0.33", @@ -9488,12 +9800,12 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" }, "readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -9511,7 +9823,7 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "import-fresh": { "version": "3.3.0", @@ -9525,7 +9837,7 @@ "import-lazy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==" }, "import-local": { "version": "3.1.0", @@ -9539,7 +9851,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" }, "in-publish": { "version": "2.0.1", @@ -9549,7 +9861,7 @@ "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "integrity": "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==", "requires": { "repeating": "^2.0.0" } @@ -9557,12 +9869,12 @@ "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==" + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -9579,7 +9891,7 @@ "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==" } } }, @@ -9593,10 +9905,15 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "inline-style-prefixer": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz", - "integrity": "sha1-hVG45bTVcyROZqNLBPfTIHaitTQ=", + "integrity": "sha512-ne8XIyyqkRaNJ1JfL1NYzNdCNxq+MCBQhC8NgOQlzNm2vv3XxlP0VSLQUbSRCF6KPEoveCVEpayHoHzcMyZsMQ==", "requires": { "bowser": "^1.7.3", "css-in-js-utils": "^2.0.0" @@ -9715,7 +10032,7 @@ "inspect-function": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha1-hdoMUli8TDMK4yg7Z0fgdZ2QpjU=", + "integrity": "sha512-becs5gzcHwPrlHawscYkyQ/ShiOiosrXPhA5RVZ3qyWH4aWdD52RnMfXq/dwQXciHwiieD8aIPwdIWYv6eL+sQ==", "requires": { "split-skip": "0.0.1", "unpack-string": "0.0.2" @@ -9724,7 +10041,7 @@ "split-skip": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha1-gK2ONumOV2RUzDtmfB3SXYZejwA=" + "integrity": "sha512-7dkvq+gofI4M8zx4iZnEZ3O1s7FP4Y/iaIDHJh5RyWrs8idcPauFi2OZe3TBi36fLvR2j5z3kSzVtz6IhPdncQ==" } } }, @@ -9747,7 +10064,7 @@ "magicli": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", + "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", "requires": { "commander": "^2.9.0", "get-stdin": "^5.0.1", @@ -9798,7 +10115,7 @@ "inspect-function": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha1-hdoMUli8TDMK4yg7Z0fgdZ2QpjU=", + "integrity": "sha512-becs5gzcHwPrlHawscYkyQ/ShiOiosrXPhA5RVZ3qyWH4aWdD52RnMfXq/dwQXciHwiieD8aIPwdIWYv6eL+sQ==", "requires": { "split-skip": "0.0.1", "unpack-string": "0.0.2" @@ -9807,14 +10124,14 @@ "split-skip": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha1-gK2ONumOV2RUzDtmfB3SXYZejwA=" + "integrity": "sha512-7dkvq+gofI4M8zx4iZnEZ3O1s7FP4Y/iaIDHJh5RyWrs8idcPauFi2OZe3TBi36fLvR2j5z3kSzVtz6IhPdncQ==" } } }, "magicli": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", + "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", "requires": { "commander": "^2.9.0", "get-stdin": "^5.0.1", @@ -9825,14 +10142,14 @@ "split-skip": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.2.tgz", - "integrity": "sha1-2J2Iu9L3Pka1FYqjcKVhIk6A1GE=" + "integrity": "sha512-weHOi8BolsDnGIwhhWHbA+wKSuSpvWwjRrdj8SdbIIis2vSwOE37CQP8x3EleuzxanUr3AK8BdUy4MkiOULPZg==" } } }, "split-skip": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha1-gK2ONumOV2RUzDtmfB3SXYZejwA=" + "integrity": "sha512-7dkvq+gofI4M8zx4iZnEZ3O1s7FP4Y/iaIDHJh5RyWrs8idcPauFi2OZe3TBi36fLvR2j5z3kSzVtz6IhPdncQ==" } } }, @@ -9903,7 +10220,7 @@ "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", "dev": true }, "ipaddr.js": { @@ -9920,7 +10237,7 @@ "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", "requires": { "kind-of": "^3.0.2" }, @@ -9933,7 +10250,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -9952,7 +10269,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "is-bigint": { "version": "1.0.4", @@ -9965,7 +10282,7 @@ "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", "requires": { "binary-extensions": "^1.0.0" } @@ -10008,7 +10325,7 @@ "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", "requires": { "kind-of": "^3.0.2" }, @@ -10021,7 +10338,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -10056,12 +10373,12 @@ "is-directory": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==" }, "is-expression": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", - "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "integrity": "sha512-vyMeQMq+AiH5uUnoBfMTwf18tO3bM6k1QXBE9D6ueAAquEfCZe3AJPtud9g6qS0+4X8xA7ndpZiDyeb2l2qOBw==", "requires": { "acorn": "~4.0.2", "object-assign": "^4.0.1" @@ -10070,7 +10387,7 @@ "acorn": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + "integrity": "sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==" } } }, @@ -10082,7 +10399,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "is-finite": { "version": "1.1.0", @@ -10092,7 +10409,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" }, "is-generator-function": { "version": "1.0.10", @@ -10113,12 +10430,12 @@ "is-in-browser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", - "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" }, "is-installed-globally": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "integrity": "sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==", "requires": { "global-dirs": "^0.1.0", "is-path-inside": "^1.0.0" @@ -10132,12 +10449,12 @@ "is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + "integrity": "sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==" }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "requires": { "kind-of": "^3.0.2" }, @@ -10150,7 +10467,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -10168,7 +10485,7 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==" }, "is-path-cwd": { "version": "2.2.0", @@ -10199,11 +10516,16 @@ "is-path-inside": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==", "requires": { "path-is-inside": "^1.0.1" } }, + "is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -10220,7 +10542,7 @@ "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==" }, "is-regex": { "version": "1.1.4", @@ -10255,7 +10577,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" }, "is-string": { "version": "1.0.7", @@ -10288,7 +10610,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "is-url": { "version": "1.2.4", @@ -10298,7 +10620,7 @@ "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" }, "is-weakref": { "version": "1.0.2", @@ -10321,28 +10643,28 @@ "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", "dev": true }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" }, "isomorphic-fetch": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "integrity": "sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA==", "requires": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" @@ -10351,7 +10673,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "its-set": { "version": "1.2.3", @@ -10408,7 +10730,7 @@ "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" }, "js-tokens": { "version": "4.0.0", @@ -10427,7 +10749,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "jsdom": { "version": "15.2.1", @@ -10547,7 +10869,7 @@ "json-css": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/json-css/-/json-css-1.5.6.tgz", - "integrity": "sha1-65ZPg0ouTqobwvaY/12wB6JsfAA=" + "integrity": "sha512-B/0T0OxZH9tSb93tXV6VOYtXqrPz/Vgz2QrCT/4NXen8HGElYkYr9V+8IrSVTMj/ftxa8cG1kcu7f3iAMlaFlQ==" }, "json-parse-better-errors": { "version": "1.0.2", @@ -10578,7 +10900,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "json5": { "version": "1.0.1", @@ -10600,7 +10922,7 @@ "jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", "requires": { "graceful-fs": "^4.1.6" } @@ -10709,7 +11031,7 @@ "jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", "requires": { "is-promise": "^2.0.0", "promise": "^7.0.1" @@ -10831,11 +11153,16 @@ "klaw": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", "requires": { "graceful-fs": "^4.1.9" } }, + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + }, "language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -10854,7 +11181,7 @@ "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "integrity": "sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==", "requires": { "package-json": "^4.0.0" } @@ -10862,7 +11189,7 @@ "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==" }, "lazystream": { "version": "1.0.1", @@ -10891,7 +11218,7 @@ "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, "requires": { "prelude-ls": "~1.1.2", @@ -10914,7 +11241,7 @@ "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -10926,7 +11253,7 @@ "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", "requires": { "error-ex": "^1.2.0" } @@ -10971,22 +11298,22 @@ "lodash._getnative": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==" }, "lodash.chunk": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", - "integrity": "sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw=" + "integrity": "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==" }, "lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", - "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=" + "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==" }, "lodash.debounce": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-3.1.1.tgz", - "integrity": "sha1-gSIRw3ipTMKdWqTjNGzwv846ffU=", + "integrity": "sha512-lcmJwMpdPAtChA4hfiwxTtgFeNAaow701wWUgVUqeD0XJF7vMXIN+bu/2FJSGxT0NUbZy9g9VFrlOFfPjl+0Ew==", "requires": { "lodash._getnative": "^3.0.0" } @@ -10994,42 +11321,42 @@ "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, "lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, "lodash.flow": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", - "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" + "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==" }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, "lodash.merge": { "version": "4.6.2", @@ -11039,23 +11366,23 @@ "lodash.padend": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" + "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==" }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "dev": true }, "lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" }, "log-symbols": { "version": "2.2.0", @@ -11083,7 +11410,12 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==" + }, + "longest-streak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", + "integrity": "sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==" }, "loose-envify": { "version": "1.4.0", @@ -11096,7 +11428,7 @@ "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", "requires": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.0" @@ -11145,7 +11477,7 @@ "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "requires": { "locate-path": "^2.0.0" } @@ -11153,7 +11485,7 @@ "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "requires": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" @@ -11170,7 +11502,7 @@ "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "requires": { "p-limit": "^1.1.0" } @@ -11178,7 +11510,7 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==" } } }, @@ -11206,21 +11538,26 @@ "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==" }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==" }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", "requires": { "object-visit": "^1.0.0" } }, + "markdown-table": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.2.tgz", + "integrity": "sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==" + }, "material-colors": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", @@ -11261,7 +11598,7 @@ "math-codegen": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/math-codegen/-/math-codegen-0.3.5.tgz", - "integrity": "sha1-R5nuRnfe0Ud2bQA8ykt4ee3UDMo=", + "integrity": "sha512-SsFYMv33FxMKYxI1PBiaZT+8AeDITK+k/PKhbHNlOPHIz5FIPF4wy78yWqanN6luXdsXENUZgCIC6xH6bfUq1g==", "requires": { "extend": "^3.0.0", "mr-parser": "^0.2.1" @@ -11270,7 +11607,7 @@ "mathquill": { "version": "0.10.1-a", "resolved": "https://registry.npmjs.org/mathquill/-/mathquill-0.10.1-a.tgz", - "integrity": "sha512-snSAEwAtwdwBFSor+nVBnWWQtTw67kgAgKMyAIxuz4ZPboy0qkWZmd7BL3lfOXp/INihhRlU1PcfaAtDaRhmzA==", + "integrity": "sha1-vyylaQEAY6w0vNXVKa3Ag3zVPD8=", "requires": { "jquery": "^1.12.3" }, @@ -11278,19 +11615,170 @@ "jquery": { "version": "1.12.4", "resolved": "https://registry.npmjs.org/jquery/-/jquery-1.12.4.tgz", - "integrity": "sha512-UEVp7PPK9xXYSk8xqXCJrkXnKZtlgWkd2GsAQbMRFK6S/ePU2JN5G2Zum8hIVjzR3CpdfSqdqAzId/xd4TJHeg==" + "integrity": "sha1-AeHfuikP5z3rp3zurLD5ui/sngw=" } } }, "max-safe-integer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/max-safe-integer/-/max-safe-integer-1.0.1.tgz", - "integrity": "sha1-84BgvixWPYwC5tSK85Ei/YO29BA=" + "integrity": "sha512-CHZ/Nopqh46UtA0YvLclxj9F95qExLmTnMS5fnYlXvX4zj1RUOC3cPaGEOMhdPE8SSudSQ6wkQUJLA5E/WeL4A==" + }, + "mdast-util-definitions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz", + "integrity": "sha512-rQ+Gv7mHttxHOBx2dkF4HWTg+EE+UR78ptQWDylzPKaQuVGdG4HIoY3SrS/pCp80nZ04greFvXbVFHT+uf0JVQ==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "mdast-util-find-and-replace": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.1.tgz", + "integrity": "sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw==", + "requires": { + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==" + } + } + }, + "mdast-util-from-markdown": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz", + "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + } + }, + "mdast-util-gfm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.1.tgz", + "integrity": "sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ==", + "requires": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + } + }, + "mdast-util-gfm-autolink-literal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.2.tgz", + "integrity": "sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==", + "requires": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + } + }, + "mdast-util-gfm-footnote": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.1.tgz", + "integrity": "sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + } + }, + "mdast-util-gfm-strikethrough": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.1.tgz", + "integrity": "sha512-zKJbEPe+JP6EUv0mZ0tQUyLQOC+FADt0bARldONot/nefuISkaZFlmVK4tU6JgfyZGrky02m/I6PmehgAgZgqg==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + } + }, + "mdast-util-gfm-table": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.4.tgz", + "integrity": "sha512-aEuoPwZyP4iIMkf2cLWXxx3EQ6Bmh2yKy9MVCg4i6Sd3cX80dcLEfXO/V4ul3pGH9czBK4kp+FAl+ZHmSUt9/w==", + "requires": { + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + } + }, + "mdast-util-gfm-task-list-item": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.1.tgz", + "integrity": "sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + } + }, + "mdast-util-to-hast": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.2.0.tgz", + "integrity": "sha512-YDwT5KhGzLgPpSnQhAlK1+WpCW4gsPmNNAxUNMkMTDhxQyPp2eX86WOelnKnLKEvSpfxqJbPbInHFkefXZBhEA==", + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/mdurl": "^1.0.0", + "mdast-util-definitions": "^5.0.0", + "mdurl": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "trim-lines": "^3.0.0", + "unist-builder": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "mdast-util-to-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz", + "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + } + }, + "mdast-util-to-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", + "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==" + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "memfs": { "version": "3.4.4", @@ -11341,12 +11829,12 @@ "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=" + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==" }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", "requires": { "camelcase-keys": "^2.0.0", "decamelize": "^1.1.2", @@ -11371,7 +11859,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "merge-stream": { "version": "2.0.0", @@ -11382,7 +11870,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "microevent.ts": { "version": "0.1.1", @@ -11390,6 +11878,320 @@ "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", "dev": true }, + "micromark": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz", + "integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "micromark-core-commonmark": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", + "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz", + "integrity": "sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==", + "requires": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-extension-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-footnote": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.0.4.tgz", + "integrity": "sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg==", + "requires": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-strikethrough": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.4.tgz", + "integrity": "sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-table": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz", + "integrity": "sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-tagfilter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.1.tgz", + "integrity": "sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA==", + "requires": { + "micromark-util-types": "^1.0.0" + } + }, + "micromark-extension-gfm-task-list-item": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.3.tgz", + "integrity": "sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-destination": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", + "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-label": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", + "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-space": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", + "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-title": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", + "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-whitespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", + "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", + "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "requires": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-chunked": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", + "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-classify-character": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", + "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", + "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", + "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-decode-string": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", + "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-encode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", + "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==" + }, + "micromark-util-html-tag-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", + "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==" + }, + "micromark-util-normalize-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", + "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", + "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "requires": { + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz", + "integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", + "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-util-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", + "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==" + }, + "micromark-util-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", + "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==" + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -11617,7 +12419,7 @@ "he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==", "dev": true }, "minimatch": { @@ -11632,13 +12434,13 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", "dev": true }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", "dev": true, "requires": { "minimist": "0.0.8" @@ -11647,7 +12449,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "supports-color": { @@ -11771,7 +12573,7 @@ "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", "dev": true, "requires": { "aproba": "^1.1.1", @@ -11832,14 +12634,19 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, "mr-parser": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/mr-parser/-/mr-parser-0.2.1.tgz", - "integrity": "sha1-hhi5ukF+KOn0OaQcaVtVTq/u2Sc=" + "integrity": "sha512-hug+mpbSSKnH13rFqy3zm+XiG+QTStiDAgMTHK355TIstQE0qBkBtSJsa5YHP94AuarVX9b/4dcebdTRZ9YiEw==" + }, + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" }, "ms": { "version": "2.1.1", @@ -11859,7 +12666,7 @@ "multicast-dns-service-types": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", "dev": true }, "mute-stream": { @@ -11904,7 +12711,7 @@ "native-or-bluebird": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/native-or-bluebird/-/native-or-bluebird-1.2.0.tgz", - "integrity": "sha1-OcR7/Xgl0fuf+tMiEK4l2q3xAck=" + "integrity": "sha512-0SH8UubxDfe382eYiwmd12qxAbiWGzlGZv6CkMA+DPojWa/Y0oH4hE0lRtFfFgJmPQFyKXeB8XxPbZz6TvvKaQ==" }, "natural-compare": { "version": "1.4.0", @@ -11931,7 +12738,7 @@ "nextafter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nextafter/-/nextafter-1.0.0.tgz", - "integrity": "sha1-t9d7U1MQ4+CX5gJauwqQNHfsGjo=", + "integrity": "sha512-7PO+A89Tll2rSEfyrjtqO0MaI37+nnxBdnQcPypfbEYYuGaJxWGCqaOwQX4a3GHNTS08l1kazuiLEWZniZjMUQ==", "requires": { "double-bits": "^1.1.0" } @@ -11962,7 +12769,7 @@ "node-ensure": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", - "integrity": "sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=" + "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==" }, "node-environment-flags": { "version": "1.0.5", @@ -12051,7 +12858,7 @@ "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", "requires": { "abbrev": "1" } @@ -12092,7 +12899,7 @@ "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + "integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==" }, "string-width": { "version": "1.0.2", @@ -12156,12 +12963,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" }, "aproba": { "version": "1.2.0", @@ -12180,7 +12987,7 @@ "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -12207,7 +13014,7 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -12305,7 +13112,7 @@ "noop-logger": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==" }, "nopt": { "version": "5.0.0", @@ -12481,12 +13288,12 @@ }, "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "agent-base": { "version": "4.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", "requires": { "es6-promisify": "^5.0.0" @@ -12494,7 +13301,7 @@ }, "agentkeepalive": { "version": "3.5.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", "requires": { "humanize-ms": "^1.2.1" @@ -12502,7 +13309,7 @@ }, "ansi-align": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", "requires": { "string-width": "^2.0.0" @@ -12510,12 +13317,12 @@ }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { "color-convert": "^1.9.0" @@ -12523,27 +13330,27 @@ }, "ansicolors": { "version": "0.3.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" }, "ansistyles": { "version": "0.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansistyles/-/ansistyles-0.1.3.tgz", "integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=" }, "aproba": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "archy": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" }, "are-we-there-yet": { "version": "1.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "requires": { "delegates": "^1.0.0", @@ -12552,7 +13359,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -12566,7 +13373,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -12576,12 +13383,12 @@ }, "asap": { "version": "2.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1": { "version": "0.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "requires": { "safer-buffer": "~2.1.0" @@ -12589,32 +13396,32 @@ }, "assert-plus": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "asynckit": { "version": "0.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sign2": { "version": "0.7.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.8.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "bcrypt-pbkdf": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "optional": true, "requires": { @@ -12623,7 +13430,7 @@ }, "bin-links": { "version": "1.1.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-1.1.8.tgz", "integrity": "sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==", "requires": { "bluebird": "^3.5.3", @@ -12636,12 +13443,12 @@ }, "bluebird": { "version": "3.5.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" }, "boxen": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", "requires": { "ansi-align": "^2.0.0", @@ -12655,7 +13462,7 @@ }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", @@ -12664,27 +13471,27 @@ }, "buffer-from": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" }, "builtins": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" }, "byline": { "version": "5.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" }, "byte-size": { "version": "5.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-5.0.1.tgz", "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==" }, "cacache": { "version": "12.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", "requires": { "bluebird": "^3.5.5", @@ -12706,27 +13513,27 @@ }, "call-limit": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/call-limit/-/call-limit-1.1.1.tgz", "integrity": "sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==" }, "camelcase": { "version": "4.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" }, "capture-stack-trace": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" }, "caseless": { "version": "0.12.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { "version": "2.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { "ansi-styles": "^3.2.1", @@ -12736,17 +13543,17 @@ }, "chownr": { "version": "1.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "ci-info": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" }, "cidr-regex": { "version": "2.0.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-2.0.10.tgz", "integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==", "requires": { "ip-regex": "^2.1.0" @@ -12754,12 +13561,12 @@ }, "cli-boxes": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" }, "cli-columns": { "version": "3.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/cli-columns/-/cli-columns-3.1.2.tgz", "integrity": "sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=", "requires": { "string-width": "^2.0.0", @@ -12768,7 +13575,7 @@ }, "cli-table3": { "version": "0.5.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", "requires": { "colors": "^1.1.2", @@ -12778,7 +13585,7 @@ }, "cliui": { "version": "5.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "requires": { "string-width": "^3.1.0", @@ -12788,17 +13595,17 @@ "dependencies": { "ansi-regex": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { "emoji-regex": "^7.0.1", @@ -12808,7 +13615,7 @@ }, "strip-ansi": { "version": "5.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" @@ -12818,12 +13625,12 @@ }, "clone": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" }, "cmd-shim": { "version": "3.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-3.0.3.tgz", "integrity": "sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==", "requires": { "graceful-fs": "^4.1.2", @@ -12832,12 +13639,12 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "color-convert": { "version": "1.9.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", "requires": { "color-name": "^1.1.1" @@ -12845,18 +13652,18 @@ }, "color-name": { "version": "1.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colors": { "version": "1.3.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", "optional": true }, "columnify": { "version": "1.5.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", "requires": { "strip-ansi": "^3.0.0", @@ -12865,7 +13672,7 @@ }, "combined-stream": { "version": "1.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { "delayed-stream": "~1.0.0" @@ -12873,12 +13680,12 @@ }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "requires": { "buffer-from": "^1.0.0", @@ -12889,7 +13696,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -12903,7 +13710,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -12913,7 +13720,7 @@ }, "config-chain": { "version": "1.1.12", - "resolved": false, + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "requires": { "ini": "^1.3.4", @@ -12922,7 +13729,7 @@ }, "configstore": { "version": "3.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", "requires": { "dot-prop": "^4.2.1", @@ -12935,12 +13742,12 @@ }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "copy-concurrently": { "version": "1.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", "requires": { "aproba": "^1.1.1", @@ -12953,24 +13760,24 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "iferr": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" } } }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-error-class": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", "requires": { "capture-stack-trace": "^1.0.0" @@ -12978,7 +13785,7 @@ }, "cross-spawn": { "version": "5.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "requires": { "lru-cache": "^4.0.1", @@ -12988,7 +13795,7 @@ "dependencies": { "lru-cache": { "version": "4.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "requires": { "pseudomap": "^1.0.2", @@ -12997,24 +13804,24 @@ }, "yallist": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" } } }, "crypto-random-string": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" }, "cyclist": { "version": "0.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" }, "dashdash": { "version": "1.14.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { "assert-plus": "^1.0.0" @@ -13022,7 +13829,7 @@ }, "debug": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { "ms": "2.0.0" @@ -13030,34 +13837,34 @@ "dependencies": { "ms": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, "debuglog": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" }, "decamelize": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "defaults": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", "requires": { "clone": "^1.0.2" @@ -13065,7 +13872,7 @@ }, "define-properties": { "version": "1.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "requires": { "object-keys": "^1.0.12" @@ -13073,27 +13880,27 @@ }, "delayed-stream": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "detect-indent": { "version": "5.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" }, "detect-newline": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" }, "dezalgo": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", "requires": { "asap": "^2.0.0", @@ -13102,7 +13909,7 @@ }, "dot-prop": { "version": "4.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", "requires": { "is-obj": "^1.0.0" @@ -13110,17 +13917,17 @@ }, "dotenv": { "version": "5.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" }, "duplexer3": { "version": "0.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, "duplexify": { "version": "3.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", "requires": { "end-of-stream": "^1.0.0", @@ -13131,7 +13938,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -13145,7 +13952,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -13155,7 +13962,7 @@ }, "ecc-jsbn": { "version": "0.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "optional": true, "requires": { @@ -13165,17 +13972,17 @@ }, "editor": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/editor/-/editor-1.0.0.tgz", "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=" }, "emoji-regex": { "version": "7.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "encoding": { "version": "0.1.12", - "resolved": false, + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", "requires": { "iconv-lite": "~0.4.13" @@ -13183,7 +13990,7 @@ }, "end-of-stream": { "version": "1.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { "once": "^1.4.0" @@ -13191,17 +13998,17 @@ }, "env-paths": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" }, "err-code": { "version": "1.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" }, "errno": { "version": "0.1.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", "requires": { "prr": "~1.0.1" @@ -13209,7 +14016,7 @@ }, "es-abstract": { "version": "1.12.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", "requires": { "es-to-primitive": "^1.1.1", @@ -13221,7 +14028,7 @@ }, "es-to-primitive": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", "requires": { "is-callable": "^1.1.4", @@ -13231,12 +14038,12 @@ }, "es6-promise": { "version": "4.2.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "es6-promisify": { "version": "5.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "requires": { "es6-promise": "^4.0.3" @@ -13244,12 +14051,12 @@ }, "escape-string-regexp": { "version": "1.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "execa": { "version": "0.7.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "requires": { "cross-spawn": "^5.0.1", @@ -13263,39 +14070,39 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "resolved": false, + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" } } }, "extend": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extsprintf": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-json-stable-stringify": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "figgy-pudding": { "version": "3.5.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" }, "find-npm-prefix": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz", "integrity": "sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==" }, "flush-write-stream": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", "requires": { "inherits": "^2.0.1", @@ -13304,7 +14111,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -13318,7 +14125,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -13328,12 +14135,12 @@ }, "forever-agent": { "version": "0.6.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { "asynckit": "^0.4.0", @@ -13343,7 +14150,7 @@ }, "from2": { "version": "2.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", "requires": { "inherits": "^2.0.1", @@ -13352,7 +14159,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -13366,7 +14173,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -13376,7 +14183,7 @@ }, "fs-minipass": { "version": "1.2.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "requires": { "minipass": "^2.6.0" @@ -13384,7 +14191,7 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "requires": { "safe-buffer": "^5.1.2", @@ -13395,7 +14202,7 @@ }, "fs-vacuum": { "version": "1.2.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs-vacuum/-/fs-vacuum-1.2.10.tgz", "integrity": "sha1-t2Kb7AekAxolSP35n17PHMizHjY=", "requires": { "graceful-fs": "^4.1.2", @@ -13405,7 +14212,7 @@ }, "fs-write-stream-atomic": { "version": "1.0.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", "requires": { "graceful-fs": "^4.1.2", @@ -13416,12 +14223,12 @@ "dependencies": { "iferr": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" }, "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -13435,7 +14242,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -13445,17 +14252,17 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "function-bind": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { "aproba": "^1.0.3", @@ -13470,12 +14277,12 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", @@ -13487,12 +14294,12 @@ }, "genfun": { "version": "5.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz", "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==" }, "gentle-fs": { "version": "2.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/gentle-fs/-/gentle-fs-2.3.1.tgz", "integrity": "sha512-OlwBBwqCFPcjm33rF2BjW+Pr6/ll2741l+xooiwTCeaX2CA1ZuclavyMBe0/KlR21/XGsgY6hzEQZ15BdNa13Q==", "requires": { "aproba": "^1.1.2", @@ -13510,24 +14317,24 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "iferr": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" } } }, "get-caller-file": { "version": "2.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-stream": { "version": "4.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "requires": { "pump": "^3.0.0" @@ -13535,7 +14342,7 @@ }, "getpass": { "version": "0.1.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { "assert-plus": "^1.0.0" @@ -13543,7 +14350,7 @@ }, "glob": { "version": "7.1.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", @@ -13556,7 +14363,7 @@ }, "global-dirs": { "version": "0.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", "requires": { "ini": "^1.3.4" @@ -13564,7 +14371,7 @@ }, "got": { "version": "6.7.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "requires": { "create-error-class": "^3.0.0", @@ -13582,24 +14389,24 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "resolved": false, + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" } } }, "graceful-fs": { "version": "4.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "har-schema": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "requires": { "ajv": "^6.12.3", @@ -13608,7 +14415,7 @@ "dependencies": { "ajv": { "version": "6.12.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { "fast-deep-equal": "^3.1.1", @@ -13619,19 +14426,19 @@ }, "fast-deep-equal": { "version": "3.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "json-schema-traverse": { "version": "0.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" } } }, "has": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "requires": { "function-bind": "^1.1.1" @@ -13639,32 +14446,32 @@ }, "has-flag": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "hosted-git-info": { "version": "2.8.9", - "resolved": false, + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "http-cache-semantics": { "version": "3.8.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" }, "http-proxy-agent": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", "requires": { "agent-base": "4", @@ -13673,7 +14480,7 @@ }, "http-signature": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { "assert-plus": "^1.0.0", @@ -13683,7 +14490,7 @@ }, "https-proxy-agent": { "version": "2.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", "requires": { "agent-base": "^4.3.0", @@ -13692,7 +14499,7 @@ }, "humanize-ms": { "version": "1.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", "requires": { "ms": "^2.0.0" @@ -13700,7 +14507,7 @@ }, "iconv-lite": { "version": "0.4.23", - "resolved": false, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -13708,12 +14515,12 @@ }, "iferr": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/iferr/-/iferr-1.0.2.tgz", "integrity": "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==" }, "ignore-walk": { "version": "3.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "requires": { "minimatch": "^3.0.4" @@ -13721,22 +14528,22 @@ }, "import-lazy": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" }, "imurmurhash": { "version": "0.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, "infer-owner": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", @@ -13745,17 +14552,17 @@ }, "inherits": { "version": "2.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "init-package-json": { "version": "1.10.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz", "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", "requires": { "glob": "^7.1.1", @@ -13770,22 +14577,22 @@ }, "ip": { "version": "1.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" }, "ip-regex": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" }, "is-callable": { "version": "1.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" }, "is-ci": { "version": "1.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "requires": { "ci-info": "^1.5.0" @@ -13793,14 +14600,14 @@ "dependencies": { "ci-info": { "version": "1.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" } } }, "is-cidr": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-3.0.0.tgz", "integrity": "sha512-8Xnnbjsb0x462VoYiGlhEi+drY8SFwrHiSYuzc/CEwco55vkehTaxAyIjEdpi3EMvLPPJAJi9FlzP+h+03gp0Q==", "requires": { "cidr-regex": "^2.0.10" @@ -13808,12 +14615,12 @@ }, "is-date-object": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" @@ -13821,7 +14628,7 @@ }, "is-installed-globally": { "version": "0.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", "requires": { "global-dirs": "^0.1.0", @@ -13830,17 +14637,17 @@ }, "is-npm": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" }, "is-obj": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, "is-path-inside": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", "requires": { "path-is-inside": "^1.0.1" @@ -13848,12 +14655,12 @@ }, "is-redirect": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" }, "is-regex": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "requires": { "has": "^1.0.1" @@ -13861,17 +14668,17 @@ }, "is-retry-allowed": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" }, "is-stream": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-symbol": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", "requires": { "has-symbols": "^1.0.0" @@ -13879,43 +14686,43 @@ }, "is-typedarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isstream": { "version": "0.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "jsbn": { "version": "0.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, "json-parse-better-errors": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-schema": { "version": "0.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "json-stringify-safe": { "version": "5.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonparse": { @@ -13925,7 +14732,7 @@ }, "jsprim": { "version": "1.4.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "requires": { "assert-plus": "1.0.0", @@ -13936,7 +14743,7 @@ }, "latest-version": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", "requires": { "package-json": "^4.0.0" @@ -13944,12 +14751,12 @@ }, "lazy-property": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lazy-property/-/lazy-property-1.0.0.tgz", "integrity": "sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=" }, "libcipm": { "version": "4.0.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/libcipm/-/libcipm-4.0.8.tgz", "integrity": "sha512-IN3hh2yDJQtZZ5paSV4fbvJg4aHxCCg5tcZID/dSVlTuUiWktsgaldVljJv6Z5OUlYspx6xQkbR0efNodnIrOA==", "requires": { "bin-links": "^1.1.2", @@ -13971,7 +14778,7 @@ }, "libnpm": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpm/-/libnpm-3.0.1.tgz", "integrity": "sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ==", "requires": { "bin-links": "^1.1.2", @@ -13998,7 +14805,7 @@ }, "libnpmaccess": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-3.0.2.tgz", "integrity": "sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ==", "requires": { "aproba": "^2.0.0", @@ -14009,7 +14816,7 @@ }, "libnpmconfig": { "version": "1.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpmconfig/-/libnpmconfig-1.2.1.tgz", "integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==", "requires": { "figgy-pudding": "^3.5.1", @@ -14019,7 +14826,7 @@ "dependencies": { "find-up": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { "locate-path": "^3.0.0" @@ -14027,7 +14834,7 @@ }, "locate-path": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { "p-locate": "^3.0.0", @@ -14036,7 +14843,7 @@ }, "p-limit": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "requires": { "p-try": "^2.0.0" @@ -14044,7 +14851,7 @@ }, "p-locate": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { "p-limit": "^2.0.0" @@ -14052,14 +14859,14 @@ }, "p-try": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" } } }, "libnpmhook": { "version": "5.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpmhook/-/libnpmhook-5.0.3.tgz", "integrity": "sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA==", "requires": { "aproba": "^2.0.0", @@ -14070,7 +14877,7 @@ }, "libnpmorg": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpmorg/-/libnpmorg-1.0.1.tgz", "integrity": "sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww==", "requires": { "aproba": "^2.0.0", @@ -14081,7 +14888,7 @@ }, "libnpmpublish": { "version": "1.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-1.1.2.tgz", "integrity": "sha512-2yIwaXrhTTcF7bkJKIKmaCV9wZOALf/gsTDxVSu/Gu/6wiG3fA8ce8YKstiWKTxSFNC0R7isPUb6tXTVFZHt2g==", "requires": { "aproba": "^2.0.0", @@ -14097,7 +14904,7 @@ }, "libnpmsearch": { "version": "2.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpmsearch/-/libnpmsearch-2.0.2.tgz", "integrity": "sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg==", "requires": { "figgy-pudding": "^3.5.1", @@ -14107,7 +14914,7 @@ }, "libnpmteam": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpmteam/-/libnpmteam-1.0.2.tgz", "integrity": "sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA==", "requires": { "aproba": "^2.0.0", @@ -14118,7 +14925,7 @@ }, "libnpx": { "version": "10.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/libnpx/-/libnpx-10.2.4.tgz", "integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==", "requires": { "dotenv": "^5.0.1", @@ -14133,7 +14940,7 @@ }, "lock-verify": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lock-verify/-/lock-verify-2.1.0.tgz", "integrity": "sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg==", "requires": { "npm-package-arg": "^6.1.0", @@ -14142,7 +14949,7 @@ }, "lockfile": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", "requires": { "signal-exit": "^3.0.2" @@ -14150,12 +14957,12 @@ }, "lodash._baseindexof": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz", "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=" }, "lodash._baseuniq": { "version": "4.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz", "integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=", "requires": { "lodash._createset": "~4.0.0", @@ -14164,17 +14971,17 @@ }, "lodash._bindcallback": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" }, "lodash._cacheindexof": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz", "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=" }, "lodash._createcache": { "version": "3.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._createcache/-/lodash._createcache-3.1.2.tgz", "integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=", "requires": { "lodash._getnative": "^3.0.0" @@ -14182,52 +14989,52 @@ }, "lodash._createset": { "version": "4.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz", "integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=" }, "lodash._getnative": { "version": "3.9.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" }, "lodash._root": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" }, "lodash.clonedeep": { "version": "4.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.restparam": { "version": "3.6.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" }, "lodash.union": { "version": "4.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" }, "lodash.uniq": { "version": "4.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, "lodash.without": { "version": "4.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz", "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=" }, "lowercase-keys": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, "lru-cache": { "version": "5.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "requires": { "yallist": "^3.0.2" @@ -14235,7 +15042,7 @@ }, "make-dir": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "requires": { "pify": "^3.0.0" @@ -14243,7 +15050,7 @@ }, "make-fetch-happen": { "version": "5.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz", "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", "requires": { "agentkeepalive": "^3.4.1", @@ -14261,17 +15068,17 @@ }, "meant": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/meant/-/meant-1.0.2.tgz", "integrity": "sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg==" }, "mime-db": { "version": "1.35.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" }, "mime-types": { "version": "2.1.19", - "resolved": false, + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", "requires": { "mime-db": "~1.35.0" @@ -14279,7 +15086,7 @@ }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" @@ -14287,12 +15094,12 @@ }, "minimist": { "version": "1.2.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minizlib": { "version": "1.3.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "requires": { "minipass": "^2.9.0" @@ -14300,7 +15107,7 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "requires": { "safe-buffer": "^5.1.2", @@ -14311,7 +15118,7 @@ }, "mississippi": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", "requires": { "concat-stream": "^1.5.0", @@ -14328,7 +15135,7 @@ }, "mkdirp": { "version": "0.5.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { "minimist": "^1.2.5" @@ -14336,14 +15143,14 @@ "dependencies": { "minimist": { "version": "1.2.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" } } }, "move-concurrently": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", "requires": { "aproba": "^1.1.1", @@ -14356,24 +15163,24 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" } } }, "ms": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "mute-stream": { "version": "0.0.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, "node-fetch-npm": { "version": "2.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", "requires": { "encoding": "^0.1.11", @@ -14383,7 +15190,7 @@ }, "node-gyp": { "version": "5.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz", "integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==", "requires": { "env-paths": "^2.2.0", @@ -14401,7 +15208,7 @@ }, "nopt": { "version": "4.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "requires": { "abbrev": "1", @@ -14410,7 +15217,7 @@ }, "normalize-package-data": { "version": "2.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "requires": { "hosted-git-info": "^2.1.4", @@ -14421,7 +15228,7 @@ "dependencies": { "resolve": { "version": "1.10.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", "requires": { "path-parse": "^1.0.6" @@ -14431,7 +15238,7 @@ }, "npm-audit-report": { "version": "1.3.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-audit-report/-/npm-audit-report-1.3.3.tgz", "integrity": "sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==", "requires": { "cli-table3": "^0.5.0", @@ -14440,7 +15247,7 @@ }, "npm-bundled": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", "requires": { "npm-normalize-package-bin": "^1.0.1" @@ -14448,12 +15255,12 @@ }, "npm-cache-filename": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz", "integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=" }, "npm-install-checks": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-3.0.2.tgz", "integrity": "sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==", "requires": { "semver": "^2.3.0 || 3.x || 4 || 5" @@ -14461,7 +15268,7 @@ }, "npm-lifecycle": { "version": "3.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz", "integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==", "requires": { "byline": "^5.0.0", @@ -14476,17 +15283,17 @@ }, "npm-logical-tree": { "version": "1.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz", "integrity": "sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==" }, "npm-normalize-package-bin": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-package-arg": { "version": "6.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", "requires": { "hosted-git-info": "^2.7.1", @@ -14497,7 +15304,7 @@ }, "npm-packlist": { "version": "1.4.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "requires": { "ignore-walk": "^3.0.1", @@ -14507,7 +15314,7 @@ }, "npm-pick-manifest": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz", "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", "requires": { "figgy-pudding": "^3.5.1", @@ -14517,7 +15324,7 @@ }, "npm-profile": { "version": "4.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-profile/-/npm-profile-4.0.4.tgz", "integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==", "requires": { "aproba": "^1.1.2 || 2", @@ -14527,7 +15334,7 @@ }, "npm-registry-fetch": { "version": "4.0.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz", "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==", "requires": { "JSONStream": "^1.3.4", @@ -14541,14 +15348,14 @@ "dependencies": { "safe-buffer": { "version": "5.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, "npm-run-path": { "version": "2.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "requires": { "path-key": "^2.0.0" @@ -14556,12 +15363,12 @@ }, "npm-user-validate": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-user-validate/-/npm-user-validate-1.0.1.tgz", "integrity": "sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw==" }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "~1.1.2", @@ -14572,27 +15379,27 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.9.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-keys": { "version": "1.0.12", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" }, "object.getownpropertydescriptors": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", "requires": { "define-properties": "^1.1.2", @@ -14601,7 +15408,7 @@ }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" @@ -14609,22 +15416,22 @@ }, "opener": { "version": "1.5.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { "os-homedir": "^1.0.0", @@ -14633,12 +15440,12 @@ }, "p-finally": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "package-json": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", "requires": { "got": "^6.7.1", @@ -14649,7 +15456,7 @@ }, "pacote": { "version": "9.5.12", - "resolved": false, + "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.5.12.tgz", "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==", "requires": { "bluebird": "^3.5.3", @@ -14686,7 +15493,7 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "requires": { "safe-buffer": "^5.1.2", @@ -14697,7 +15504,7 @@ }, "parallel-transform": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", "requires": { "cyclist": "~0.2.2", @@ -14707,7 +15514,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -14721,7 +15528,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -14731,57 +15538,57 @@ }, "path-exists": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" }, "path-key": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "path-parse": { "version": "1.0.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "performance-now": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, "prepend-http": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "promise-inflight": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" }, "promise-retry": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", "requires": { "err-code": "^1.0.0", @@ -14790,14 +15597,14 @@ "dependencies": { "retry": { "version": "0.10.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" } } }, "promzard": { "version": "0.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", "requires": { "read": "1" @@ -14805,12 +15612,12 @@ }, "proto-list": { "version": "1.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" }, "protoduck": { "version": "5.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", "requires": { "genfun": "^5.0.0" @@ -14818,22 +15625,22 @@ }, "prr": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "pseudomap": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { "version": "1.1.29", - "resolved": false, + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" }, "pump": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "requires": { "end-of-stream": "^1.1.0", @@ -14842,7 +15649,7 @@ }, "pumpify": { "version": "1.5.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "requires": { "duplexify": "^3.6.0", @@ -14852,7 +15659,7 @@ "dependencies": { "pump": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "requires": { "end-of-stream": "^1.1.0", @@ -14863,22 +15670,22 @@ }, "punycode": { "version": "1.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qrcode-terminal": { "version": "0.12.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==" }, "qs": { "version": "6.5.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "query-string": { "version": "6.8.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.8.2.tgz", "integrity": "sha512-J3Qi8XZJXh93t2FiKyd/7Ec6GNifsjKXUsVFkSBj/kjLsDylWhnCz4NT1bkPcKotttPW+QbKGqqPH8OoI2pdqw==", "requires": { "decode-uri-component": "^0.2.0", @@ -14888,12 +15695,12 @@ }, "qw": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/qw/-/qw-1.0.1.tgz", "integrity": "sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=" }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { "deep-extend": "^0.6.0", @@ -14904,7 +15711,7 @@ }, "read": { "version": "1.0.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", "requires": { "mute-stream": "~0.0.4" @@ -14912,7 +15719,7 @@ }, "read-cmd-shim": { "version": "1.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz", "integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==", "requires": { "graceful-fs": "^4.1.2" @@ -14920,7 +15727,7 @@ }, "read-installed": { "version": "4.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", "requires": { "debuglog": "^1.0.1", @@ -14934,7 +15741,7 @@ }, "read-package-json": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.1.tgz", "integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==", "requires": { "glob": "^7.1.1", @@ -14946,7 +15753,7 @@ }, "read-package-tree": { "version": "5.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz", "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", "requires": { "read-package-json": "^2.0.0", @@ -14956,7 +15763,7 @@ }, "readable-stream": { "version": "3.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { "inherits": "^2.0.3", @@ -14966,7 +15773,7 @@ }, "readdir-scoped-modules": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", "requires": { "debuglog": "^1.0.1", @@ -14977,7 +15784,7 @@ }, "registry-auth-token": { "version": "3.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", "requires": { "rc": "^1.1.6", @@ -14986,7 +15793,7 @@ }, "registry-url": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", "requires": { "rc": "^1.0.1" @@ -14994,7 +15801,7 @@ }, "request": { "version": "2.88.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { "aws-sign2": "~0.7.0", @@ -15021,27 +15828,27 @@ }, "require-directory": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "resolve-from": { "version": "4.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, "retry": { "version": "0.12.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, "rimraf": { "version": "2.7.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { "glob": "^7.1.3" @@ -15049,7 +15856,7 @@ }, "run-queue": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", "requires": { "aproba": "^1.1.1" @@ -15057,29 +15864,29 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" } } }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { "version": "5.7.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-diff": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", "requires": { "semver": "^5.0.3" @@ -15087,12 +15894,12 @@ }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "sha": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/sha/-/sha-3.0.0.tgz", "integrity": "sha512-DOYnM37cNsLNSGIG/zZWch5CKIRNoLdYUQTQlcgkRkoYIUwDYjqDyye16YcDZg/OPdcbUgTKMjc4SY6TB7ZAPw==", "requires": { "graceful-fs": "^4.1.2" @@ -15100,7 +15907,7 @@ }, "shebang-command": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "requires": { "shebang-regex": "^1.0.0" @@ -15108,27 +15915,27 @@ }, "shebang-regex": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "slide": { "version": "1.1.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" }, "smart-buffer": { "version": "4.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" }, "socks": { "version": "2.3.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", "requires": { "ip": "1.1.5", @@ -15137,7 +15944,7 @@ }, "socks-proxy-agent": { "version": "4.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", "requires": { "agent-base": "~4.2.1", @@ -15146,7 +15953,7 @@ "dependencies": { "agent-base": { "version": "4.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", "requires": { "es6-promisify": "^5.0.0" @@ -15156,12 +15963,12 @@ }, "sorted-object": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz", "integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=" }, "sorted-union-stream": { "version": "2.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/sorted-union-stream/-/sorted-union-stream-2.1.3.tgz", "integrity": "sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=", "requires": { "from2": "^1.3.0", @@ -15170,7 +15977,7 @@ "dependencies": { "from2": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/from2/-/from2-1.3.0.tgz", "integrity": "sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=", "requires": { "inherits": "~2.0.1", @@ -15179,12 +15986,12 @@ }, "isarray": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "readable-stream": { "version": "1.1.14", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { "core-util-is": "~1.0.0", @@ -15195,14 +16002,14 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, "spdx-correct": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", "requires": { "spdx-expression-parse": "^3.0.0", @@ -15211,12 +16018,12 @@ }, "spdx-exceptions": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" }, "spdx-expression-parse": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "requires": { "spdx-exceptions": "^2.1.0", @@ -15225,17 +16032,17 @@ }, "spdx-license-ids": { "version": "3.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" }, "split-on-first": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" }, "sshpk": { "version": "1.14.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "requires": { "asn1": "~0.2.3", @@ -15251,7 +16058,7 @@ }, "ssri": { "version": "6.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "requires": { "figgy-pudding": "^3.5.1" @@ -15259,7 +16066,7 @@ }, "stream-each": { "version": "1.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", "requires": { "end-of-stream": "^1.1.0", @@ -15268,7 +16075,7 @@ }, "stream-iterate": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/stream-iterate/-/stream-iterate-1.2.0.tgz", "integrity": "sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=", "requires": { "readable-stream": "^2.1.5", @@ -15277,7 +16084,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -15291,7 +16098,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -15301,7 +16108,7 @@ }, "stream-shift": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, "strict-uri-encode": { @@ -15311,7 +16118,7 @@ }, "string-width": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { "is-fullwidth-code-point": "^2.0.0", @@ -15320,17 +16127,17 @@ "dependencies": { "ansi-regex": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "strip-ansi": { "version": "4.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { "ansi-regex": "^3.0.0" @@ -15353,12 +16160,12 @@ }, "stringify-package": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz", "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==" }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -15366,17 +16173,17 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supports-color": { "version": "5.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { "has-flag": "^3.0.0" @@ -15384,7 +16191,7 @@ }, "tar": { "version": "4.4.19", - "resolved": false, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", "requires": { "chownr": "^1.1.4", @@ -15398,7 +16205,7 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "requires": { "safe-buffer": "^5.1.2", @@ -15407,19 +16214,19 @@ }, "safe-buffer": { "version": "5.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "yallist": { "version": "3.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, "term-size": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", "requires": { "execa": "^0.7.0" @@ -15427,17 +16234,17 @@ }, "text-table": { "version": "0.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, "through": { "version": "2.3.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", "requires": { "readable-stream": "^2.1.5", @@ -15446,7 +16253,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -15460,7 +16267,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -15470,17 +16277,17 @@ }, "timed-out": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" }, "tiny-relative-date": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz", "integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==" }, "tough-cookie": { "version": "2.4.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { "psl": "^1.1.24", @@ -15489,7 +16296,7 @@ }, "tunnel-agent": { "version": "0.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { "safe-buffer": "^5.0.1" @@ -15497,28 +16304,28 @@ }, "tweetnacl": { "version": "0.14.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, "typedarray": { "version": "0.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "uid-number": { "version": "0.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" }, "umask": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" }, "unique-filename": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "requires": { "unique-slug": "^2.0.0" @@ -15526,7 +16333,7 @@ }, "unique-slug": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", "requires": { "imurmurhash": "^0.1.4" @@ -15534,7 +16341,7 @@ }, "unique-string": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", "requires": { "crypto-random-string": "^1.0.0" @@ -15542,17 +16349,17 @@ }, "unpipe": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unzip-response": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" }, "update-notifier": { "version": "2.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", "requires": { "boxen": "^1.2.1", @@ -15569,7 +16376,7 @@ }, "uri-js": { "version": "4.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", "requires": { "punycode": "^2.1.0" @@ -15577,14 +16384,14 @@ "dependencies": { "punycode": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" } } }, "url-parse-lax": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", "requires": { "prepend-http": "^1.0.1" @@ -15592,17 +16399,17 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util-extend": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=" }, "util-promisify": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz", "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", "requires": { "object.getownpropertydescriptors": "^2.0.3" @@ -15610,12 +16417,12 @@ }, "uuid": { "version": "3.3.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "validate-npm-package-license": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "requires": { "spdx-correct": "^3.0.0", @@ -15624,7 +16431,7 @@ }, "validate-npm-package-name": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", "requires": { "builtins": "^1.0.3" @@ -15632,7 +16439,7 @@ }, "verror": { "version": "1.10.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { "assert-plus": "^1.0.0", @@ -15642,7 +16449,7 @@ }, "wcwidth": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", "requires": { "defaults": "^1.0.3" @@ -15650,7 +16457,7 @@ }, "which": { "version": "1.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "requires": { "isexe": "^2.0.0" @@ -15658,12 +16465,12 @@ }, "which-module": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { "version": "1.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "requires": { "string-width": "^1.0.2" @@ -15671,7 +16478,7 @@ "dependencies": { "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", @@ -15683,7 +16490,7 @@ }, "widest-line": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", "requires": { "string-width": "^2.1.1" @@ -15691,7 +16498,7 @@ }, "worker-farm": { "version": "1.7.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", "requires": { "errno": "~0.1.7" @@ -15699,7 +16506,7 @@ }, "wrap-ansi": { "version": "5.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "requires": { "ansi-styles": "^3.2.0", @@ -15709,17 +16516,17 @@ "dependencies": { "ansi-regex": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { "emoji-regex": "^7.0.1", @@ -15729,7 +16536,7 @@ }, "strip-ansi": { "version": "5.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" @@ -15739,12 +16546,12 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "2.4.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", "requires": { "graceful-fs": "^4.1.11", @@ -15754,27 +16561,27 @@ }, "xdg-basedir": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" }, "xtend": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "y18n": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, "yallist": { "version": "3.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" }, "yargs": { "version": "14.2.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", "requires": { "cliui": "^5.0.0", @@ -15792,12 +16599,12 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "find-up": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { "locate-path": "^3.0.0" @@ -15805,12 +16612,12 @@ }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "locate-path": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { "p-locate": "^3.0.0", @@ -15819,7 +16626,7 @@ }, "p-limit": { "version": "2.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "requires": { "p-try": "^2.0.0" @@ -15827,7 +16634,7 @@ }, "p-locate": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { "p-limit": "^2.0.0" @@ -15835,12 +16642,12 @@ }, "p-try": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "string-width": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { "emoji-regex": "^7.0.1", @@ -15850,7 +16657,7 @@ }, "strip-ansi": { "version": "5.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" @@ -15860,7 +16667,7 @@ }, "yargs-parser": { "version": "15.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", "requires": { "camelcase": "^5.0.0", @@ -15869,7 +16676,7 @@ "dependencies": { "camelcase": { "version": "5.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" } } @@ -15879,7 +16686,7 @@ "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "requires": { "path-key": "^2.0.0" } @@ -15906,7 +16713,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" }, "nwsapi": { "version": "2.2.0", @@ -15917,7 +16724,7 @@ "oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" }, "oauth-sign": { "version": "0.9.0", @@ -15927,12 +16734,12 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -15942,7 +16749,7 @@ "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "requires": { "is-descriptor": "^0.1.0" } @@ -15955,7 +16762,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -16012,7 +16819,7 @@ "magicli": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", + "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", "requires": { "commander": "^2.9.0", "get-stdin": "^5.0.1", @@ -16025,7 +16832,7 @@ "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", "requires": { "isobject": "^3.0.0" } @@ -16087,7 +16894,7 @@ "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "requires": { "isobject": "^3.0.1" } @@ -16125,7 +16932,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } @@ -16187,12 +16994,12 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==" }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" }, "osenv": { "version": "0.1.5", @@ -16211,12 +17018,12 @@ "p-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-debounce/-/p-debounce-1.0.0.tgz", - "integrity": "sha1-y38svu/YegnrqGHhErZ1J+Yh4v0=" + "integrity": "sha512-ttOxn4Yt0hzIsLLqKi/Ry9QRxW+UQKdoWHz7g99Ci57zPkqUU3kbWKAeHuv+HfRLe109acYLUY6kuVCOOqnt4g==" }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" }, "p-limit": { "version": "2.3.0", @@ -16257,7 +17064,7 @@ "package-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "integrity": "sha512-q/R5GrMek0vzgoomq6rm9OX+3PQve8sLwTirmK30YB3Cu0Bbt9OX9M/SIUnroN5BGJkzwGsFwDaRGD9EwBOlCA==", "requires": { "got": "^6.7.1", "registry-auth-token": "^3.0.1", @@ -16273,7 +17080,7 @@ "got": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "integrity": "sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==", "requires": { "create-error-class": "^3.0.0", "duplexer3": "^0.1.4", @@ -16389,7 +17196,7 @@ "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==" }, "passport": { "version": "0.4.1", @@ -16411,7 +17218,7 @@ "passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", "requires": { "passport-strategy": "1.x.x" } @@ -16431,7 +17238,7 @@ "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" }, "path-browserify": { "version": "1.0.1", @@ -16441,27 +17248,27 @@ "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==" }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-is-inside": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==" }, "path-parse": { "version": "1.0.7", @@ -16471,7 +17278,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "path-type": { "version": "4.0.0", @@ -16486,7 +17293,7 @@ "pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "pdf-parse": { "version": "1.1.1", @@ -16534,7 +17341,7 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "perfect-scrollbar": { "version": "1.5.5", @@ -16544,7 +17351,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "picocolors": { "version": "1.0.0", @@ -16564,12 +17371,12 @@ "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "requires": { "pinkie": "^2.0.0" } @@ -16630,7 +17437,7 @@ "plist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz", - "integrity": "sha1-CEtQk93JJQbiWfh0uNmxr7jHlZM=", + "integrity": "sha512-dL9Xc2Aj3YyBnwvCNuHmFl2LWvQacm/HEAsoVwLiuu0POboMChETt5wexpU1P6F6MnibIucXlVsMFFgNUT2IyA==", "requires": { "base64-js": "0.0.8", "util-deprecate": "1.0.2", @@ -16641,7 +17448,7 @@ "base64-js": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==" } } }, @@ -16681,7 +17488,7 @@ "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==" }, "postcss": { "version": "7.0.39", @@ -16879,13 +17686,13 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==" }, "prettier": { "version": "2.7.1", @@ -16934,7 +17741,7 @@ "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, "process-nextick-args": { "version": "2.0.1", @@ -16957,7 +17764,7 @@ "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "dev": true }, "prop-types": { @@ -16970,6 +17777,11 @@ "react-is": "^16.13.1" } }, + "property-information": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", + "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==" + }, "prosemirror-commands": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz", @@ -17003,7 +17815,7 @@ "prosemirror-find-replace": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/prosemirror-find-replace/-/prosemirror-find-replace-0.9.0.tgz", - "integrity": "sha1-QgsENNF5xdBJD44hSNhVGpVJY4I=" + "integrity": "sha512-LfhQ/Zr0PkkJpCsr9vTJ5ZPYh49mSVVG+hHJ6djT+chlCW+t2ilSxBpBG+2IeE/I5nlbcvuLLAbxeI1g3pTCpA==" }, "prosemirror-history": { "version": "1.3.0", @@ -17095,13 +17907,13 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" }, "psl": { "version": "1.8.0", @@ -17333,12 +18145,12 @@ "pure-color": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", - "integrity": "sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=" + "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==" }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" }, "qs": { "version": "6.5.3", @@ -17359,7 +18171,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", "dev": true }, "querystringify": { @@ -17381,7 +18193,7 @@ "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" }, "randombytes": { "version": "2.1.0", @@ -17484,7 +18296,7 @@ "react-base16-styling": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz", - "integrity": "sha1-OFjyTpxN2MvT9wLz901YHKKRcmk=", + "integrity": "sha512-EPuchwVvYPSFFIjGpH0k6wM0HQsmJ0vCk7BSl5ryxMVFIWW4hX4Kksu4PNtxfgOxDebTLkJQ8iC7zwAql0eusg==", "requires": { "base16": "^1.0.0", "lodash.curry": "^4.0.1", @@ -17553,7 +18365,7 @@ "react-dock": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/react-dock/-/react-dock-0.2.4.tgz", - "integrity": "sha1-5yfcdVCztzEWY13LnA4E0Lev4Xw=", + "integrity": "sha512-ywUJPC/TIM9PO700skka0fH4aqbrH8RojUXejZFvjtqlc5KZ+xjHqFdo4A3j+dp+0NLFZ3Nai4xzcf3FUJ9BsQ==", "requires": { "lodash.debounce": "^3.1.1", "prop-types": "^15.5.8" @@ -17715,6 +18527,35 @@ "resolved": "https://registry.npmjs.org/react-loading/-/react-loading-2.0.3.tgz", "integrity": "sha512-Vdqy79zq+bpeWJqC+xjltUjuGApyoItPgL0vgVfcJHhqwU7bAMKzysfGW/ADu6i0z0JiOCRJjo+IkFNkRNbA3A==" }, + "react-markdown": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.3.tgz", + "integrity": "sha512-We36SfqaKoVNpN1QqsZwWSv/OZt5J15LNgTLWynwAN5b265hrQrsjMtlRNwUvS+YyR3yDM8HpTNc4pK9H/Gc0A==", + "requires": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.3.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-measure": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-2.5.2.tgz", @@ -17897,7 +18738,7 @@ "react-themeable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz", - "integrity": "sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=", + "integrity": "sha512-kl5tQ8K+r9IdQXZd8WLa+xxYN04lLnJXRVhHfdgwsUJr/SlKJxIejoc9z9obEkx1mdqbTw1ry43fxEUwyD9u7w==", "requires": { "object-assign": "^3.0.0" }, @@ -17905,7 +18746,7 @@ "object-assign": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==" } } }, @@ -17960,7 +18801,7 @@ "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -17970,7 +18811,7 @@ "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "requires": { "graceful-fs": "^4.1.2", "pify": "^2.0.0", @@ -17982,7 +18823,7 @@ "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -17991,7 +18832,7 @@ "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -18000,7 +18841,7 @@ "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "requires": { "pinkie-promise": "^2.0.0" } @@ -18046,12 +18887,12 @@ "readline": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", - "integrity": "sha1-xYDXfvLPyHUrEySYBg3JeTp6wBw=" + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "requires": { "resolve": "^1.1.6" } @@ -18077,7 +18918,7 @@ "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "integrity": "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==", "requires": { "indent-string": "^2.1.0", "strip-indent": "^1.0.1" @@ -18086,7 +18927,7 @@ "reduce-flatten": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" + "integrity": "sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ==" }, "redux": { "version": "4.2.0", @@ -18143,20 +18984,62 @@ "registry-url": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", "requires": { "rc": "^1.0.1" } }, + "rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + } + }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==" + }, + "remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + } + }, + "remark-parse": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", + "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + } + }, + "remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + } }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==" }, "renderkid": { "version": "3.0.0", @@ -18242,12 +19125,12 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" }, "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", "requires": { "is-finite": "^1.0.0" } @@ -18351,7 +19234,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "require-from-string": { "version": "2.0.2", @@ -18366,7 +19249,7 @@ "require-package-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", - "integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=" + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==" }, "require_optional": { "version": "1.0.1", @@ -18380,14 +19263,14 @@ "resolve-from": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + "integrity": "sha512-qpFcKaXsq8+oRoLilkwyc7zHGF5i9Q2/25NIgLQQ/+VVv9rU4qvr6nXVAw1DsnXJyQkZsR4Ytfbtg5ehfcUssQ==" } } }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "resize-observer-polyfill": { @@ -18433,7 +19316,7 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==" }, "responselike": { "version": "2.0.0", @@ -18468,7 +19351,7 @@ "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true }, "reveal.js": { @@ -18479,7 +19362,7 @@ "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", "requires": { "align-text": "^0.1.1" } @@ -18514,7 +19397,7 @@ "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", "dev": true, "requires": { "aproba": "^1.1.1" @@ -18545,6 +19428,14 @@ } } }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "requires": { + "mri": "^1.1.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -18553,7 +19444,7 @@ "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", "requires": { "ret": "~0.1.10" } @@ -18641,13 +19532,13 @@ "scss-loader": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/scss-loader/-/scss-loader-0.0.1.tgz", - "integrity": "sha1-6uAXueDzjBKlMtslwiC5Avs05nE=", + "integrity": "sha512-SbT/smRJjkvvdHSEdAYAplosVkrtaSwwgUlnQCOuDS5sOKNjrS/eYCMvKeV6+YxK5cCOCsOJZd3vltrXatFp+g==", "dev": true }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "integrity": "sha512-dYE8LhncfBUar6POCxMTm0Ln+erjeczqEvCJib5/7XNkdw1FkUGgwMPY360FY0FgPWQxHWCx29Jl3oejyGLM9Q==", "requires": { "js-base64": "^2.1.8", "source-map": "^0.4.2" @@ -18656,7 +19547,7 @@ "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "integrity": "sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==", "requires": { "amdefine": ">=0.0.4" } @@ -18671,17 +19562,17 @@ "section-iterator": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz", - "integrity": "sha1-v0RNev7rlK1Dw5rS+yYVFifMuio=" + "integrity": "sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ==" }, "select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "dev": true }, "selfsigned": { @@ -18701,12 +19592,12 @@ "semver-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=" + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" }, "semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", "requires": { "semver": "^5.0.3" } @@ -18770,7 +19661,7 @@ "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, "requires": { "accepts": "~1.3.4", @@ -18800,7 +19691,7 @@ "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, "requires": { "depd": "~1.1.2", @@ -18812,13 +19703,13 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "setprototypeof": { @@ -18849,7 +19740,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "set-value": { "version": "2.0.1", @@ -18865,7 +19756,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "requires": { "is-extendable": "^0.1.0" } @@ -18875,7 +19766,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "setprototypeof": { "version": "1.2.0", @@ -19037,7 +19928,7 @@ "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "requires": { "shebang-regex": "^1.0.0" } @@ -19045,7 +19936,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==" }, "shelljs": { "version": "0.8.5", @@ -19080,7 +19971,7 @@ "simple-assign": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/simple-assign/-/simple-assign-0.1.0.tgz", - "integrity": "sha1-F/0wZqXz13OPUDIbsPFMooHMS6o=" + "integrity": "sha512-otdSSQzuVsmDoe5MnSm4ZgHd5sl0ak6A1CTjW1R/DUHQ8xoZuU1NUzf9x6n9Dvp3nxpvW51WNMQ/7rQ9432xDg==" }, "simple-concat": { "version": "1.0.1", @@ -19100,7 +19991,7 @@ "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "requires": { "is-arrayish": "^0.3.1" }, @@ -19115,7 +20006,7 @@ "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", "dev": true }, "slice-ansi": { @@ -19132,7 +20023,7 @@ "sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" }, "snapdragon": { "version": "0.8.2", @@ -19160,7 +20051,7 @@ "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "requires": { "is-descriptor": "^0.1.0" } @@ -19168,7 +20059,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "requires": { "is-extendable": "^0.1.0" } @@ -19176,7 +20067,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -19193,7 +20084,7 @@ "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "requires": { "is-descriptor": "^1.0.0" } @@ -19242,7 +20133,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -19338,7 +20229,7 @@ "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==" }, "debug": { "version": "4.1.1", @@ -19351,7 +20242,7 @@ "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" } } }, @@ -19421,7 +20312,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" }, "source-map-resolve": { "version": "0.5.3", @@ -19456,10 +20347,15 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" }, + "space-separated-tokens": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz", + "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==" + }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", "optional": true, "requires": { "memory-pager": "^1.0.2" @@ -19562,7 +20458,7 @@ "split-skip": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.2.tgz", - "integrity": "sha1-2J2Iu9L3Pka1FYqjcKVhIk6A1GE=" + "integrity": "sha512-weHOi8BolsDnGIwhhWHbA+wKSuSpvWwjRrdj8SdbIIis2vSwOE37CQP8x3EleuzxanUr3AK8BdUy4MkiOULPZg==" }, "split-string": { "version": "3.1.0", @@ -19575,7 +20471,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "sshpk": { "version": "1.17.0", @@ -19627,7 +20523,7 @@ "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "requires": { "is-descriptor": "^0.1.0" } @@ -19706,7 +20602,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -19796,7 +20692,7 @@ "magicli": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha1-zufQ+7THBRiqyxHsPrfiX/SaSSE=", + "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", "requires": { "commander": "^2.9.0", "get-stdin": "^5.0.1", @@ -19838,7 +20734,7 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==" } } }, @@ -19857,6 +20753,14 @@ "schema-utils": "^1.0.0" } }, + "style-to-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", + "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "requires": { + "inline-style-parser": "0.1.1" + } + }, "styled-components": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", @@ -20203,7 +21107,7 @@ "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==" + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" }, "to-fast-properties": { "version": "2.0.0", @@ -20226,7 +21130,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -20274,7 +21178,7 @@ "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", "requires": { "abbrev": "1" } @@ -20330,11 +21234,21 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, + "trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" }, + "trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==" + }, "true-case-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", @@ -20399,7 +21313,7 @@ "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true } } @@ -20715,6 +21629,12 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -20797,7 +21717,7 @@ "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "requires": { "graceful-fs": "^4.1.6" } @@ -20827,12 +21747,12 @@ "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==" }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", "requires": { "center-align": "^0.1.1", "right-align": "^0.1.1", @@ -20913,10 +21833,24 @@ "pako": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" } } }, + "unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "requires": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + } + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -20954,6 +21888,64 @@ "crypto-random-string": "^1.0.0" } }, + "unist-builder": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz", + "integrity": "sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-generated": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz", + "integrity": "sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==" + }, + "unist-util-is": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz", + "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==" + }, + "unist-util-position": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.3.tgz", + "integrity": "sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-stringify-position": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz", + "integrity": "sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-visit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz", + "integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + } + }, + "unist-util-visit-parents": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz", + "integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + } + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -20986,7 +21978,7 @@ "has-value": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -20996,7 +21988,7 @@ "isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", "requires": { "isarray": "1.0.0" } @@ -21006,7 +21998,7 @@ "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==" } } }, @@ -21078,7 +22070,7 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", "dev": true } } @@ -21184,6 +22176,24 @@ "resolved": "https://registry.npmjs.org/uuid-js/-/uuid-js-0.7.5.tgz", "integrity": "sha1-bIhtAqU9LUDc8l2RoXC0p7JblNA=" }, + "uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "requires": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "dependencies": { + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" + } + } + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -21231,6 +22241,35 @@ } } }, + "vfile": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.4.tgz", + "integrity": "sha512-KI+7cnst03KbEyN1+JE504zF5bJBZa+J+CrevLeyIMq0aPU681I2rQ5p4PlnQ6exFtWiUrg26QUdFMnAKR6PIw==", + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + } + }, + "vfile-location": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz", + "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==", + "requires": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + } + }, + "vfile-message": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz", + "integrity": "sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + } + }, "void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", @@ -21348,6 +22387,11 @@ "minimalistic-assert": "^1.0.0" } }, + "web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==" + }, "web-request": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/web-request/-/web-request-1.0.7.tgz", @@ -21579,7 +22623,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "debug": { @@ -21604,7 +22648,7 @@ "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", "dev": true, "requires": { "errno": "^0.1.3", @@ -21650,7 +22694,7 @@ "resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", "dev": true, "requires": { "resolve-from": "^3.0.0" @@ -21659,7 +22703,7 @@ "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", "dev": true }, "semver": { @@ -22073,7 +23117,7 @@ "lodash": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + "integrity": "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==" } } }, @@ -22200,7 +23244,7 @@ "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==" + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" }, "yn": { "version": "2.0.0", @@ -22222,6 +23266,11 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==" + }, + "zwitch": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", + "integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==" } } } diff --git a/package.json b/package.json index 2e3ef0a07..ccbc73a79 100644 --- a/package.json +++ b/package.json @@ -66,17 +66,17 @@ "@types/prosemirror-view": "^1.23.1", "@types/rc-switch": "^1.9.2", "@types/react": "^18.0.15", - "@types/react-icons": "^3.0.0", - "@types/react-reconciler": "^0.26.4", - "@types/react-transition-group": "^4.4.5", "@types/react-autosuggest": "^9.3.14", "@types/react-color": "^2.17.6", "@types/react-datepicker": "^3.1.8", "@types/react-dom": "^18.0.6", "@types/react-grid-layout": "^1.3.2", + "@types/react-icons": "^3.0.0", "@types/react-measure": "^2.0.8", + "@types/react-reconciler": "^0.26.4", "@types/react-select": "^3.1.2", "@types/react-table": "^6.8.9", + "@types/react-transition-group": "^4.4.5", "@types/request": "^2.48.8", "@types/request-promise": "^4.1.48", "@types/rimraf": "^2.0.5", @@ -84,6 +84,7 @@ "@types/shelljs": "^0.8.11", "@types/socket.io": "^2.1.13", "@types/socket.io-client": "^1.4.36", + "@types/socket.io-parser": "^3.0.0", "@types/typescript": "^2.0.0", "@types/uuid": "^3.4.10", "@types/valid-url": "^1.0.3", @@ -137,6 +138,7 @@ "@hig/theme-context": "^2.1.3", "@hig/theme-data": "^2.23.1", "@material-ui/core": "^4.12.3", + "@octokit/core": "^4.0.4", "@react-google-maps/api": "^2.7.0", "@react-three/fiber": "^6.2.3", "@types/bezier-js": "^4.1.0", @@ -272,6 +274,7 @@ "react-icons": "^4.3.1", "react-jsx-parser": "^1.29.0", "react-loading": "^2.0.3", + "react-markdown": "^8.0.3", "react-measure": "^2.5.2", "react-refresh-typescript": "^2.0.7", "react-resizable": "^1.11.1", @@ -281,6 +284,8 @@ "react-table": "^6.11.5", "react-transition-group": "^4.4.2", "readline": "^1.3.0", + "rehype-raw": "^6.1.1", + "remark-gfm": "^3.0.1", "request": "^2.88.2", "request-promise": "^4.2.6", "reveal.js": "^4.3.0", diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 4751acf44..4ed785983 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/Utils.ts b/src/Utils.ts index 528a429d0..6fc00040f 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -111,6 +111,7 @@ export namespace Utils { const isTransparentFunctionHack = 'isTransparent(__value__)'; export const noRecursionHack = '__noRecursion'; + export const noDragsDocFilter = 'noDragDocs:any:check'; export function IsRecursiveFilter(val: string) { return !val.includes(noRecursionHack); } @@ -125,7 +126,7 @@ export namespace Utils { // bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field } - export function PropUnsetFilter(prop: string) { + export function IsPropUnsetFilter(prop: string) { return `${prop}:any,${noRecursionHack}:unset`; } @@ -220,8 +221,8 @@ export namespace Utils { if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) { return Math.ceil(targetY + minSpacing + targetHgt - contextHgt); } - if (scrollTop >= Math.max(0, targetY - minSpacing - targetHgt)) { - return Math.max(0, Math.floor(targetY - minSpacing - targetHgt)); + if (scrollTop >= Math.max(0, targetY - minSpacing)) { + return Math.max(0, Math.floor(targetY - minSpacing)); } } diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 1b0ba6bc3..5a34fcf11 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -77,7 +77,7 @@ export namespace DocServer { } export function getFieldWriteMode(field: string) { - return fieldWriteModes[field] || WriteMode.Default; + return Doc.CurrentUserEmail === 'guest' ? WriteMode.LiveReadonly : fieldWriteModes[field] || WriteMode.Default; } export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) { @@ -178,7 +178,7 @@ export namespace DocServer { _isReadOnly = true; _CreateField = field => (_cache[field[Id]] = field); _UpdateField = emptyFunction; - _RespondToUpdate = emptyFunction; // bcz: option: don't clear RespondToUpdate to continue to receive updates as others change the DB + // _RespondToUpdate = emptyFunction; // bcz: option: don't clear RespondToUpdate to continue to receive updates as others change the DB } } diff --git a/src/client/Network.ts b/src/client/Network.ts index c781d4b6b..a222b320f 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -1,57 +1,54 @@ -import { Utils } from "../Utils"; +import { Utils } from '../Utils'; import requestPromise = require('request-promise'); -import { Upload } from "../server/SharedMediaTypes"; +import { Upload } from '../server/SharedMediaTypes'; export namespace Networking { + export async function FetchFromServer(relativeRoute: string) { + return (await fetch(relativeRoute)).text(); + } - export async function FetchFromServer(relativeRoute: string) { - return (await fetch(relativeRoute)).text(); - } + export async function PostToServer(relativeRoute: string, body?: any) { + const options = { + uri: Utils.prepend(relativeRoute), + method: 'POST', + body, + json: true, + }; + return requestPromise.post(options); + } - export async function PostToServer(relativeRoute: string, body?: any) { - const options = { - uri: Utils.prepend(relativeRoute), - method: "POST", - body, - json: true - }; - return requestPromise.post(options); - } - - /** - * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint - * with the mapping of GUID to filem as parameters. - * - * @param files the files to be uploaded to the server - * @returns the response as a json from the server - */ - export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> { - const formData = new FormData(); - if (Array.isArray(files)) { - if (!files.length) { - return []; - } - files.forEach(file => formData.append(Utils.GenerateGuid(), file)); - } else { - formData.append(Utils.GenerateGuid(), files); + /** + * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint + * with the mapping of GUID to filem as parameters. + * + * @param files the files to be uploaded to the server + * @returns the response as a json from the server + */ + export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> { + const formData = new FormData(); + if (Array.isArray(files)) { + if (!files.length) { + return []; } - const parameters = { - method: 'POST', - body: formData - }; - const response = await fetch("/uploadFormData", parameters); - return response.json(); - } - - export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> { - const parameters = { - method: 'POST', - body: JSON.stringify({ videoId }), - json: true - }; - const response = await fetch("/uploadYoutubeVideo", parameters); - return response.json(); - } - + files.forEach(file => formData.append(Utils.GenerateGuid(), file)); + } else { + formData.append(Utils.GenerateGuid(), files); + } + const parameters = { + method: 'POST', + body: formData, + }; + const response = await fetch('/uploadFormData', parameters); + return response.json(); + } -}
\ No newline at end of file + export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> { + const parameters = { + method: 'POST', + body: JSON.stringify({ videoId }), + json: true, + }; + const response = await fetch('/uploadYoutubeVideo', parameters); + return response.json(); + } +} diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 4e98a90a3..d99cd2dac 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -26,6 +26,7 @@ export enum DocumentType { FUNCPLOT = 'funcplot', // function plotter MAP = 'map', DATAVIZ = 'dataviz', + LOADING = 'loading', // special purpose wrappers that either take no data or are compositions of lower level types LINK = 'link', @@ -63,4 +64,5 @@ export enum CollectionViewType { Grid = 'grid', Pile = 'pileup', StackedTimeline = 'stacked timeline', + NoteTaking = 'notetaking', } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ed4c99d70..57a24b304 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -31,7 +31,7 @@ import { CollectionView } from '../views/collections/CollectionView'; import { ContextMenu } from '../views/ContextMenu'; import { ContextMenuProps } from '../views/ContextMenuItem'; import { DFLT_IMAGE_NATIVE_DIM } from '../views/global/globalCssVariables.scss'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from '../views/InkingStroke'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke } from '../views/InkingStroke'; import { AudioBox } from '../views/nodes/AudioBox'; import { FontIconBox } from '../views/nodes/button/FontIconBox'; import { ColorBox } from '../views/nodes/ColorBox'; @@ -48,6 +48,7 @@ import { KeyValueBox } from '../views/nodes/KeyValueBox'; import { LabelBox } from '../views/nodes/LabelBox'; import { LinkBox } from '../views/nodes/LinkBox'; import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup'; +import { LoadingBox } from '../views/nodes/LoadingBox'; import { MapBox } from '../views/nodes/MapBox/MapBox'; import { PDFBox } from '../views/nodes/PDFBox'; import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox'; @@ -264,12 +265,6 @@ export class DocumentOptions { baseProto?: boolean; // is this a base prototoype dontRegisterView?: boolean; lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. - 'onDoubleClick-rawScript'?: string; // onDoubleClick script in raw text form - 'onChildDoubleClick-rawScript'?: string; // onChildDoubleClick script in raw text form - 'onChildClick-rawScript'?: string; // on ChildClick script in raw text form - 'onClick-rawScript'?: string; // onClick script in raw text form - 'onCheckedClick-rawScript'?: string; // onChecked script in raw text form - 'onCheckedClick-params'?: List<string>; // parameter list for onChecked treeview functions columnHeaders?: List<SchemaHeaderField>; // headers for stacking views schemaHeaders?: List<SchemaHeaderField>; // headers for schema view clipWidth?: number; // percent transition from before to after in comparisonBox @@ -465,7 +460,7 @@ export namespace Docs { DocumentType.AUDIO, { layout: { view: AudioBox, dataField: defaultDataKey }, - options: { _height: 100, backgroundColor: 'lightGray', forceReflow: true, nativeDimModifiable: true, links: '@links(self)' }, + options: { _height: 100, backgroundColor: 'lightGray', _fitWidth: true, forceReflow: true, nativeDimModifiable: true, links: '@links(self)' }, }, ], [ @@ -654,6 +649,13 @@ export namespace Docs { options: { _fitWidth: true, nativeDimModifiable: true, links: '@links(self)' }, }, ], + [ + DocumentType.LOADING, + { + layout: { view: LoadingBox, dataField: '' }, + options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' }, + }, + ], ]); const suffix = 'Proto'; @@ -792,7 +794,7 @@ export namespace Docs { * only when creating a DockDocument from the current user's already existing * main document. */ - function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string) { + function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc) { const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_" const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_'); @@ -803,21 +805,36 @@ export namespace Docs { dataProps.isPrototype = true; dataProps.author = Doc.CurrentUserEmail; dataProps.creationDate = new DateField(); - dataProps[`${fieldKey}-lastModified`] = new DateField(); + if (fieldKey) { + dataProps[`${fieldKey}-lastModified`] = new DateField(); + dataProps[fieldKey] = data; + + // so that the list of annotations is already initialised, prevents issues in addonly. + // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. + dataProps[fieldKey + '-annotations'] = new List<Doc>(); + dataProps[fieldKey + '-sidebar'] = new List<Doc>(); + } - dataProps[fieldKey] = data; + // users placeholderDoc as proto if it exists + const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); - // so that the list of annotations is already initialised, prevents issues in addonly. - // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. - dataProps[fieldKey + '-annotations'] = new List<Doc>(); - dataProps[fieldKey + '-sidebar'] = new List<Doc>(); - const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); + if (placeholderDoc) { + dataDoc.proto = proto; + } const viewFirstProps: { [id: string]: any } = {}; viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; viewFirstProps['acl-Override'] = 'None'; viewFirstProps.author = Doc.CurrentUserEmail; - const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); + let viewDoc: Doc; + // determines whether viewDoc should be created using placeholder Doc or default + if (placeholderDoc) { + placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined; + placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined; + viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true); + } else { + viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); + } Doc.assign(viewDoc, viewProps, true, true); ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); @@ -829,12 +846,13 @@ export namespace Docs { updateCachedAcls(dataDoc); updateCachedAcls(viewDoc); + return viewDoc; } - export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}) { + export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) { const imgField = url instanceof ImageField ? url : new ImageField(url); - return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc); } export function PresDocument(options: DocumentOptions = {}) { @@ -845,12 +863,12 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script : undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined }); } - export function VideoDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options); + export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { + return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc); } - export function YoutubeDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options); + export function YoutubeDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { + return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options, undefined, undefined, undefined, overwriteDoc); } export function WebCamDocument(url: string, options: DocumentOptions = {}) { @@ -865,8 +883,16 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options); } - export function AudioDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }); + export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { + return InstanceFromProto( + Prototypes.get(DocumentType.AUDIO), + new AudioField(url), + { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }, + undefined, + undefined, + undefined, + overwriteDoc + ); } export function RecordingDocument(url: string, options: DocumentOptions = {}) { @@ -880,6 +906,9 @@ export namespace Docs { export function ColorDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options); } + export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, ''); + } export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); @@ -924,9 +953,22 @@ export namespace Docs { return linkDoc; } - export function InkDocument(color: string, tool: string, strokeWidth: number, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: PointData[], options: DocumentOptions = {}) { + export function InkDocument( + color: string, + tool: string, + strokeWidth: number, + strokeBezier: string, + fillColor: string, + arrowStart: string, + arrowEnd: string, + dash: string, + points: PointData[], + isInkMask: boolean, + options: DocumentOptions = {} + ) { const I = new Doc(); I[Initializing] = true; + I.isInkMask = isInkMask; I.type = DocumentType.INK; I.layout = InkingStroke.LayoutString('data'); I.color = color; @@ -957,13 +999,13 @@ export namespace Docs { return I; } - export function PdfDocument(url: string, options: DocumentOptions = {}) { + export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { const width = options._width || undefined; const height = options._height || undefined; const nwid = options._nativeWidth || undefined; const nhght = options._nativeHeight || undefined; if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width); - return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options); + return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options, undefined, undefined, undefined, overwriteDoc); } export function WebDocument(url: string, options: DocumentOptions = {}) { @@ -1041,6 +1083,17 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId); } + export function NoteTakingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { + return InstanceFromProto( + Prototypes.get(DocumentType.COL), + new List(documents), + { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _viewType: CollectionViewType.NoteTaking }, + id, + undefined, + protoId + ); + } + export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn }); } @@ -1065,7 +1118,7 @@ export namespace Docs { } export function ButtonDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), 'onClick-rawScript': '-script-' }); + return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) }); } export function SliderDocument(options?: DocumentOptions) { @@ -1083,8 +1136,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) }); } - export function DataVizDocument(url: string, options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }); + export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) { + return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }, undefined, undefined, undefined, overwriteDoc); } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { @@ -1125,30 +1178,6 @@ export namespace Docs { } export namespace DocUtils { - export function Excluded(d: Doc, docFilters: string[]) { - const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - docFilters.forEach(filter => { - const fields = filter.split(':'); - const key = fields[0]; - const value = fields[1]; - const modifiers = fields[2]; - if (!filterFacets[key]) { - filterFacets[key] = {}; - } - filterFacets[key][value] = modifiers; - }); - - if (d.z) return false; - for (const facetKey of Object.keys(filterFacets)) { - const facet = filterFacets[facetKey]; - const xs = Object.keys(facet).filter(value => facet[value] === 'x'); - const failsNotEqualFacets = xs?.some(value => Doc.matchFieldValue(d, facetKey, value)); - if (failsNotEqualFacets) { - return true; - } - } - return false; - } /** * @param docs * @param docFilters @@ -1182,7 +1211,7 @@ export namespace DocUtils { return false; } - for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies')) { + for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(':')[0])) { const facet = filterFacets[facetKey]; // facets that match some value in the field of the document (e.g. some text field) @@ -1282,10 +1311,11 @@ export namespace DocUtils { export let ActiveRecordings: { props: FieldViewProps; getAnchor: () => Doc }[] = []; - export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) { + export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) { broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1)); return DocUtils.ActiveRecordings.map(audio => { - const link = DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline'); + const sourceDoc = getSourceDoc(); + const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline'); link && (link.followLinkLocation = 'add:right'); return link; }); @@ -1356,7 +1386,17 @@ export namespace DocUtils { scripts && Object.keys(scripts).map(key => { if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && scripts[key]) { - doc[key] = ScriptField.MakeScript(scripts[key], { dragData: DragManager.DocumentDragData.name, value: 'any', scriptContext: 'any', documentView: Doc.name }, { _readOnly_: true }); + doc[key] = ScriptField.MakeScript(scripts[key], { + dragData: DragManager.DocumentDragData.name, + value: 'any', + _readOnly_: 'boolean', + scriptContext: 'any', + thisContainer: Doc.name, + documentView: Doc.name, + heading: Doc.name, + checked: 'boolean', + containingTreeView: Doc.name, + }); } }); funcs && @@ -1414,7 +1454,7 @@ export namespace DocUtils { created = Docs.Create.RecordingDocument(field.url.href, resolved); layout = RecordingBox.LayoutString; } else if (field instanceof InkField) { - created = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, resolved); + created = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved); layout = InkingStroke.LayoutString; } else if (field instanceof List && field[0] instanceof Doc) { created = Docs.Create.StackingDocument(DocListCast(field), resolved); @@ -1434,8 +1474,16 @@ export namespace DocUtils { return created; } - export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { - let ctor: ((path: string, options: DocumentOptions) => Doc | Promise<Doc | undefined>) | undefined = undefined; + /** + * + * @param type the type of file. + * @param path the path to the file. + * @param options the document options. + * @param overwriteDoc the placeholder loading doc. + * @returns + */ + export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise<Opt<Doc>> { + let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined = undefined; if (type.indexOf('image') !== -1) { ctor = Docs.Create.ImageDocument; if (!options._width) options._width = 300; @@ -1484,10 +1532,10 @@ export namespace DocUtils { options = { ...options, _width: 400, _height: 512, title: path }; } - return ctor ? ctor(path, options) : undefined; + return ctor ? ctor(path, options, overwriteDoc) : undefined; } - export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false): void { + export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void { !simpleMenu && ContextMenu.Instance.addItem({ description: 'Quick Notes', @@ -1503,6 +1551,9 @@ export namespace DocUtils { }); textDoc.layoutKey = 'layout_' + note.title; textDoc[textDoc.layoutKey] = note; + if (pivotField) { + textDoc[pivotField] = pivotValue; + } docTextAdder(textDoc); }), icon: StrCast(note.icon) as IconProp, @@ -1523,6 +1574,9 @@ export namespace DocUtils { newDoc.y = y; EquationBox.SelectOnLoad = newDoc[Id]; if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id]; + if (pivotField) { + newDoc[pivotField] = pivotValue; + } docAdder?.(newDoc); } }), @@ -1694,14 +1748,15 @@ export namespace DocUtils { return dd; } - async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) { + async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, rootDoc?: Doc) { if (result instanceof Error) { alert(`Upload failed: ${result.message}`); return; } const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); - const doc = await DocUtils.DocumentFromType(type, pathname, full); + const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc); + rootDoc && (rootDoc.isLoading = undefined); if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; @@ -1711,7 +1766,8 @@ export namespace DocUtils { .replace(/\.[a-z0-9]*$/, ''); if (Upload.isImageInformation(result)) { const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); - proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (StrCast((result.exifData?.data as any)?.Orientation).includes('Rotate 90') ? 5 : undefined); + const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase(); + proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); proto['data-nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto['data-nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); if (NumCast(proto['data-nativeOrientation']) >= 5) { @@ -1729,6 +1785,9 @@ export namespace DocUtils { proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); } } + if (Upload.isVideoInformation(result)) { + proto['data-duration'] = result.duration; + } generatedDocuments.push(doc); } } @@ -1763,10 +1822,27 @@ export namespace DocUtils { source: { name, type }, result, } of await Networking.UploadYoutubeToServer(videoId)) { - name && type && processFileupload(generatedDocuments, name, type, result, options); + name && processFileupload(generatedDocuments, name, type, result, options); } return generatedDocuments; } + + export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) { + const generatedDocuments: Doc[] = []; + Networking.UploadYoutubeToServer(videoId).then(upfiles => { + const { + source: { name, type }, + result, + } = upfiles.lastElement(); + if ((result as any).message) { + if (overwriteDoc) { + overwriteDoc.isLoading = false; + overwriteDoc.errorMessage = (result as any).message; + } + } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); + }); + } + export async function uploadFilesToDocs(files: File[], options: DocumentOptions) { const generatedDocuments: Doc[] = []; const upfiles = await Networking.UploadFilesToServer(files); @@ -1779,6 +1855,22 @@ export namespace DocUtils { return generatedDocuments; } + export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) { + const generatedDocuments: Doc[] = []; + Networking.UploadFilesToServer([file]).then(upfiles => { + const { + source: { name, type }, + result, + } = upfiles.lastElement(); + if ((result as any).message) { + if (overwriteDoc) { + overwriteDoc.isLoading = false; + overwriteDoc.errorMessage = (result as any).message; + } + } else name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); + }); + } + // copies the specified drag factory document export function copyDragFactory(dragFactory: Doc) { if (!dragFactory) return undefined; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index aaad1392f..6283337e0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -2,6 +2,7 @@ import { forOwn } from "lodash"; import { reaction } from "mobx"; import * as rp from 'request-promise'; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; import { RichTextField } from "../../fields/RichTextField"; @@ -15,6 +16,7 @@ import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents"; import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; import { TreeViewType } from "../views/collections/CollectionTreeView"; +import { DashboardView } from "../views/DashboardView"; import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox"; @@ -96,6 +98,44 @@ export class CurrentUserUtils { return DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs); } + /// Initializes templates for editing click funcs of a document + static setupChildClickEditors(doc: Doc, field = "clickFuncs-child") { + const tempClicks = DocCast(doc[field]); + const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true}; + const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ + { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"}, + { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}]; + const reqdClickList = reqdTempOpts.map(opts => { + const allOpts = {...reqdClickOpts, ...opts.opts}; + const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; + return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, allOpts),allOpts); + }); + + const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true}; + return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); + } + + /// Initializes templates for editing click funcs of a document + static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") { + const tempClicks = DocCast(doc[field]); + const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true}; + const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ + { opts: { title: "onClick"}, script: "console.log( 'click')"}, + { opts: { title: "onDoubleClick"}, script: "console.log( 'double click')"}, + { opts: { title: "onChildClick"}, script: "console.log( 'child click')"}, + { opts: { title: "onChildDoubleClick"}, script: "console.log( 'child double click')"}, + { opts: { title: "onCheckedClick"}, script: "console.log( heading, checked, containingTreeView)"}, + ]; + const reqdClickList = reqdTempOpts.map(opts => { + const allOpts = {...reqdClickOpts, ...opts.opts}; + const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; + return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title); + }); + + const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, system: true}; + return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); + } + /// Initializes templates that can be applied to notes static setupNoteTemplates(doc: Doc, field="template-notes") { const tempNotes = DocCast(doc[field]); @@ -104,7 +144,7 @@ export class CurrentUserUtils { { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" }, { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; const reqdNoteList = reqdTempOpts.map(opts => { - const reqdOpts = {...opts, title: "text", system: true}; + const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true, system: true}; const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined; return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); }); @@ -117,10 +157,10 @@ export class CurrentUserUtils { static setupDocTemplates(doc: Doc, field="myTemplates") { DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"}); const templates = [ - DocCast(doc.presElement), CurrentUserUtils.setupNoteTemplates(doc), CurrentUserUtils.setupClickEditorTemplates(doc) ]; + CurrentUserUtils.setupChildClickEditors(doc) const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); @@ -157,6 +197,7 @@ export class CurrentUserUtils { makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _showTitle: "creationDate"}), makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), + makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.BUTTON,"data", { iconTemplate:DocumentType.FONTICON}), //nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now @@ -196,9 +237,9 @@ export class CurrentUserUtils { const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text", layout: "<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" + - ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` + - " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" + - ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + + ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` + + " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" + + ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + "</HTMLdiv>" }, "header"); @@ -215,6 +256,7 @@ export class CurrentUserUtils { creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist }[] = [ {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true }}, + {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200 }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100 }}, {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _fitWidth:false, _backgroundGridShow: true, }}, {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }}, @@ -227,7 +269,7 @@ export class CurrentUserUtils { {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, // {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}}, - {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }}, + {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _chromeHidden: true, boxShadow: "0 0" }}, {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, @@ -240,6 +282,7 @@ export class CurrentUserUtils { return [ { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, }, + { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, }, { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, }, { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, }, { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, }, @@ -248,11 +291,12 @@ export class CurrentUserUtils { { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, }, { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} }, { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}}, - { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} }, - { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}}, + { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} }, + { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}}, { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, }, - { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, scripts: {onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, }, - { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: {onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } }, + { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, funcs: { hidden: 'IsNoviceMode()'}}, + { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc,scripts: { onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, }, + { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: { onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } }, ].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, })) } @@ -288,8 +332,13 @@ export class CurrentUserUtils { { title: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", funcs: {hidden: "IsNoviceMode()"} }, { title: "Imports", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", }, { title: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", }, +<<<<<<< HEAD { title: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs:{badgeValue:badgeValue}}, { title: "Trails", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}}, +======= + { title: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs: {badgeValue:badgeValue}}, + { title: "Trails", target: this.setupTrails(doc, "myTrails"), icon: "pres-trail", }, +>>>>>>> master { title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}})); @@ -574,14 +623,17 @@ export class CurrentUserUtils { /// initializes the required buttons in the expanding button menu at the bottom of the Dash window static setupDockedButtons(doc: Doc, field="myDockedBtns") { const dockedBtns = DocCast(doc[field]); - const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}) => + const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}, funcs?: {[key:string]:string}) => DocUtils.AssignScripts(DocUtils.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ?? - CurrentUserUtils.createToolButton(opts), scripts); + CurrentUserUtils.createToolButton(opts), scripts, funcs); const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }}, - { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }} - ]; + { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }}, + // { scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, opts: { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, width: 20}}, + // { scripts: { onClick:""}, opts: { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, width: 20}, funcs: { buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}}, + // { scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, opts:{title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, width: 20,} }, + ]; const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts)); const dockBtnsReqdOpts = { title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true, @@ -596,8 +648,8 @@ export class CurrentUserUtils { return [ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, scripts: {script: 'setFont(value, _readOnly_)'}, btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, - { title: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions }, - { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts: {script: '{ return setFontColor(value, _readOnly_); }'}}, + { title: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions }, + { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts: {script: '{ return setFontColor(value, _readOnly_);}'}}, { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", scripts: {onClick: '{ return toggleBold(_readOnly_); }'} }, { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} }, { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} }, @@ -610,7 +662,8 @@ export class CurrentUserUtils { { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }}, { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} }, { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} }, - { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}}, + { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}}, + { title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}}, ]; } @@ -622,8 +675,9 @@ export class CurrentUserUtils { // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} }, { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:'{ return setActiveTool("circle", _readOnly_);}'} }, // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveTool("square")' }, - { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick: '{ return setActiveTool("line", _readOnly_);}' }}, - { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: "{ return setFillColor(value, _readOnly_);}"} }, + { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick:'{ return setActiveTool("line", _readOnly_);}' }}, + { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} }, + { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} }, { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1}, { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} }, ]; @@ -648,12 +702,14 @@ export class CurrentUserUtils { CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, - CollectionViewType.Grid]), + CollectionViewType.Grid, CollectionViewType.NoteTaking]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, + { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_)'}}, { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, + { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}, width: 20, scripts: {}}, { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, - { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected - { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'setHeaderColor(value, _readOnly_)'}}, + { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected + { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform { title: "Text", icon: "text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available { title: "Ink", icon: "ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', inearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`} }, // Always available @@ -766,54 +822,6 @@ export class CurrentUserUtils { DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); return myImports; } - - static setupClickEditorTemplates(doc: Doc) { - if (doc["clickFuncs-child"] === undefined) { - // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target - const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript( - "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self]))) ", - { thisContainer: Doc.name }), { - title: "Click to open in target", _width: 300, _height: 200, - targetScriptKey: "onChildClick", system: true - }); - - const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript( "openOnRight(self.doubleClickView)", {}), - { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick", system: true }); - - doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates", system: true }); - } - // this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved. - PromiseValue(Cast(doc["clickFuncs-child"], Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); - - if (doc.clickFuncs === undefined) { - const onClick = Docs.Create.ScriptingDocument(undefined, { - title: "onClick", "onClick-rawScript": "console.log('click')", - isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200, system: true - }, "onClick"); - const onChildClick = Docs.Create.ScriptingDocument(undefined, { - title: "onChildClick", "onChildClick-rawScript": "console.log('child click')", - isTemplateDoc: true, isTemplateForField: "onChildClick", _width: 300, _height: 200, system: true - }, "onChildClick"); - const onDoubleClick = Docs.Create.ScriptingDocument(undefined, { - title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')", - isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200, system: true - }, "onDoubleClick"); - const onChildDoubleClick = Docs.Create.ScriptingDocument(undefined, { - title: "onChildDoubleClick", "onChildDoubleClick-rawScript": "console.log('child double click')", - isTemplateDoc: true, isTemplateForField: "onChildDoubleClick", _width: 300, _height: 200, system: true - }, "onChildDoubleClick"); - const onCheckedClick = Docs.Create.ScriptingDocument(undefined, { - title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", - "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, - isTemplateForField: "onCheckedClick", _width: 300, _height: 200, system: true - }, "onCheckedClick"); - doc.clickFuncs = Docs.Create.TreeDocument([onClick, onChildClick, onDoubleClick, onCheckedClick], { title: "onClick funcs", system: true }); - } - PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); - - return doc.clickFuncs as Doc; - } - /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be /// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field /// whether to revert to "default" values, or to leave them as the user/system last set them. @@ -895,7 +903,14 @@ export class CurrentUserUtils { Doc.CurrentUserEmail = result.email; resolvedPorts = JSON.parse(await (await fetch("/resolvedPorts")).text()); DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email); - result.cacheDocumentIds && (await DocServer.GetRefFields(result.cacheDocumentIds.split(";"))); + if (result.cacheDocumentIds) + { + const ids = result.cacheDocumentIds.split(";"); + const batch = 10000; + for (let i = 0; i < ids.length; i = Math.min(ids.length, i+batch)) { + await DocServer.GetRefFields(ids.slice(i, i+batch)); + } + } return result; } else { throw new Error("There should be a user! Why does Dash think there isn't one?"); @@ -906,13 +921,20 @@ export class CurrentUserUtils { public static async loadUserDocument(id: string) { await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => { const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids); - if (userDocumentId !== "guest") { + if (userDocumentId) { return DocServer.GetRefField(userDocumentId).then(async field => { Docs.newAccount = !(field instanceof Doc); await Docs.Prototypes.initialize(); const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc; - Docs.newAccount &&(userDoc.activePage = "home"); - return this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId); + this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId); + if (Docs.newAccount) { + if (Doc.CurrentUserEmail === "guest") { + DashboardView.createNewDashboard(undefined, "guest dashboard"); + } else { + userDoc.activePage = "home"; + } + } + return userDoc; }); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index a6dcda4bc..0a61f3478 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -1,37 +1,35 @@ -import * as interpreter from "words-to-numbers"; +import * as interpreter from 'words-to-numbers'; // @ts-ignore bcz: how are you supposed to include these definitions since dom-speech-recognition isn't a module? -import type { } from "@types/dom-speech-recognition"; -import { Doc, Opt } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { RichTextField } from "../../fields/RichTextField"; -import { listSpec } from "../../fields/Schema"; -import { Cast, CastCtor } from "../../fields/Types"; -import { AudioField, ImageField } from "../../fields/URLField"; -import { Utils } from "../../Utils"; -import { Docs } from "../documents/Documents"; -import { DocumentType } from "../documents/DocumentTypes"; -import { DictationOverlay } from "../views/DictationOverlay"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { SelectionManager } from "./SelectionManager"; -import { UndoManager } from "./UndoManager"; - +import type {} from '@types/dom-speech-recognition'; +import { Doc, Opt } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { RichTextField } from '../../fields/RichTextField'; +import { listSpec } from '../../fields/Schema'; +import { Cast, CastCtor } from '../../fields/Types'; +import { AudioField, ImageField } from '../../fields/URLField'; +import { Utils } from '../../Utils'; +import { Docs } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; +import { DictationOverlay } from '../views/DictationOverlay'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { SelectionManager } from './SelectionManager'; +import { UndoManager } from './UndoManager'; /** * This namespace provides a singleton instance of a manager that * handles the listening and text-conversion of user speech. - * + * * The basic manager functionality can be attained by the DictationManager.Controls namespace, which provide * a simple recording operation that returns the interpreted text as a string. - * + * * Additionally, however, the DictationManager also exposes the ability to execute voice commands within Dash. * It stores a default library of registered commands that can be triggered by listen()'ing for a phrase and then * passing the results into the execute() function. - * + * * In addition to compile-time default commands, you can invoke DictationManager.Commands.Register(Independent|Dependent) * to add new commands as classes or components are constructed. */ export namespace DictationManager { - /** * Some type maneuvering to access Webkit's built-in * speech recognizer. @@ -42,27 +40,26 @@ export namespace DictationManager { } } const { webkitSpeechRecognition }: CORE.IWindow = window as any as CORE.IWindow; - export const placeholder = "Listening..."; + export const placeholder = 'Listening...'; export namespace Controls { - - export const Infringed = "unable to process: dictation manager still involved in previous session"; + export const Infringed = 'unable to process: dictation manager still involved in previous session'; const browser = (() => { const identifier = navigator.userAgent.toLowerCase(); - if (identifier.indexOf("safari") >= 0) { - return "Safari"; + if (identifier.indexOf('safari') >= 0) { + return 'Safari'; } - if (identifier.indexOf("chrome") >= 0) { - return "Chrome"; + if (identifier.indexOf('chrome') >= 0) { + return 'Chrome'; } - if (identifier.indexOf("firefox") >= 0) { - return "Firefox"; + if (identifier.indexOf('firefox') >= 0) { + return 'Firefox'; } - return "Unidentified Browser"; + return 'Unidentified Browser'; })(); const unsupported = `listening is not supported in ${browser}`; - const intraSession = ". "; - const interSession = " ... "; + const intraSession = '. '; + const interSession = ' ... '; export let isListening = false; let isManuallyStopped = false; @@ -74,7 +71,7 @@ export namespace DictationManager { export type InterimResultHandler = (results: string) => any; export type ContinuityArgs = { indefinite: boolean } | false; - export type DelimiterArgs = { inter: string, intra: string }; + export type DelimiterArgs = { inter: string; intra: string }; export type ListeningUIStatus = { interim: boolean } | false; export interface ListeningOptions { @@ -105,21 +102,21 @@ export namespace DictationManager { try { results = await (pendingListen = listenImpl(options)); pendingListen = undefined; - // if (results) { - // Utils.CopyText(results); - // if (overlay) { - // DictationOverlay.Instance.isListening = false; - // const execute = options?.tryExecute; - // DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results; - // DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; - // } - // options?.tryExecute && await DictationManager.Commands.execute(results); - // } + if (results) { + Utils.CopyText(results); + if (overlay) { + DictationOverlay.Instance.isListening = false; + const execute = options?.tryExecute; + DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results; + DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; + } + options?.tryExecute && (await DictationManager.Commands.execute(results)); + } } catch (e: any) { console.log(e); if (overlay) { DictationOverlay.Instance.isListening = false; - DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`; + DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${'error' in e ? e.error : 'unknown error'}`; DictationOverlay.Instance.dictationSuccess = false; } } finally { @@ -131,7 +128,7 @@ export namespace DictationManager { const listenImpl = (options?: Partial<ListeningOptions>) => { if (!recognizer) { - console.log("DictationManager:" + unsupported); + console.log('DictationManager:' + unsupported); return unsupported; } if (isListening) { @@ -146,16 +143,17 @@ export namespace DictationManager { const intra = options?.delimiters?.intra; const inter = options?.delimiters?.inter; - recognizer.onstart = () => console.log("initiating speech recognition session..."); + recognizer.onstart = () => console.log('initiating speech recognition session...'); recognizer.interimResults = handler !== undefined; recognizer.continuous = continuous === undefined ? false : continuous !== false; - recognizer.lang = language === undefined ? "en-US" : language; + recognizer.lang = language === undefined ? 'en-US' : language; recognizer.start(); return new Promise<string>((resolve, reject) => { - recognizer.onerror = (e: any) => { // e is SpeechRecognitionError but where is that defined? - if (!(indefinite && e.error === "no-speech")) { + recognizer.onerror = (e: any) => { + // e is SpeechRecognitionError but where is that defined? + if (!(indefinite && e.error === 'no-speech')) { recognizer.stop(); resolve(e); //reject(e); @@ -165,7 +163,7 @@ export namespace DictationManager { recognizer.onresult = (e: SpeechRecognitionEvent) => { current = synthesize(e, intra); let matchedTerminator: string | undefined; - if (options?.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) { + if (options?.terminators && (matchedTerminator = options.terminators.find(end => (current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false)))) { current = matchedTerminator; recognizer.abort(); return complete(); @@ -191,7 +189,7 @@ export namespace DictationManager { current && sessionResults.push(current); sessionResults.length && resolve(sessionResults.join(inter || interSession)); } else { - resolve(current || ""); + resolve(current || ''); } current = undefined; sessionResults = []; @@ -201,7 +199,6 @@ export namespace DictationManager { recognizer.onerror = null; recognizer.onend = null; }; - }); }; @@ -222,171 +219,173 @@ export namespace DictationManager { } return transcripts.join(delimiter || intraSession); }; - } - // export namespace Commands { - - // export const dictationFadeDuration = 2000; - - // export type IndependentAction = (target: DocumentView) => any | Promise<any>; - // export type IndependentEntry = { action: IndependentAction, restrictTo?: DocumentType[] }; - - // export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise<any>; - // export type DependentEntry = { expression: RegExp, action: DependentAction, restrictTo?: DocumentType[] }; - - // export const RegisterIndependent = (key: string, value: IndependentEntry) => Independent.set(key, value); - // export const RegisterDependent = (entry: DependentEntry) => Dependent.push(entry); - - // export const execute = async (phrase: string) => { - // return UndoManager.RunInBatch(async () => { - // const targets = SelectionManager.Views(); - // if (!targets || !targets.length) { - // return; - // } - - // phrase = phrase.toLowerCase(); - // const entry = Independent.get(phrase); - - // if (entry) { - // let success = false; - // const restrictTo = entry.restrictTo; - // for (const target of targets) { - // if (!restrictTo || validate(target, restrictTo)) { - // await entry.action(target); - // success = true; - // } - // } - // return success; - // } - - // for (const entry of Dependent) { - // const regex = entry.expression; - // const matches = regex.exec(phrase); - // regex.lastIndex = 0; - // if (matches !== null) { - // let success = false; - // const restrictTo = entry.restrictTo; - // for (const target of targets) { - // if (!restrictTo || validate(target, restrictTo)) { - // await entry.action(target, matches); - // success = true; - // } - // } - // return success; - // } - // } - - // return false; - // }, "Execute Command"); - // }; - - // const ConstructorMap = new Map<DocumentType, CastCtor>([ - // [DocumentType.COL, listSpec(Doc)], - // [DocumentType.AUDIO, AudioField], - // [DocumentType.IMG, ImageField], - // [DocumentType.IMPORT, listSpec(Doc)], - // [DocumentType.RTF, "string"] - // ]); - - // const tryCast = (view: DocumentView, type: DocumentType) => { - // const ctor = ConstructorMap.get(type); - // if (!ctor) { - // return false; - // } - // return Cast(Doc.GetProto(view.props.Document).data, ctor) !== undefined; - // }; - - // const validate = (target: DocumentView, types: DocumentType[]) => { - // for (const type of types) { - // if (tryCast(target, type)) { - // return true; - // } - // } - // return false; - // }; - - // const interpretNumber = (number: string) => { - // const initial = parseInt(number); - // if (!isNaN(initial)) { - // return initial; - // } - // const converted = interpreter.wordsToNumbers(number, { fuzzy: true }); - // if (converted === null) { - // return NaN; - // } - // return typeof converted === "string" ? parseInt(converted) : converted; - // }; - - // const Independent = new Map<string, IndependentEntry>([ - - // ["clear", { - // action: (target: DocumentView) => Doc.GetProto(target.props.Document).data = new List(), - // restrictTo: [DocumentType.COL] - // }], - - // ["open fields", { - // action: (target: DocumentView) => { - // const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); - // target.props.addDocTab(kvp, "add:right"); - // } - // }], - - // ["new outline", { - // action: (target: DocumentView) => { - // const newBox = Docs.Create.TextDocument("", { _width: 400, _height: 200, title: "My Outline", _autoHeight: true }); - // const proto = newBox.proto!; - // const prompt = "Press alt + r to start dictating here..."; - // const head = 3; - // const anchor = head + prompt.length; - // const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; - // proto.data = new RichTextField(proseMirrorState); - // proto.backgroundColor = "#eeffff"; - // target.props.addDocTab(newBox, "add:right"); - // } - // }] - - // ]); - - // const Dependent = new Array<DependentEntry>( - - // { - // expression: /create (\w+) documents of type (image|nested collection)/g, - // action: (target: DocumentView, matches: RegExpExecArray) => { - // const count = interpretNumber(matches[1]); - // const what = matches[2]; - // const dataDoc = Doc.GetProto(target.props.Document); - // const fieldKey = "data"; - // if (isNaN(count)) { - // return; - // } - // for (let i = 0; i < count; i++) { - // let created: Doc | undefined; - // switch (what) { - // case "image": - // created = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"); - // break; - // case "nested collection": - // created = Docs.Create.FreeformDocument([], {}); - // break; - // } - // created && Doc.AddDocToList(dataDoc, fieldKey, created); - // } - // }, - // restrictTo: [DocumentType.COL] - // }, - - // { - // expression: /view as (freeform|stacking|masonry|schema|tree)/g, - // action: (target: DocumentView, matches: RegExpExecArray) => { - // const mode = matches[1]; - // mode && (target.props.Document._viewType = mode); - // }, - // restrictTo: [DocumentType.COL] - // } - - // ); - - // } - -}
\ No newline at end of file + export namespace Commands { + export const dictationFadeDuration = 2000; + + export type IndependentAction = (target: DocumentView) => any | Promise<any>; + export type IndependentEntry = { action: IndependentAction; restrictTo?: DocumentType[] }; + + export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise<any>; + export type DependentEntry = { expression: RegExp; action: DependentAction; restrictTo?: DocumentType[] }; + + export const RegisterIndependent = (key: string, value: IndependentEntry) => Independent.set(key, value); + export const RegisterDependent = (entry: DependentEntry) => Dependent.push(entry); + + export const execute = async (phrase: string) => { + return UndoManager.RunInBatch(async () => { + console.log('PHRASE: ' + phrase); + const targets = SelectionManager.Views(); + if (!targets || !targets.length) { + return; + } + + phrase = phrase.toLowerCase(); + const entry = Independent.get(phrase); + + if (entry) { + let success = false; + const restrictTo = entry.restrictTo; + for (const target of targets) { + if (!restrictTo || validate(target, restrictTo)) { + await entry.action(target); + success = true; + } + } + return success; + } + + for (const entry of Dependent) { + const regex = entry.expression; + const matches = regex.exec(phrase); + regex.lastIndex = 0; + if (matches !== null) { + let success = false; + const restrictTo = entry.restrictTo; + for (const target of targets) { + if (!restrictTo || validate(target, restrictTo)) { + await entry.action(target, matches); + success = true; + } + } + return success; + } + } + + return false; + }, 'Execute Command'); + }; + + const ConstructorMap = new Map<DocumentType, CastCtor>([ + [DocumentType.COL, listSpec(Doc)], + [DocumentType.AUDIO, AudioField], + [DocumentType.IMG, ImageField], + [DocumentType.IMPORT, listSpec(Doc)], + [DocumentType.RTF, 'string'], + ]); + + const tryCast = (view: DocumentView, type: DocumentType) => { + const ctor = ConstructorMap.get(type); + if (!ctor) { + return false; + } + return Cast(Doc.GetProto(view.props.Document).data, ctor) !== undefined; + }; + + const validate = (target: DocumentView, types: DocumentType[]) => { + for (const type of types) { + if (tryCast(target, type)) { + return true; + } + } + return false; + }; + + const interpretNumber = (number: string) => { + const initial = parseInt(number); + if (!isNaN(initial)) { + return initial; + } + const converted = interpreter.wordsToNumbers(number, { fuzzy: true }); + if (converted === null) { + return NaN; + } + return typeof converted === 'string' ? parseInt(converted) : converted; + }; + + const Independent = new Map<string, IndependentEntry>([ + [ + 'clear', + { + action: (target: DocumentView) => (Doc.GetProto(target.props.Document).data = new List()), + restrictTo: [DocumentType.COL], + }, + ], + + [ + 'open fields', + { + action: (target: DocumentView) => { + const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); + target.props.addDocTab(kvp, 'add:right'); + }, + }, + ], + + [ + 'new outline', + { + action: (target: DocumentView) => { + const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _autoHeight: true }); + const proto = newBox.proto!; + const prompt = 'Press alt + r to start dictating here...'; + const head = 3; + const anchor = head + prompt.length; + const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; + proto.data = new RichTextField(proseMirrorState); + proto.backgroundColor = '#eeffff'; + target.props.addDocTab(newBox, 'add:right'); + }, + }, + ], + ]); + + const Dependent = new Array<DependentEntry>( + { + expression: /create (\w+) documents of type (image|nested collection)/g, + action: (target: DocumentView, matches: RegExpExecArray) => { + const count = interpretNumber(matches[1]); + const what = matches[2]; + const dataDoc = Doc.GetProto(target.props.Document); + const fieldKey = 'data'; + if (isNaN(count)) { + return; + } + for (let i = 0; i < count; i++) { + let created: Doc | undefined; + switch (what) { + case 'image': + created = Docs.Create.ImageDocument('https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg'); + break; + case 'nested collection': + created = Docs.Create.FreeformDocument([], {}); + break; + } + created && Doc.AddDocToList(dataDoc, fieldKey, created); + } + }, + restrictTo: [DocumentType.COL], + }, + + { + expression: /view as (freeform|stacking|masonry|schema|tree)/g, + action: (target: DocumentView, matches: RegExpExecArray) => { + const mode = matches[1]; + mode && (target.props.Document._viewType = mode); + }, + restrictTo: [DocumentType.COL], + } + ); + } +} diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index d3ac2f03f..2ca5d1095 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -12,6 +12,9 @@ import { CollectionFreeFormView } from '../views/collections/collectionFreeForm' import { CollectionView } from '../views/collections/CollectionView'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; +import { listSpec } from '../../fields/Schema'; +import { AudioField } from '../../fields/URLField'; +const { Howl } = require('howler'); export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @@ -151,7 +154,7 @@ export class DocumentManager { CollectionDockingView.AddSplit(doc, 'right'); finished?.(); }; - public jumpToDocument = async ( + public jumpToDocument = ( targetDoc: Doc, // document to display willZoom: boolean, // whether to zoom doc to take up most of screen createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist @@ -162,8 +165,8 @@ export class DocumentManager { finished?: () => void, originalTarget?: Doc, noSelect?: boolean, - presZoom?: number - ): Promise<void> => { + presZoomScale?: number + ): void => { originalTarget = originalTarget ?? targetDoc; const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; const docView = getFirstDocView(targetDoc, originatingDoc); @@ -186,11 +189,25 @@ export class DocumentManager { } else { finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined); !noSelect && docView?.select(false); + if (originatingDoc?.followLinkAudio) { + const anno = Cast(finalTargetDoc[Doc.LayoutFieldKey(finalTargetDoc) + '-audioAnnotations'], listSpec(AudioField), null).lastElement(); + if (anno) { + if (anno instanceof AudioField) { + new Howl({ + src: [anno.url.href], + format: ['mp3'], + autoplay: true, + loop: false, + volume: 0.5, + }); + } + } + } } finished?.(); }; const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc); - const contextDocs = docContext.length ? await DocListCastAsync(docContext[0].data) : undefined; + const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined; const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined; const targetDocContext = contextDoc || annotatedDoc; const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above @@ -201,7 +218,7 @@ export class DocumentManager { annoContainerView.focus(targetDoc, { originalTarget, willZoom, - scale: presZoom, + scale: presZoomScale, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { focusAndFinish(true); @@ -220,7 +237,7 @@ export class DocumentManager { focusView.focus(originalTarget ?? targetDoc, { originalTarget, willZoom, - scale: presZoom, + scale: presZoomScale, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { focusAndFinish(forceDidFocus || didFocus); @@ -248,7 +265,9 @@ export class DocumentManager { afterFocus: async () => { targetDocContext._viewTransition = undefined; if (targetDocContext.layoutKey === 'layout_icon') { - targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoom)); + targetDocContextView.iconify(() => + this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale) + ); } return ViewAdjustment.doNothing; }, @@ -292,7 +311,7 @@ export class DocumentManager { const docContextView = this.getFirstDocumentView(docContext[0]); if (docContextView) { return docContextView.iconify(() => - this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoom) + this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale) ); } } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 09b463c2f..664933de0 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,41 +1,35 @@ -import { action } from "mobx"; -import { DateField } from "../../fields/DateField"; -import { Doc, Field, Opt } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { PrefetchProxy } from "../../fields/Proxy"; -import { listSpec } from "../../fields/Schema"; -import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; -import { ScriptField } from "../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types"; -import { emptyFunction, Utils } from "../../Utils"; -import { Docs, DocUtils } from "../documents/Documents"; -import * as globalCssVariables from "../views/global/globalCssVariables.scss"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { SnappingManager } from "./SnappingManager"; -import { UndoManager } from "./UndoManager"; - -export type dropActionType = "alias" | "copy" | "move" | "same" | "proto" | "none" | undefined; // undefined = move, "same" = move but don't call removeDropProperties +import { action, observable, runInAction } from 'mobx'; +import { DateField } from '../../fields/DateField'; +import { Doc, Field, Opt } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { PrefetchProxy } from '../../fields/Proxy'; +import { listSpec } from '../../fields/Schema'; +import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; +import { ScriptField } from '../../fields/ScriptField'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; +import { emptyFunction, Utils } from '../../Utils'; +import { Docs, DocUtils } from '../documents/Documents'; +import * as globalCssVariables from '../views/global/globalCssVariables.scss'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { SnappingManager } from './SnappingManager'; +import { UndoManager } from './UndoManager'; + +export type dropActionType = 'alias' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call removeDropProperties /** * Initialize drag - * @param _reference: The HTMLElement that is being dragged + * @param _reference: The HTMLElement that is being dragged * @param docFunc: The Dash document being moved * @param moveFunc: The function called when the document is moved * @param dropAction: What to do with the document when it is dropped * @param dragStarted: Method to call when the drag is started */ -export function SetupDrag( - _reference: React.RefObject<HTMLElement>, - docFunc: () => Doc | Promise<Doc> | undefined, - moveFunc?: DragManager.MoveFunction, - dropAction?: dropActionType, - dragStarted?: () => void -) { +export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) { const onRowMove = async (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointermove', onRowMove); document.removeEventListener('pointerup', onRowUp); const doc = await docFunc(); if (doc) { @@ -47,7 +41,7 @@ export function SetupDrag( } }; const onRowUp = (): void => { - document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointermove', onRowMove); document.removeEventListener('pointerup', onRowUp); }; const onItemDown = async (e: React.PointerEvent) => { @@ -58,8 +52,8 @@ export function SetupDrag( const dragDoc = await docFunc(); dragDoc && DragManager.StartWindowDrag?.(e, [dragDoc]); } else { - document.addEventListener("pointermove", onRowMove); - document.addEventListener("pointerup", onRowUp); + document.addEventListener('pointermove', onRowMove); + document.addEventListener('pointerup', onRowUp); } } }; @@ -69,15 +63,19 @@ export function SetupDrag( export namespace DragManager { let dragDiv: HTMLDivElement; let dragLabel: HTMLDivElement; - export let StartWindowDrag: Opt<((e: { pageX: number, pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => void)>; + export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => void>; export let CompleteWindowDrag: Opt<(aborted: boolean) => void>; - export function GetRaiseWhenDragged() { return BoolCast(Doc.UserDoc()._raiseWhenDragged); } - export function SetRaiseWhenDragged(val:boolean) { Doc.UserDoc()._raiseWhenDragged = val } + export function GetRaiseWhenDragged() { + return BoolCast(Doc.UserDoc()._raiseWhenDragged); + } + export function SetRaiseWhenDragged(val: boolean) { + Doc.UserDoc()._raiseWhenDragged = val; + } export function Root() { - const root = document.getElementById("root"); + const root = document.getElementById('root'); if (!root) { - throw new Error("No root element found"); + throw new Error('No root element found'); } return root; } @@ -85,27 +83,20 @@ export namespace DragManager { export type MoveFunction = (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; export type RemoveFunction = (document: Doc | Doc[]) => boolean; - export interface DragDropDisposer { (): void; } + export interface DragDropDisposer { + (): void; + } export interface DragOptions { dragComplete?: (e: DragCompleteEvent) => void; // function to invoke when drag has completed - hideSource?: boolean; // hide source document during drag - offsetX?: number; // offset of top left of source drag visual from cursor + hideSource?: boolean; // hide source document during drag + offsetX?: number; // offset of top left of source drag visual from cursor offsetY?: number; noAutoscroll?: boolean; } // event called when the drag operation results in a drop action export class DropEvent { - constructor( - readonly x: number, - readonly y: number, - readonly complete: DragCompleteEvent, - readonly shiftKey: boolean, - readonly altKey: boolean, - readonly metaKey: boolean, - readonly ctrlKey: boolean, - readonly embedKey: boolean, - ) { } + constructor(readonly x: number, readonly y: number, readonly complete: DragCompleteEvent, readonly shiftKey: boolean, readonly altKey: boolean, readonly metaKey: boolean, readonly ctrlKey: boolean, readonly embedKey: boolean) {} } // event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated @@ -137,20 +128,22 @@ export namespace DragManager { treeViewDoc?: Doc; offset: number[]; canEmbed?: boolean; - userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys - defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action - dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move' + userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys + defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action + dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move' removeDropProperties?: string[]; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts } export class LinkDragData { - constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc,) { + constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc) { this.linkDragView = dragView; this.linkSourceGetAnchor = linkSourceGetAnchor; } - get dragDocument() { return this.linkDragView.props.Document; } + get dragDocument() { + return this.linkDragView.props.Document; + } linkSourceGetAnchor: () => Doc; linkSourceDoc?: Doc; linkDragView: DocumentView; @@ -177,43 +170,29 @@ export namespace DragManager { userDropAction: dropActionType; } - export function MakeDropTarget( - element: HTMLElement, - dropFunc: (e: Event, de: DropEvent) => void, - doc?: Doc, - preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void, - ): DragDropDisposer { - if ("canDrop" in element.dataset) { - throw new Error( - "Element is already droppable, can't make it droppable again" - ); + export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc?: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer { + if ('canDrop' in element.dataset) { + throw new Error("Element is already droppable, can't make it droppable again"); } - element.dataset.canDrop = "true"; + element.dataset.canDrop = 'true'; const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent<DropEvent>).detail; preDropFunc?.(e, de, StrCast(doc?.targetDropAction) as dropActionType); }; - element.addEventListener("dashOnDrop", handler); - doc && element.addEventListener("dashPreDrop", preDropHandler); + element.addEventListener('dashOnDrop', handler); + doc && element.addEventListener('dashPreDrop', preDropHandler); return () => { - element.removeEventListener("dashOnDrop", handler); - doc && element.removeEventListener("dashPreDrop", preDropHandler); + element.removeEventListener('dashOnDrop', handler); + doc && element.removeEventListener('dashPreDrop', preDropHandler); delete element.dataset.canDrop; }; } // drag a document and drop it (or make an alias/copy on drop) - export function StartDocumentDrag( - eles: HTMLElement[], - dragData: DocumentDragData, - downX: number, - downY: number, - options?: DragOptions, - dropEvent?: () => any - ) { + export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, dropEvent?: () => any) { const addAudioTag = (dropDoc: any) => { - dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField); + dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField()); dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); return dropDoc; }; @@ -222,19 +201,27 @@ export namespace DragManager { dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; - docDragData.droppedDocuments = - await Promise.all(dragData.draggedDocuments.map(async d => - !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? - addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : - docDragData.dropAction === "alias" ? Doc.MakeAlias(d) : - docDragData.dropAction === "proto" ? Doc.GetProto(d) : - docDragData.dropAction === "copy" ? - (await Doc.MakeClone(d)).clone : d)); - !["same", "proto"].includes(docDragData.dropAction as any) && docDragData.droppedDocuments.forEach((drop: Doc, i: number) => { - const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []); - const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps)); - remProps.map(prop => drop[prop] = undefined); - }); + docDragData.droppedDocuments = await Promise.all( + dragData.draggedDocuments.map(async d => + !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) + ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) + : docDragData.dropAction === 'alias' + ? Doc.MakeAlias(d) + : docDragData.dropAction === 'proto' + ? Doc.GetProto(d) + : docDragData.dropAction === 'copy' + ? ( + await Doc.MakeClone(d) + ).clone + : d + ) + ); + !['same', 'proto'].includes(docDragData.dropAction as any) && + docDragData.droppedDocuments.forEach((drop: Doc, i: number) => { + const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec('string'), []); + const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps)); + remProps.map(prop => (drop[prop] = undefined)); + }); } return e; }; @@ -243,23 +230,22 @@ export namespace DragManager { return true; } - // drag a button template and drop a new button - export function - StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { + // drag a button template and drop a new button + export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { const finishDrag = (e: DragCompleteEvent) => { const bd = Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) }); params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields initialize?.(bd); - Doc.GetProto(bd)["onClick-paramFieldKeys"] = new List<string>(params); + Doc.GetProto(bd)['onClick-paramFieldKeys'] = new List<string>(params); e.docDragData && (e.docDragData.droppedDocuments = [bd]); return e; }; options = options ?? {}; - options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate + options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag); } - // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption + // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption export function StartAnchorAnnoDrag(eles: HTMLElement[], dragData: AnchorAnnoDragData, downX: number, downY: number, options?: DragOptions) { StartDrag(eles, dragData, downX, downY, options); } @@ -286,8 +272,8 @@ export namespace DragManager { let near = dragPt; const intersect = (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, dragx: number, dragy: number) => { if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return undefined; // Check if none of the lines are of length 0 - const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)); - if (denominator === 0) return undefined; // Lines are parallel + const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); + if (denominator === 0) return undefined; // Lines are parallel const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator; // let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator; @@ -315,14 +301,14 @@ export namespace DragManager { }); return { x: near[0], y: near[1] }; } - // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line + // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) { const snapThreshold = Utils.SNAP_THRESHOLD; const snapVal = (pts: number[], drag: number, snapLines: number[]) => { if (snapLines.length) { - const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt + const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt const rangePts = [drag - offs[0], drag - offs[1], drag - offs[2]]; // left, mid, right or top, mid, bottom pts to try to snap to snaplines - const closestPts = rangePts.map(pt => snapLines.reduce((nearest, curr) => Math.abs(nearest - pt) > Math.abs(curr - pt) ? curr : nearest)); + const closestPts = rangePts.map(pt => snapLines.reduce((nearest, curr) => (Math.abs(nearest - pt) > Math.abs(curr - pt) ? curr : nearest))); const closestDists = rangePts.map((pt, i) => Math.abs(pt - closestPts[i])); const minIndex = closestDists[0] < closestDists[1] && closestDists[0] < closestDists[2] ? 0 : closestDists[1] < closestDists[2] ? 1 : 2; return closestDists[minIndex] < snapThreshold ? closestPts[minIndex] + offs[minIndex] : drag; @@ -331,85 +317,122 @@ export namespace DragManager { }; return { x: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.vertSnapLines()), - y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()) + y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()), }; } - export let docsBeingDragged: Doc[] = []; + export let docsBeingDragged: Doc[] = observable([] as Doc[]); export let CanEmbed = false; export let DocDragData: DocumentDragData | undefined; export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { - if (dragData.dropAction === "none") return; + if (dragData.dropAction === 'none') return; DocDragData = dragData instanceof DocumentDragData ? dragData : undefined; - const batch = UndoManager.StartBatch("dragging"); + const batch = UndoManager.StartBatch('dragging'); eles = eles.filter(e => e); CanEmbed = dragData.canEmbed || false; if (!dragDiv) { - dragDiv = document.createElement("div"); - dragDiv.className = "dragManager-dragDiv"; - dragDiv.style.pointerEvents = "none"; - dragLabel = document.createElement("div"); - dragLabel.className = "dragManager-dragLabel"; - dragLabel.style.zIndex = "100001"; - dragLabel.style.fontSize = "10px"; - dragLabel.style.position = "absolute"; - dragLabel.innerText = "drag titlebar to embed on drop"; // bcz: need to move this to a status bar + dragDiv = document.createElement('div'); + dragDiv.className = 'dragManager-dragDiv'; + dragDiv.style.pointerEvents = 'none'; + dragLabel = document.createElement('div'); + dragLabel.className = 'dragManager-dragLabel'; + dragLabel.style.zIndex = '100001'; + dragLabel.style.fontSize = '10px'; + dragLabel.style.position = 'absolute'; + dragLabel.innerText = 'drag titlebar to embed on drop'; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); } - Object.assign(dragDiv.style, { width: "", height: "", overflow: "" }); + Object.assign(dragDiv.style, { width: '', height: '', overflow: '' }); dragDiv.hidden = false; - const scaleXs: number[] = [], scaleYs: number[] = [], xs: number[] = [], ys: number[] = []; + const scalings: number[] = [], + xs: number[] = [], + ys: number[] = []; - docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const elesCont = { - left: Number.MAX_SAFE_INTEGER, right: Number.MIN_SAFE_INTEGER, - top: Number.MAX_SAFE_INTEGER, bottom: Number.MIN_SAFE_INTEGER + left: Number.MAX_SAFE_INTEGER, + right: Number.MIN_SAFE_INTEGER, + top: Number.MAX_SAFE_INTEGER, + bottom: Number.MIN_SAFE_INTEGER, }; + let rot = 0; + const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const dragElements = eles.map(ele => { + let useDim = false; + if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') { + ele = ele.parentElement.parentElement.parentElement; + const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1'); + if (rotStr) rot = Number(rotStr); + } else { + useDim = true; + } + if (rot < 0) rot += 360; if (!ele.parentNode) dragDiv.appendChild(ele); - const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement; + const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement); const children = Array.from(dragElement.children); - while (children.length) { // need to replace all the maker node reference ids with new unique ids. otherwise, the clone nodes will reference the original nodes which are all hidden during the drag + while (children.length) { + // need to replace all the maker node reference ids with new unique ids. otherwise, the clone nodes will reference the original nodes which are all hidden during the drag const next = children.pop(); next && children.push(...Array.from(next.children)); if (next) { - ["marker-start", "marker-mid", "marker-end"].forEach(field => { - if (next.localName.startsWith("path") && (next.attributes as any)[field]) { - next.setAttribute(field, (next.attributes as any)[field].value.replace("#", "#X")); + ['marker-start', 'marker-mid', 'marker-end'].forEach(field => { + if (next.localName.startsWith('path') && (next.attributes as any)[field]) { + next.setAttribute(field, (next.attributes as any)[field].value.replace('#', '#X')); } }); - if (next.localName.startsWith("marker")) { - next.id = "X" + next.id; + if (next.localName.startsWith('marker')) { + next.id = 'X' + next.id; } } } const rect = ele.getBoundingClientRect(); - const scaleX = rect.width / (ele.offsetWidth || rect.width); - const scaleY = ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX; + const w = ele.offsetWidth || rect.width; + const h = ele.offsetHeight || rect.height; + const rotR = -(rot / 180) * Math.PI; + const tl = [0, 0]; + const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w]; + const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h]; + const br = [Math.cos(rotR) * w + Math.sin(rotR) * h, Math.cos(-rotR) * h - Math.sin(rotR) * w]; + const minx = Math.min(tl[0], tr[0], br[0], bl[0]); + const maxx = Math.max(tl[0], tr[0], br[0], bl[0]); + const miny = Math.min(tl[1], tr[1], br[1], bl[1]); + const maxy = Math.max(tl[1], tr[1], br[1], bl[1]); + const scaling = rect.width / (Math.abs(maxx - minx) || 1); elesCont.left = Math.min(rect.left, elesCont.left); elesCont.top = Math.min(rect.top, elesCont.top); elesCont.right = Math.max(rect.right, elesCont.right); elesCont.bottom = Math.max(rect.bottom, elesCont.bottom); - xs.push(rect.left); - ys.push(rect.top); - scaleXs.push(scaleX); - scaleYs.push(scaleY); + xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left); + ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top); + scalings.push(scaling); + const width = useDim ? getComputedStyle(ele).width : ''; + const height = useDim ? getComputedStyle(ele).height : ''; Object.assign(dragElement.style, { - opacity: "0.7", position: "absolute", margin: "0", top: "0", bottom: "", left: "0", color: "black", transition: "none", - borderRadius: getComputedStyle(ele).borderRadius, zIndex: globalCssVariables.contextMenuZindex, - transformOrigin: "0 0", width: `${rect.width / scaleX}px`, height: `${rect.height / scaleY}px`, - transform: `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`, + opacity: '0.7', + position: 'absolute', + margin: '0', + top: '0', + bottom: '', + left: '0', + color: 'black', + transition: 'none', + borderRadius: getComputedStyle(ele).borderRadius, + zIndex: globalCssVariables.contextMenuZindex, + transformOrigin: '0 0', + width, + height, + transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`, }); - dragLabel.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0) - 20}px)`; - - if (docsBeingDragged.length) { - const pdfBox = dragElement.getElementsByTagName("canvas"); - const pdfBoxSrc = ele.getElementsByTagName("canvas"); - Array.from(pdfBox).filter(pb => pb.width && pb.height).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); + dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; + + if (docsToDrag.length) { + const pdfBox = dragElement.getElementsByTagName('canvas'); + const pdfBoxSrc = ele.getElementsByTagName('canvas'); + Array.from(pdfBox) + .filter(pb => pb.width && pb.height) + .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); } - [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => - (ele as any).style && ((ele as any).style.pointerEvents = "none")); + [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none')); dragDiv.appendChild(dragElement); if (dragElement !== ele) { @@ -426,10 +449,12 @@ export namespace DragManager { return dragElement; }); + runInAction(() => docsBeingDragged.push(...docsToDrag)); + const hideDragShowOriginalElements = (hide: boolean) => { - dragLabel.style.display = hide ? "" : "none"; + dragLabel.style.display = hide ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); - eles.forEach(ele => ele.hidden = hide); + eles.forEach(ele => (ele.hidden = hide)); }; options?.hideSource && hideDragShowOriginalElements(true); @@ -443,32 +468,33 @@ export namespace DragManager { AbortDrag = () => { options?.dragComplete?.(new DragCompleteEvent(true, dragData)); - cleanupDrag(); + cleanupDrag(true); }; - const cleanupDrag = action(() => { + const cleanupDrag = action((undo: boolean) => { hideDragShowOriginalElements(false); - document.removeEventListener("pointermove", moveHandler, true); - document.removeEventListener("pointerup", upHandler); + document.removeEventListener('pointermove', moveHandler, true); + document.removeEventListener('pointerup', upHandler, true); SnappingManager.SetIsDragging(false); SnappingManager.clearSnapLines(); - batch.end(); + if (batch.end() && undo) UndoManager.Undo(); + docsBeingDragged.length = 0; }); var startWindowDragTimer: any; const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { - dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction; + dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.ctrlKey ? 'alias' : dragData.defaultDropAction; } - if (((e.target as any)?.className === "lm_tabs" || (e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) { + if (((e.target as any)?.className === 'lm_tabs' || (e.target as any)?.className === 'lm_header' || e?.shiftKey) && dragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { startWindowDragTimer = setTimeout(async () => { startWindowDragTimer = undefined; - dragData.dropAction = dragData.userDropAction || "same"; + dragData.dropAction = dragData.userDropAction || 'same'; AbortDrag(); await finishDrag?.(new DragCompleteEvent(true, dragData)); - DragManager.StartWindowDrag?.(e, dragData.droppedDocuments, (aborted) => { - if (!aborted && (dragData.dropAction === "move" || dragData.dropAction === "same")) { + DragManager.StartWindowDrag?.(e, dragData.droppedDocuments, aborted => { + if (!aborted && (dragData.dropAction === 'move' || dragData.dropAction === 'same')) { dragData.removeDocument?.(dragData.draggedDocuments[0]); } }); @@ -484,7 +510,7 @@ export namespace DragManager { if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) { const autoScrollHandler = () => { target.dispatchEvent( - new CustomEvent<React.DragEvent>("dashDragAutoScroll", { + new CustomEvent<React.DragEvent>('dashDragAutoScroll', { bubbles: true, detail: { shiftKey: e.shiftKey, @@ -493,7 +519,7 @@ export namespace DragManager { ctrlKey: e.ctrlKey, clientX: e.clientX, clientY: e.clientY, - dataTransfer: new DataTransfer, + dataTransfer: new DataTransfer(), button: e.button, buttons: e.buttons, getModifierState: e.getModifierState, @@ -505,8 +531,8 @@ export namespace DragManager { screenX: e.screenX, screenY: e.screenY, detail: e.detail, - view: e.view ? e.view : new Window as any, - nativeEvent: new DragEvent("dashDragAutoScroll"), + view: e.view ? e.view : (new Window() as any), + nativeEvent: new DragEvent('dashDragAutoScroll'), currentTarget: target, target: target, bubbles: true, @@ -514,14 +540,14 @@ export namespace DragManager { defaultPrevented: true, eventPhase: e.eventPhase, isTrusted: true, - preventDefault: () => "not implemented for this event" ? false : false, - isDefaultPrevented: () => "not implemented for this event" ? false : false, - stopPropagation: () => "not implemented for this event" ? false : false, - isPropagationStopped: () => "not implemented for this event" ? false : false, + preventDefault: () => ('not implemented for this event' ? false : false), + isDefaultPrevented: () => ('not implemented for this event' ? false : false), + stopPropagation: () => ('not implemented for this event' ? false : false), + isPropagationStopped: () => ('not implemented for this event' ? false : false), persist: emptyFunction, timeStamp: e.timeStamp, - type: "dashDragAutoScroll" - } + type: 'dashDragAutoScroll', + }, }) ); @@ -536,21 +562,19 @@ export namespace DragManager { const moveVec = { x: x - lastPt.x, y: y - lastPt.y }; lastPt = { x, y }; - dragLabel.style.transform = `translate(${xs[0] + moveVec.x + (options?.offsetX || 0)}px, ${ys[0] + moveVec.y + (options?.offsetY || 0) - 20}px)`; - dragElements.map((dragElement, i) => (dragElement.style.transform = - `translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) - ); + dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg) scale(${scalings[i]})`)); + dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; }; const upHandler = (e: PointerEvent) => { clearTimeout(startWindowDragTimer); startWindowDragTimer = undefined; - dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, cleanupDrag); + dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, () => cleanupDrag(false)); }; - document.addEventListener("pointermove", moveHandler, true); - document.addEventListener("pointerup", upHandler); + document.addEventListener('pointermove', moveHandler, true); + document.addEventListener('pointerup', upHandler, true); } - async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number, y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { + async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { const dropArgs = { bubbles: true, detail: { @@ -560,13 +584,13 @@ export namespace DragManager { altKey: e.altKey, metaKey: e.metaKey, ctrlKey: e.ctrlKey, - embedKey: CanEmbed - } + embedKey: CanEmbed, + }, }; - target.dispatchEvent(new CustomEvent<DropEvent>("dashPreDrop", dropArgs)); + target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); await finishDrag?.(complete); - target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", dropArgs)); + target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs)); options?.dragComplete?.(complete); endDrag?.(); } -}
\ No newline at end of file +} diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 256ab5c44..7c209d1e0 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,37 +1,41 @@ -import { DragManager } from "./DragManager"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { DocumentType } from "../documents/DocumentTypes"; -import { ObjectField } from "../../fields/ObjectField"; -import { StrCast, Cast } from "../../fields/Types"; -import { Docs } from "../documents/Documents"; -import { ScriptField, ComputedField } from "../../fields/ScriptField"; -import { RichTextField } from "../../fields/RichTextField"; -import { ImageField } from "../../fields/URLField"; -import { ScriptingGlobals } from "./ScriptingGlobals"; -import { listSpec } from "../../fields/Schema"; +import { DragManager } from './DragManager'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { DocumentType } from '../documents/DocumentTypes'; +import { ObjectField } from '../../fields/ObjectField'; +import { StrCast, Cast } from '../../fields/Types'; +import { Docs } from '../documents/Documents'; +import { ScriptField, ComputedField } from '../../fields/ScriptField'; +import { RichTextField } from '../../fields/RichTextField'; +import { ImageField } from '../../fields/URLField'; +import { ScriptingGlobals } from './ScriptingGlobals'; +import { listSpec } from '../../fields/Schema'; +import { ButtonType } from '../views/nodes/button/FontIconBox'; -export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = "") { +export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') { if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated doc.isTemplateDoc = makeTemplate(doc, first, rename); return doc; } -// +// // converts 'doc' into a template that can be used to render other documents. // the title of doc is used to determine which field is being templated, so -// passing a value for 'rename' allows the doc to be given a meangingful name +// passing a value for 'rename' allows the doc to be given a meangingful name // after it has been converted to function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; - if (layoutDoc.layout instanceof Doc) { // its already a template + if (layoutDoc.layout instanceof Doc) { + // its already a template return true; } const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0]; - const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, ""); + const fieldKey = layout.replace("fieldKey={'", '').replace(/'}$/, ''); const docs = DocListCast(layoutDoc[fieldKey]); let any = false; docs.forEach(d => { - if (!StrCast(d.title).startsWith("-")) { - const params = StrCast(d.title).match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", ""); + if (!StrCast(d.title).startsWith('-')) { + const params = StrCast(d.title) + .match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1] + .replace('()', ''); if (params) { any = makeTemplate(d, false) || any; d.PARAMS = params; @@ -43,12 +47,13 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und } }); if (first) { - if (!docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template + if (!docs.length) { + // bcz: feels hacky : if the root level document has items, it's not a field template any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any; } } if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) { - if (!StrCast(layoutDoc.title).startsWith("-")) { + if (!StrCast(layoutDoc.title).startsWith('-')) { any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc)); } } @@ -59,23 +64,29 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.map((doc, i) => { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant - if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes("FontIconBox")) { + if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) { if (data.removeDropProperties || dbox.removeDropProperties) { //dbox = Doc.MakeAlias(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon dbox = Doc.MakeAlias(dbox); - const dragProps = Cast(dbox.removeDropProperties, listSpec("string"), []); + const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []); const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps)); - remProps.map(prop => dbox[prop] = undefined); + remProps.map(prop => (dbox[prop] = undefined)); } } else if (!doc.onDragStart && !doc.isButtonBar) { - const layoutDoc = doc;// doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; + const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; if (layoutDoc.type !== DocumentType.FONTICON) { !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); } layoutDoc.isTemplateDoc = true; dbox = Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, - backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt" + _nativeWidth: 100, + _nativeHeight: 100, + _width: 100, + _height: 100, + backgroundColor: StrCast(doc.backgroundColor), + title: StrCast(layoutDoc.title), + btnType: ButtonType.ClickButton, + icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt', }); dbox.dragFactory = layoutDoc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; @@ -86,5 +97,10 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data.droppedDocuments[i] = dbox; }); } -ScriptingGlobals.add(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }, - "converts the dropped data to buttons", "(dragData: any)");
\ No newline at end of file +ScriptingGlobals.add( + function convertToButtons(dragData: any) { + convertDropDataToButtons(dragData as DragManager.DocumentDragData); + }, + 'converts the dropped data to buttons', + '(dragData: any)' +); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 59334f6a2..c8b784390 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -14,6 +14,7 @@ import { GroupMemberView } from './GroupMemberView'; import { SharingManager, User } from './SharingManager'; import { listSpec } from '../../fields/Schema'; import { DateField } from '../../fields/DateField'; +import { Id } from '../../fields/FieldSymbols'; /** * Interface for options for the react-select component @@ -49,9 +50,11 @@ export class GroupManager extends React.Component<{}> { * Fetches the list of users stored on the database. */ populateUsers = async () => { - const userList = await RequestPromise.get(Utils.prepend('/getUsers')); - const raw = JSON.parse(userList) as User[]; - raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email))); + if (Doc.UserDoc()[Id] !== '__guest__') { + const userList = await RequestPromise.get(Utils.prepend('/getUsers')); + const raw = JSON.parse(userList) as User[]; + raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email))); + } }; /** diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 289c5bc51..4af51b9a0 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -1,12 +1,12 @@ -import React = require("react"); -import { Utils } from "../../Utils"; -import "./InteractionUtils.scss"; +import React = require('react'); +import { Utils } from '../../Utils'; +import './InteractionUtils.scss'; export namespace InteractionUtils { - export const MOUSETYPE = "mouse"; - export const TOUCHTYPE = "touch"; - export const PENTYPE = "pen"; - export const ERASERTYPE = "eraser"; + export const MOUSETYPE = 'mouse'; + export const TOUCHTYPE = 'touch'; + export const PENTYPE = 'pen'; + export const ERASERTYPE = 'eraser'; const POINTER_PEN_BUTTON = -1; const REACT_POINTER_PEN_BUTTON = 0; @@ -19,24 +19,23 @@ export namespace InteractionUtils { readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[], readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[], readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent - ) { } + ) {} } - export interface MultiTouchEventDisposer { (): void; } + export interface MultiTouchEventDisposer { + (): void; + } /** * * @param element - element to turn into a touch target * @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart) */ - export function MakeMultiTouchTarget( - element: HTMLElement, - startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void - ): MultiTouchEventDisposer { + export function MakeMultiTouchTarget(element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer { const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail); // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined; // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined; - element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler); + element.addEventListener('dashOnTouchStart', onMultiTouchStartHandler); // if (onMultiTouchMoveHandler) { // element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler); // } @@ -44,7 +43,7 @@ export namespace InteractionUtils { // element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler); // } return () => { - element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler); + element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler); // if (onMultiTouchMoveHandler) { // element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler); // } @@ -59,14 +58,11 @@ export namespace InteractionUtils { * @param element - element to add events to * @param func - function to add to the event */ - export function MakeHoldTouchTarget( - element: HTMLElement, - func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void - ): MultiTouchEventDisposer { + export function MakeHoldTouchTarget(element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer { const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail); - element.addEventListener("dashOnTouchHoldStart", handler); + element.addEventListener('dashOnTouchHoldStart', handler); return () => { - element.removeEventListener("dashOnTouchHoldStart", handler); + element.removeEventListener('dashOnTouchHoldStart', handler); }; } @@ -89,71 +85,108 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, - color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean, - downHdlr?: ((e: React.PointerEvent) => void)) { + export function CreatePolyline( + points: { X: number; Y: number }[], + left: number, + top: number, + color: string, + width: number, + strokeWidth: number, + lineJoin: string, + lineCap: string, + bezier: string, + fill: string, + arrowStart: string, + arrowEnd: string, + markerScale: number, + dash: string | undefined, + scalex: number, + scaley: number, + shape: string, + pevents: string, + opacity: number, + nodefs: boolean, + downHdlr?: (e: React.PointerEvent) => void + ) { const pts = shape ? makePolygon(shape, points) : points; if (isNaN(scalex)) scalex = 1; if (isNaN(scaley)) scaley = 1; - const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `; - const strpts = bezier ? - pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") : - pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, ""); + const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `; + const strpts = bezier + ? pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? '' : (i === 0 ? 'M' + toScr(pt) : '') + 'C' + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), '') + : pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, ''); const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined; const defGuid = Utils.GenerateGuid(); - const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements; + const Tag = (bezier ? 'path' : 'polyline') as keyof JSX.IntrinsicElements; const markerStrokeWidth = strokeWidth / 2; - const arrowWidthFactor = 3 * (markerScale || 0.5);// used to be 1.5 + const arrowWidthFactor = 3 * (markerScale || 0.5); // used to be 1.5 const arrowLengthFactor = 5 * (markerScale || 0.5); const arrowNotchFactor = 2 * (markerScale || 0.5); - return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */} - {nodefs ? (null) : <defs> - {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : - <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible"> - <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" /> - </marker>} - {arrowStart !== "arrow" ? (null) : - <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7"> - <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3} - points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${markerStrokeWidth * arrowWidthFactor}, 0 0`} /> - </marker>} - {arrowEnd !== "arrow" ? (null) : - <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7"> - <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3} - points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} /> - </marker>} - </defs>} - - <Tag - d={bezier ? strpts : undefined} - points={bezier ? undefined : strpts} - style={{ - // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, - fill: fill && fill !== "transparent" ? fill : "none", - opacity: 1.0, - // opacity: strokeWidth !== width ? 0.5 : undefined, - pointerEvents: pevents as any, - stroke: color ?? "rgb(0, 0, 0)", - strokeWidth: strokeWidth, - strokeLinecap: lineCap as any, - strokeDasharray: dashArray - }} - markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`} - markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`} - /> - - </svg>); + return ( + <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}> + {' '} + {/* setting the svg fill sets the arrowStart fill */} + {nodefs ? null : ( + <defs> + {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : ( + <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible"> + <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" /> + </marker> + )} + {arrowStart !== 'arrow' ? null : ( + <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7"> + <polygon + style={{ stroke: color }} + strokeLinejoin={lineJoin as any} + strokeWidth={(markerStrokeWidth * 2) / 3} + points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${ + markerStrokeWidth * arrowWidthFactor + }, 0 0`} + /> + </marker> + )} + {arrowEnd !== 'arrow' ? null : ( + <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7"> + <polygon + style={{ stroke: color }} + strokeLinejoin={lineJoin as any} + strokeWidth={(markerStrokeWidth * 2) / 3} + points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} + /> + </marker> + )} + </defs> + )} + <Tag + d={bezier ? strpts : undefined} + points={bezier ? undefined : strpts} + style={{ + // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, + fill: fill && fill !== 'transparent' ? fill : 'none', + opacity: 1.0, + // opacity: strokeWidth !== width ? 0.5 : undefined, + pointerEvents: pevents as any, + stroke: color ?? 'rgb(0, 0, 0)', + strokeWidth: strokeWidth, + strokeLinecap: lineCap as any, + strokeDasharray: dashArray, + transition: 'inherit', + }} + markerStart={`url(#${arrowStart === 'dot' ? arrowStart + defGuid : arrowStart + 'Start' + defGuid})`} + markerEnd={`url(#${arrowEnd === 'dot' ? arrowEnd + defGuid : arrowEnd + 'End' + defGuid})`} + /> + </svg> + ); } - export function makePolygon(shape: string, points: { X: number, Y: number }[]) { + export function makePolygon(shape: string, points: { X: number; Y: number }[]) { if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) { //pointer is up (first and last points are the same) - if (shape === "arrow" || shape === "line" || shape === "circle") { + if (shape === 'arrow' || shape === 'line' || shape === 'circle') { //if arrow or line, the two end points should be the starting and the ending point var left = points[0].X; var top = points[0].Y; @@ -175,7 +208,7 @@ export namespace InteractionUtils { left = points[0].X; bottom = points[points.length - 1].Y; top = points[0].Y; - if (shape !== "arrow" && shape !== "line" && shape !== "circle") { + if (shape !== 'arrow' && shape !== 'line' && shape !== 'circle') { //switch left/right and top/bottom if needed if (left > right) { const temp = right; @@ -191,14 +224,13 @@ export namespace InteractionUtils { } points = []; switch (shape) { - case "rectangle": + case 'rectangle': points.push({ X: left, Y: top }); points.push({ X: right, Y: top }); points.push({ X: right, Y: bottom }); points.push({ X: left, Y: bottom }); points.push({ X: left, Y: top }); - return points; - case "triangle": + case 'triangle': // points.push({ X: left, Y: bottom }); // points.push({ X: right, Y: bottom }); // points.push({ X: (right + left) / 2, Y: top }); @@ -219,62 +251,39 @@ export namespace InteractionUtils { points.push({ X: left, Y: bottom }); points.push({ X: left, Y: bottom }); - - - return points; - case "circle": + case 'circle': const centerX = (Math.max(left, right) + Math.min(left, right)) / 2; const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2; const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) { for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX; points.push({ X: x, Y: y }); } for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX; const newX = centerX - (x - centerX); points.push({ X: newX, Y: y }); } - points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) }); + points.push({ X: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(top, bottom) - centerY, 2)) + centerX, Y: Math.min(top, bottom) }); } else { for (var x = Math.min(left, right); x < Math.max(left, right); x++) { - const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY; points.push({ X: x, Y: y }); } for (var x = Math.max(left, right); x > Math.min(left, right); x--) { - const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY; const newY = centerY - (y - centerY); points.push({ X: x, Y: newY }); } - points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY }); + points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY }); } - return points; - // case "arrow": - // const x1 = left; - // const y1 = top; - // const x2 = right; - // const y2 = bottom; - // const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2))); - // const L2 = L1 / 5; - // const angle = 0.785398; - // const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle)); - // const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle)); - // const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle)); - // const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle)); - // points.push({ X: x1, Y: y1 }); - // points.push({ X: x2, Y: y2 }); - // points.push({ X: x3, Y: y3 }); - // points.push({ X: x4, Y: y4 }); - // points.push({ X: x2, Y: y2 }); - // return points; - case "line": + case 'line': points.push({ X: left, Y: top }); points.push({ X: right, Y: bottom }); return points; - default: - return points; } + return points; } /** * Returns whether or not the pointer event passed in is of the type passed in @@ -284,11 +293,14 @@ export namespace InteractionUtils { export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean { switch (type) { // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2 - case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0); - case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON); + case PENTYPE: + return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0); + case ERASERTYPE: + return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON); case TOUCHTYPE: return e.pointerType === TOUCHTYPE; - default: return e.pointerType === type; + default: + return e.pointerType === type; } } @@ -305,7 +317,7 @@ export namespace InteractionUtils { * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point) * @param pts - n-arbitrary long list of points */ - export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } { + export function CenterPoint(pts: React.Touch[]): { X: number; Y: number } { const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length; const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length; return { X: centerX, Y: centerY }; @@ -324,9 +336,9 @@ export namespace InteractionUtils { const newDist = TwoPointEuclidist(pt1, pt2); /** if they have the same sign, then we are either pinching in or out. - * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch) - * so that it can still pan without freaking out - */ + * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch) + * so that it can still pan without freaking out + */ if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) { return Math.sign(oldDist - newDist); } @@ -372,8 +384,6 @@ export namespace InteractionUtils { // These might not be very useful anymore, but I'll leave them here for now -syip2 { - - /** * Returns the type of Touch Interaction from a list of points. * Also returns any data that is associated with a Touch Interaction diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts deleted file mode 100644 index 7bffb0379..000000000 --- a/src/client/util/RecordingApi.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; -import { IReactionDisposer, observable, reaction } from 'mobx'; -import { NumCast } from '../../fields/Types'; -import { Doc } from '../../fields/Doc'; -import { VideoBox } from '../views/nodes/VideoBox'; -import { scaleDiverging } from 'd3-scale'; -import { Transform } from './Transform'; - -type Movement = { - time: number; - panX: number; - panY: number; - scale: number; -}; - -type Presentation = { - movements: Array<Movement> | null; - meta: Object; -}; - -export class RecordingApi { - private static NULL_PRESENTATION: Presentation = { - movements: null, - meta: {}, - }; - - // instance variables - private currentPresentation: Presentation; - private isRecording: boolean; - private absoluteStart: number; - - // create static instance and getter for global use - @observable static _instance: RecordingApi; - public static get Instance(): RecordingApi { - return RecordingApi._instance; - } - public constructor() { - // init the global instance - RecordingApi._instance = this; - - // init the instance variables - this.currentPresentation = RecordingApi.NULL_PRESENTATION; - this.isRecording = false; - this.absoluteStart = -1; - - // used for tracking movements in the view frame - this.disposeFunc = null; - this.recordingFFView = null; - - // for now, set playFFView - this.playFFView = null; - this.timers = null; - } - - // little helper :) - private get isInitPresenation(): boolean { - return this.currentPresentation.movements === null; - } - - public start = (meta?: Object): Error | undefined => { - // check if already init a presentation - if (!this.isInitPresenation) { - console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.'); - return new Error('[recordingApi.ts] start()'); - } - - // update the presentation mode - Doc.UserDoc().presentationMode = 'recording'; - - // (1a) get start date for presenation - const startDate = new Date(); - // (1b) set start timestamp to absolute timestamp - this.absoluteStart = startDate.getTime(); - - // (2) assign meta content if it exists - this.currentPresentation.meta = meta || {}; - // (3) assign start date to currentPresenation - this.currentPresentation.movements = []; - // (4) set isRecording true to allow trackMovements - this.isRecording = true; - }; - - public clear = (): Error | Presentation => { - // TODO: maybe archive the data? - if (this.isRecording) { - console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first'); - return new Error('[recordingApi.ts] clear()'); - } - - // update the presentation mode - Doc.UserDoc().presentationMode = 'none'; - // set the previus recording view to the play view - this.playFFView = this.recordingFFView; - - const presCopy = { ...this.currentPresentation }; - - // clear presenation data - this.currentPresentation = RecordingApi.NULL_PRESENTATION; - // clear isRecording - this.isRecording = false; - // clear absoluteStart - this.absoluteStart = -1; - // clear the disposeFunc - this.removeRecordingFFView(); - - return presCopy; - }; - - public pause = (): Error | undefined => { - if (this.isInitPresenation) { - console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first'); - return new Error('[recordingApi.ts] pause(): no presentation'); - } - // don't allow track movments - this.isRecording = false; - - // set adjust absoluteStart to add the time difference - const timestamp = new Date().getTime(); - this.absoluteStart = timestamp - this.absoluteStart; - }; - - public resume = () => { - this.isRecording = true; - // set absoluteStart to the difference in time - this.absoluteStart = new Date().getTime() - this.absoluteStart; - }; - - private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => { - // ensure we are recording - if (!this.isRecording) { - return new Error('[recordingApi.ts] trackMovements()'); - } - // check to see if the presetation is init - if (this.isInitPresenation) { - return new Error('[recordingApi.ts] trackMovements(): no presentation'); - } - - // get the time - const time = new Date().getTime() - this.absoluteStart; - // make new movement object - const movement: Movement = { time, panX, panY, scale }; - - // add that movement to the current presentation data's movement array - this.currentPresentation.movements && this.currentPresentation.movements.push(movement); - }; - - // instance variable for the FFView - private disposeFunc: IReactionDisposer | null; - private recordingFFView: CollectionFreeFormView | null; - - // set the FFView that will be used in a reaction to track the movements - public setRecordingFFView = (view: CollectionFreeFormView): void => { - // set the view to the current view - if (view === this.recordingFFView || view == null) return; - - // this.recordingFFView = view; - // set the reaction to track the movements - this.disposeFunc = reaction( - () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }), - res => res.x !== -1 && res.y !== -1 && this.isRecording && this.trackMovements(res.x, res.y, res.scale) - ); - - // for now, set the most recent recordingFFView to the playFFView - this.recordingFFView = view; - }; - - // call on dispose function to stop tracking movements - public removeRecordingFFView = (): void => { - this.disposeFunc?.(); - this.disposeFunc = null; - }; - - // TODO: extract this into different class with pause and resume recording - // TODO: store the FFview with the movements - private playFFView: CollectionFreeFormView | null; - private timers: NodeJS.Timeout[] | null; - - public setPlayFFView = (view: CollectionFreeFormView): void => { - this.playFFView = view; - }; - - // pausing movements will dispose all timers that are planned to replay the movements - // play movemvents will recreate them when the user resumes the presentation - public pauseMovements = (): undefined | Error => { - if (this.playFFView === null) { - return new Error('[recordingApi.ts] pauseMovements() failed: no view'); - } - - if (!this._isPlaying) { - //return new Error('[recordingApi.ts] pauseMovements() failed: not playing') - return; - } - this._isPlaying = false; - // TODO: set userdoc presentMode to browsing - this.timers?.map(timer => clearTimeout(timer)); - - // this.videoBox = null; - }; - - private videoBox: VideoBox | null = null; - - // by calling pause on the VideoBox, the pauseMovements will be called - public pauseVideoAndMovements = (): boolean => { - this.videoBox?.Pause(); - - this.pauseMovements(); - return this.videoBox == null; - }; - - public _isPlaying = false; - - public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => { - if (presentation.movements === null || this.playFFView === null) { - return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view'); - } - if (this._isPlaying) return; - - this._isPlaying = true; - Doc.UserDoc().presentationMode = 'watching'; - - // TODO: consider this bug at the end of the clip on seek - this.videoBox = videoBox || null; - - // only get the movements that are remaining in the video time left - const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000); - - // helper to replay a movement - const document = this.playFFView; - let preScale = -1; - const zoomAndPan = (movement: Movement) => { - const { panX, panY, scale } = movement; - scale !== -1 && preScale !== scale && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); - document.Document._panX = panX; - document.Document._panY = panY; - - preScale = scale; - }; - - // set the first frame to be at the start of the pres - zoomAndPan(filteredMovements[0]); - - // make timers that will execute each movement at the correct replay time - this.timers = filteredMovements.map(movement => { - const timeDiff = movement.time - timeViewed * 1000; - return setTimeout(() => { - // replay the movement - zoomAndPan(movement); - // if last movement, presentation is done -> set the instance var - if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; - }, timeDiff); - }); - }; - - // Unfinished code for tracing multiple free form views - // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map() - - // export function AddRecordingFFView(ffView: CollectionFreeFormView): void { - // pres.set(ffView, - // reaction(() => ({ x: ffView.panX, y: ffView.panY }), - // (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y))) - // ) - // } - - // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void { - // const disposer = pres.get(ffView); - // disposer?.(); - // pres.delete(ffView) - // } -} diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss new file mode 100644 index 000000000..5a2f2fcad --- /dev/null +++ b/src/client/util/ReportManager.scss @@ -0,0 +1,88 @@ +@import '../views/global/globalCssVariables'; + +.issue-list-wrapper { + position: relative; + min-width: 250px; + background-color: $light-blue; + overflow-y: scroll; +} + +.issue-list { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px; + margin: 5px; + border-radius: 5px; + border: 1px solid grey; + background-color: lightgoldenrodyellow; +} + +// issue should pop up when the user hover over the issue +.issue-list:hover { + box-shadow: 2px; + cursor: pointer; + border: 3px solid #252b33; +} + +.issue-content { + background-color: white; + padding: 10px; + flex: 1 1 auto; + overflow-y: scroll; +} + +.issue-title { + font-size: 20px; + font-weight: 600; + color: black; +} + +.issue-body { + padding: 0 10px; + width: 100%; + text-align: left; +} + +.issue-body > * { + margin-top: 5px; +} + +.issue-body img, +.issue-body video { + display: block; + max-width: 100%; +} + +.report-issue-fab { + position: fixed; + bottom: 20px; + right: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.loading-center { + margin: auto 0; +} + +.settings-content label { + margin-top: 10px; +} + +.report-disclaimer { + font-size: 8px; + color: grey; + padding-right: 50px; + font-style: italic; + text-align: left; +} + +.flex-select { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx new file mode 100644 index 000000000..180d8294f --- /dev/null +++ b/src/client/util/ReportManager.tsx @@ -0,0 +1,297 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ColorState, SketchPicker } from 'react-color'; +import { Doc } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; +import { BoolCast, Cast, StrCast } from '../../fields/Types'; +import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; +import { DocServer } from '../DocServer'; +import { Networking } from '../Network'; +import { MainViewModal } from '../views/MainViewModal'; +import { FontIconBox } from '../views/nodes/button/FontIconBox'; +import { DragManager } from './DragManager'; +import { GroupManager } from './GroupManager'; +import './SettingsManager.scss'; +import './ReportManager.scss'; +import { undoBatch } from './UndoManager'; +import { Octokit } from "@octokit/core"; +import { CheckBox } from '../views/search/CheckBox'; +import ReactLoading from 'react-loading'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +const higflyout = require('@hig/flyout'); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +export class ReportManager extends React.Component<{}> { + public static Instance: ReportManager; + @observable private isOpen = false; + + private octokit: Octokit; + + @observable public issues: any[] = []; + @action setIssues = action((issues: any[]) => { this.issues = issues; }); + + // undefined is the default - null is if the user is making an issue + @observable public selectedIssue: any = undefined; + @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; }); + + // only get the open issues + @observable public shownIssues = this.issues.filter(issue => issue.state === 'open'); + + public updateIssueSearch = action((query: string = '') => { + if (query === '') { + this.shownIssues = this.issues.filter(issue => issue.state === 'open'); + return; + } + this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase())); + }); + + constructor(props: {}) { + super(props); + ReportManager.Instance = this; + + this.octokit = new Octokit({ + auth: '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' + }); + } + + public close = action(() => (this.isOpen = false)); + public open = action(() => { + if (this.issues.length === 0) { + // load in the issues if not already loaded + this.getAllIssues() + .then(issues => { + this.setIssues(issues); + this.updateIssueSearch(); + }) + .catch(err => console.log(err)); + } + (this.isOpen = true) + }); + + @observable private bugTitle = ''; + @action setBugTitle = action((title: string) => { this.bugTitle = title; }); + @observable private bugDescription = ''; + @action setBugDescription = action((description: string) => { this.bugDescription = description; }); + @observable private bugType = ''; + @action setBugType = action((type: string) => { this.bugType = type; }); + @observable private bugPriority = ''; + @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; }); + + // private toGithub = false; + // will always be set to true - no alterntive option yet + private toGithub = true; + + private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`; + + public async getAllIssues() : Promise<any[]> { + const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', { + owner: 'brown-dash', + repo: 'Dash-Web', + }); + + // 200 status means success + if (res.status === 200) { + return res.data; + } else { + throw new Error('Error getting issues'); + } + } + + // turns an upload link into a servable link + // ex: + // C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png + // -> http://localhost:1050/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png + private fileLinktoServerLink = (fileLink: string) => { + const serverUrl = 'https://browndash.com/'; + + const regex = 'public' + const publicIndex = fileLink.indexOf(regex) + regex.length; + + const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`; + return finalUrl; + } + + public async reportIssue() { + if (this.bugTitle === '' || this.bugDescription === '' + || this.bugType === '' || this.bugPriority === '') { + alert('Please fill out all required fields to report an issue.'); + return; + } + + if (this.toGithub) { + + const formattedLinks = (this.fileLinks ?? []).map(this.fileLinktoServerLink) + + const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', { + owner: 'brown-dash', + repo: 'Dash-Web', + title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail), + body: `${this.bugDescription} \n\nfiles:\n${formattedLinks.join('\n')}`, + labels: [ + 'from-dash-app', + this.bugType, + this.bugPriority + ] + }); + + // 201 status means success + if (req.status !== 201) { + alert('Error creating issue on github.'); + // on error, don't close the modal + return; + } + } + else { + // if not going to github issues, not sure what to do yet... + } + + // if we're down here, then we're good to go. reset the fields. + this.setBugTitle(''); + this.setBugDescription(''); + // this.toGithub = false; + this.setFileLinks([]); + this.setBugType(''); + this.setBugPriority(''); + this.close(); + } + + @observable public fileLinks: any = []; + @action setFileLinks = action((links: any) => { this.fileLinks = links; }); + + private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server } + + private uploadFiles = (input: any) => { + // keep null while uploading + this.setFileLinks(null); + // upload the files to the server + if (input.files && input.files.length !== 0) { + const fileArray: File[] = Array.from(input.files); + (Networking.UploadFilesToServer(fileArray)).then(links => { + console.log('finshed uploading', links.map(this.getServerPath)); + this.setFileLinks((links ?? []).map(this.getServerPath)); + }) + } + + } + + + private renderIssue = (issue: any) => { + + const isReportingIssue = issue === null; + + return isReportingIssue ? + // report issue + (<div className="settings-content"> + <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3> + <label>Please leave a title for the bug.</label><br /> + <input type="text" placeholder='title' onChange={(e) => this.setBugTitle(e.target.value)} required/> + <br /> + <label>Please leave a description for the bug and how it can be recreated.</label> + <textarea placeholder='description' onChange={(e) => this.setBugDescription(e.target.value)} required/> + <br /> + {/* {<label>Send to github issues? </label> + <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} /> + <br /> } */} + + <label>Please label the issue</label> + <div className='flex-select'> + <select name="bugType" onChange={e => this.bugType = e.target.value}> + <option value="" disabled selected>Type</option> + <option value="bug">Bug</option> + <option value="cosmetic">Poor Design or Cosmetic</option> + <option value="documentation">Poor Documentation</option> + </select> + + <select name="bigPriority" onChange={e => this.bugPriority = e.target.value}> + <option value="" disabled selected>Priority</option> + <option value="priority-low">Low</option> + <option value="priority-medium">Medium</option> + <option value="priority-high">High</option> + </select> + </div> + + + <div> + <label>Upload media that shows the bug (optional)</label> + <input type="file" name="file" multiple accept='audio/*, video/*, image/*' onChange={e => this.uploadFiles(e.target)}/> + </div> + <br /> + + <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button> + </div>) + : + // view issue + ( + <div className='issue-container'> + <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5> + <div className='issue-title'> + {issue.title} + </div> + <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> + </div> + ); + } + + private showReportIssueScreen = () => { + this.setSelectedIssue(null); + } + + private closeReportIssueScreen = () => { + this.setSelectedIssue(undefined); + } + + private get reportInterface() { + + const isReportingIssue = this.selectedIssue === null; + + return ( + <div className="settings-interface"> + <div className='issue-list-wrapper'> + <h3>Current Issues</h3> + <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br /> + {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)} + + {/* <div className="settings-user"> + <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button> + </div> */} + </div> + + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon={'times'} color="black" size={'lg'} /> + </div> + + <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}> + {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)} + </div> + + <div className='report-issue-fab'> + <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span> + <button + onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()} + >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button> + </div> + + + </div> + ); + } + + render() { + return ( + <MainViewModal + contents={this.reportInterface} + isDisplayed={this.isOpen} + interactive={true} + closeOnExternalClick={this.close} + dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }} + /> + ); + } +} diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 3791bec73..6dcdcb71b 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -5,12 +5,13 @@ // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; -import * as ts from "typescript"; -import { Doc, Field } from "../../fields/Doc"; -import { scriptingGlobals, ScriptingGlobals } from "./ScriptingGlobals"; +import * as ts from 'typescript'; +import { Doc, Field } from '../../fields/Doc'; +import { ObjectField } from '../../fields/ObjectField'; +import { ScriptField } from '../../fields/ScriptField'; +import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; export { ts }; - export interface ScriptSuccess { success: true; result: any; @@ -46,7 +47,6 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is return false; } - function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors.length) || !script) { @@ -63,7 +63,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => { const argsArray: any[] = []; for (const name of customParams) { - if (name === "this") { + if (name === 'this') { continue; } if (name in args) { @@ -86,11 +86,10 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an return { success: true, result }; } catch (error) { - if (batch) { batch.end(); } - onError?.(script + " " + error); + onError?.(script + ' ' + error); return { success: false, error, result: errorVal }; } }; @@ -151,16 +150,16 @@ class ScriptingCompilerHost { } export type Traverser = (node: ts.Node, indentation: string) => boolean | void; -export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser }; +export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser }; export type Transformer = { - transformer: ts.TransformerFactory<ts.SourceFile>, - getVars?: () => { capturedVariables: { [name: string]: Field } } + transformer: ts.TransformerFactory<ts.SourceFile>; + getVars?: () => { capturedVariables: { [name: string]: Field } }; }; export interface ScriptOptions { requiredType?: string; // does function required a typed return value - addReturn?: boolean; // does the compiler automatically add a return statement + addReturn?: boolean; // does the compiler automatically add a return statement params?: { [name: string]: string }; // list of function parameters and their types - capturedVariables?: { [name: string]: Field }; // list of captured variables + capturedVariables?: { [name: string]: Doc | number | string | boolean }; // list of captured variables typecheck?: boolean; // should the compiler perform typechecking editable?: boolean; // can the script edit Docs traverser?: TraverserParam; @@ -169,24 +168,32 @@ export interface ScriptOptions { } // function forEachNode(node:ts.Node, fn:(node:any) => void); -function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") { - return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => { - forEachNode(n, onEnter, onExit, indentation + " "); - }) || (onExit && onExit(node, indentation)); +function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = '') { + return ( + onEnter(node, indentation) || + ts.forEachChild(node, (n: any) => { + forEachNode(n, onEnter, onExit, indentation + ' '); + }) || + (onExit && onExit(node, indentation)) + ); } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { - const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; + const captured = options.capturedVariables ?? {}; + const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, ''); + const found = ScriptField.GetScriptFieldCache(script + ':' + signature); + if (found) return found as CompiledScript; + const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; if (options.params && !options.params.this) options.params.this = Doc.name; if (options.params && !options.params.self) options.params.self = Doc.name; if (options.globals) { ScriptingGlobals.setScriptingGlobals(options.globals); } - const host = new ScriptingCompilerHost; + const host = new ScriptingCompilerHost(); if (options.traverser) { const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); - const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser; - const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined; + const onEnter = typeof options.traverser === 'object' ? options.traverser.onEnter : options.traverser; + const onLeave = typeof options.traverser === 'object' ? options.traverser.onLeave : undefined; forEachNode(sourceFile, onEnter, onLeave); } if (options.transformer) { @@ -199,17 +206,17 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp } const transformed = result.transformed; const printer = ts.createPrinter({ - newLine: ts.NewLineKind.LineFeed + newLine: ts.NewLineKind.LineFeed, }); script = printer.printFile(transformed[0]); result.dispose(); } const paramNames: string[] = []; - if ("this" in params || "this" in capturedVariables) { - paramNames.push("this"); + if ('this' in params || 'this' in capturedVariables) { + paramNames.push('this'); } for (const key in params) { - if (key === "this") continue; + if (key === 'this') continue; paramNames.push(key); } const paramList = paramNames.map(key => { @@ -217,21 +224,21 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp return `${key}: ${val}`; }); for (const key in capturedVariables) { - if (key === "this") continue; + if (key === 'this') continue; const val = capturedVariables[key]; paramNames.push(key); - paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`); + paramList.push(`${key}: ${typeof val === 'object' ? Object.getPrototypeOf(val).constructor.name : typeof val}`); } - const paramString = paramList.join(", "); - const body = addReturn && !script.startsWith("{ return") ? `return ${script};` : script; + const paramString = paramList.join(', '); + const body = addReturn && !script.startsWith('{ return') ? `return ${script};` : script; const reqTypes = requiredType ? `: ${requiredType}` : ''; const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; - host.writeFile("file.ts", funcScript); + host.writeFile('file.ts', funcScript); if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); - const program = ts.createProgram(["file.ts"], {}, host); + const program = ts.createProgram(['file.ts'], {}, host); const testResult = program.emit(); - const outputText = host.readFile("file.js"); + const outputText = host.readFile('file.js'); const diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); @@ -240,6 +247,8 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp if (options.globals) { ScriptingGlobals.resetScriptingGlobals(); } + !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript); + //console.log('COMPILED: ' + script + ':' + signature); return result; } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 1c84af94a..7a555d5f8 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -101,6 +101,9 @@ export namespace SelectionManager { } } ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) { + if (colType === ('tab' as any)) { + return SelectionManager.Views().lastElement()?.props.renderDepth === 0; + } let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true; }); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 12d1793af..cf143c5e8 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -4,6 +4,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorState, SketchPicker } from 'react-color'; import { Doc } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; import { BoolCast, Cast, StrCast } from '../../fields/Types'; import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; @@ -81,7 +82,7 @@ export class SettingsManager extends React.Component<{}> { if (this.playgroundMode) { DocServer.Control.makeReadOnly(); addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); - } else DocServer.Control.makeEditable(); + } else Doc.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); }); @undoBatch diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index f3ff842fb..4b0310e76 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -23,6 +23,7 @@ import { GroupMemberView } from './GroupMemberView'; import { SelectionManager } from './SelectionManager'; import './SharingManager.scss'; import { LinkManager } from './LinkManager'; +import { Id } from '../../fields/FieldSymbols'; export interface User { email: string; @@ -136,7 +137,7 @@ export class SharingManager extends React.Component<{}> { * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. */ populateUsers = async () => { - if (!this.populating) { + if (!this.populating && Doc.UserDoc()[Id] !== '__guest__') { this.populating = true; const userList = await RequestPromise.get(Utils.prepend('/getUsers')); const raw = JSON.parse(userList) as User[]; diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts index d512e4802..4a2ccd706 100644 --- a/src/client/util/TrackMovements.ts +++ b/src/client/util/TrackMovements.ts @@ -1,27 +1,26 @@ -import { IReactionDisposer, observable, observe, reaction } from "mobx"; -import { NumCast } from "../../fields/Types"; -import { Doc, DocListCast } from "../../fields/Doc"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { Id } from "../../fields/FieldSymbols"; +import { IReactionDisposer, observable, observe, reaction } from 'mobx'; +import { NumCast } from '../../fields/Types'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { CollectionDockingView } from '../views/collections/CollectionDockingView'; +import { Id } from '../../fields/FieldSymbols'; export type Movement = { - time: number, - panX: number, - panY: number, - scale: number, - docId: string, -} + time: number; + panX: number; + panY: number; + scale: number; + docId: string; +}; export type Presentation = { - movements: Movement[] | null, - totalTime: number, - meta: Object | Object[], -} + movements: Movement[] | null; + totalTime: number; + meta: Object | Object[]; +}; export class TrackMovements { - private static get NULL_PRESENTATION(): Presentation { - return { movements: null, meta: {}, totalTime: -1, } + return { movements: null, meta: {}, totalTime: -1 }; } // instance variables @@ -32,16 +31,17 @@ export class TrackMovements { private recordingFFViews: Map<string, IReactionDisposer> | null; private tabChangeDisposeFunc: IReactionDisposer | null; - // create static instance and getter for global use @observable static _instance: TrackMovements; - static get Instance(): TrackMovements { return TrackMovements._instance } + static get Instance(): TrackMovements { + return TrackMovements._instance; + } constructor() { // init the global instance TrackMovements._instance = this; // init the instance variables - this.currentPresentation = TrackMovements.NULL_PRESENTATION + this.currentPresentation = TrackMovements.NULL_PRESENTATION; this.tracking = false; this.absoluteStart = -1; @@ -52,28 +52,37 @@ export class TrackMovements { // little helper :) private get nullPresentation(): boolean { - return this.currentPresentation.movements === null + return this.currentPresentation.movements === null; } private addRecordingFFView(doc: Doc, key: string = doc[Id]): void { // console.info('adding dispose func : docId', key, 'doc', doc); - if (this.recordingFFViews === null) { console.warn('addFFView on null RecordingApi'); return; } - if (this.recordingFFViews.has(key)) { console.warn('addFFView : key already in map'); return; } + if (this.recordingFFViews === null) { + console.warn('addFFView on null RecordingApi'); + return; + } + if (this.recordingFFViews.has(key)) { + console.warn('addFFView : key already in map'); + return; + } const disposeFunc = reaction( - () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0)}), - (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovement(res.x, res.y, key, res.scale), + () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0) }), + res => res.x !== -1 && res.y !== -1 && this.tracking && this.trackMovement(res.x, res.y, key, res.scale) ); this.recordingFFViews?.set(key, disposeFunc); } private removeRecordingFFView = (key: string) => { // console.info('removing dispose func : docId', key); - if (this.recordingFFViews === null) { console.warn('removeFFView on null RecordingApi'); return; } + if (this.recordingFFViews === null) { + console.warn('removeFFView on null RecordingApi'); + return; + } this.recordingFFViews.get(key)?.(); this.recordingFFViews.delete(key); - } + }; // in the case where only one tab was changed (updates not across dashboards), set only one to true private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => { @@ -86,7 +95,6 @@ export class TrackMovements { if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]); } - // new tab was added - need to add it if (tabbedFFViews.size > this.recordingFFViews.size) { for (const DashDoc of tabbedDocs) { @@ -103,13 +111,13 @@ export class TrackMovements { // tab was removed - need to remove it from recordingFFViews else if (tabbedFFViews.size < this.recordingFFViews.size) { for (const [key] of this.recordingFFViews) { - if (!tabbedFFViews.has(key)) { + if (!tabbedFFViews.has(key)) { this.removeRecordingFFView(key); if (onlyOne) return; - } + } } } - } + }; private initTabTracker = () => { if (this.recordingFFViews === null) { @@ -117,18 +125,19 @@ export class TrackMovements { } // init the dispose funcs on the page - const docList = DocListCast(CollectionDockingView.Instance.props.Document.data); + const docList = DocListCast(CollectionDockingView.Instance?.props.Document.data); this.updateRecordingFFViewsFromTabs(docList); // create a reaction to monitor changes in tabs - this.tabChangeDisposeFunc = - reaction(() => CollectionDockingView.Instance.props.Document.data, - (change) => { - // TODO: consider changing between dashboards - // console.info('change in tabs', change); - this.updateRecordingFFViewsFromTabs(DocListCast(change), true); - }); - } + this.tabChangeDisposeFunc = reaction( + () => CollectionDockingView.Instance?.props.Document.data, + change => { + // TODO: consider changing between dashboards + // console.info('change in tabs', change); + this.updateRecordingFFViewsFromTabs(DocListCast(change), true); + } + ); + }; start = (meta?: Object) => { this.initTabTracker(); @@ -142,12 +151,12 @@ export class TrackMovements { this.absoluteStart = startDate.getTime(); // (2) assign meta content if it exists - this.currentPresentation.meta = meta || {} + this.currentPresentation.meta = meta || {}; // (3) assign start date to currentPresenation - this.currentPresentation.movements = [] + this.currentPresentation.movements = []; // (4) set tracking true to allow trackMovements - this.tracking = true - } + this.tracking = true; + }; /* stops the video and returns the presentatation; if no presentation, returns undefined */ yieldPresentation(clearData: boolean = true): Presentation | null { @@ -157,7 +166,7 @@ export class TrackMovements { // set the previus recording view to the play view // this.playFFView = this.recordingFFView; - // ensure we add the endTime now that they are done recording + // ensure we add the endTime now that they are done recording const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart }; // reset the current presentation @@ -169,10 +178,10 @@ export class TrackMovements { finish = (): void => { // make is tracking false - this.tracking = false + this.tracking = false; // reset the RecordingApi instance this.clear(); - } + }; private clear = (): void => { // clear the disposeFunc if we are done (not tracking) @@ -187,44 +196,46 @@ export class TrackMovements { } // clear presenation data - this.currentPresentation = TrackMovements.NULL_PRESENTATION + this.currentPresentation = TrackMovements.NULL_PRESENTATION; // clear absoluteStart - this.absoluteStart = -1 - } + this.absoluteStart = -1; + }; removeAllRecordingFFViews = () => { - if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; } + if (this.recordingFFViews === null) { + console.warn('removeAllFFViews on null RecordingApi'); + return; + } for (const [id, disposeFunc] of this.recordingFFViews) { // console.info('calling dispose func : docId', id); disposeFunc(); this.recordingFFViews.delete(id); } - } + }; private trackMovement = (panX: number, panY: number, docId: string, scale: number = 0) => { // ensure we are recording to track if (!this.tracking) { - console.error('[recordingApi.ts] trackMovements(): tracking is false') + console.error('[recordingApi.ts] trackMovements(): tracking is false'); return; } // check to see if the presetation is init - if not, we are between segments // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres) - // bacuse tracking should be false inbetween segments high key + // bacuse tracking should be false inbetween segments high key if (this.nullPresentation) { - console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments') + console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments'); return; } // get the time - const time = new Date().getTime() - this.absoluteStart + const time = new Date().getTime() - this.absoluteStart; // make new movement object - const movement: Movement = { time, panX, panY, scale, docId } + const movement: Movement = { time, panX, panY, scale, docId }; // add that movement to the current presentation data's movement array - this.currentPresentation.movements && this.currentPresentation.movements.push(movement) - } - + this.currentPresentation.movements && this.currentPresentation.movements.push(movement); + }; // method that concatenates an array of presentatations into one public concatPresentations = (presentations: Presentation[]): Presentation => { @@ -233,13 +244,15 @@ export class TrackMovements { let sumTime = 0; let combinedMetas: any[] = []; - presentations.forEach((presentation) => { + presentations.forEach(presentation => { const { movements, totalTime, meta } = presentation; // update movements if they had one if (movements) { // add the summed time to the movements - const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } }); + const addedTimeMovements = movements.map(move => { + return { ...move, time: move.time + sumTime }; + }); // concat the movements already in the combined presentation with these new ones combinedMovements.push(...addedTimeMovements); } @@ -252,6 +265,6 @@ export class TrackMovements { }); // return the combined presentation with the updated total summed time - return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; - } + return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; + }; } diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index d1f1a0099..d0aec45a6 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,5 +1,5 @@ -import { observable, action, runInAction } from "mobx"; -import { Without } from "../../Utils"; +import { observable, action, runInAction } from 'mobx'; +import { Without } from '../../Utils'; function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); @@ -28,9 +28,9 @@ function propertyDecorator(target: any, key: string | symbol) { } finally { batch.end(); } - } + }, }); - } + }, }); } @@ -39,7 +39,7 @@ export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { if (!key) { return function () { - const batch = UndoManager.StartBatch(""); + const batch = UndoManager.StartBatch(''); try { return target.apply(undefined, arguments); } finally { @@ -96,7 +96,7 @@ export namespace UndoManager { } export function PrintBatches(): void { - console.log("Open Undo Batches:"); + console.log('Open Undo Batches:'); GetOpenBatches().forEach(batch => console.log(batch.batchName)); } @@ -106,23 +106,25 @@ export namespace UndoManager { } export function FilterBatches(fieldTypes: string[]) { const fieldCounts: { [key: string]: number } = {}; - const lastStack = UndoManager.undoStack.slice(-1)[0];//.lastElement(); + const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement(); if (lastStack) { lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1)); const fieldCount2: { [key: string]: number } = {}; - runInAction(() => - UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { - if (fieldTypes.includes(ev.prop)) { - fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; - if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; - return false; - } - return true; - })); + runInAction( + () => + (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { + if (fieldTypes.includes(ev.prop)) { + fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; + if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; + return false; + } + return true; + })) + ); } } export function TraceOpenBatches() { - console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join("\n\t")}\n`); + console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join('\n\t')}\n`); } export class Batch { private disposed: boolean = false; @@ -133,15 +135,15 @@ export namespace UndoManager { private dispose = (cancel: boolean) => { if (this.disposed) { - throw new Error("Cannot dispose an already disposed batch"); + throw new Error('Cannot dispose an already disposed batch'); } this.disposed = true; openBatches.splice(openBatches.indexOf(this)); - EndBatch(cancel); - } + return EndBatch(cancel); + }; - end = () => { this.dispose(false); }; - cancel = () => { this.dispose(true); }; + end = () => this.dispose(false); + cancel = () => this.dispose(true); } export function StartBatch(batchName: string): Batch { @@ -163,7 +165,9 @@ export namespace UndoManager { } redoStack.length = 0; currentBatch = undefined; + return true; } + return false; }); export function RunInTempBatch<T>(fn: () => T) { @@ -232,5 +236,4 @@ export namespace UndoManager { undoStack.push(commands); }); - -}
\ No newline at end of file +} diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index ea24dbf6d..1e6a377de 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -1,10 +1,10 @@ -@import "global/globalCssVariables"; +@import 'global/globalCssVariables'; .contextMenu-cont { position: absolute; display: flex; z-index: 100000; - box-shadow: 0px 3px 4px rgba(0,0,0,30%); + box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%); flex-direction: column; background: whitesmoke; border-radius: 3px; @@ -28,9 +28,9 @@ position: absolute; display: flex; z-index: 1000; - box-shadow: #AAAAAA .2vw .2vw .4vw; + box-shadow: #aaaaaa 0.2vw 0.2vw 0.4vw; flex-direction: column; - border: 1px solid #BBBBBBBB; + border: 1px solid #bbbbbbbb; border-radius: 15px; padding-top: 10px; padding-bottom: 10px; @@ -49,7 +49,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - transition: all .1s; + transition: all 0.1s; border-style: none; // padding: 10px 0px 10px 0px; white-space: nowrap; @@ -58,7 +58,7 @@ text-transform: uppercase; padding-right: 30px; - .icon-background { + .contextMenu-item-icon-background { pointer-events: all; background-color: transparent; width: 35px; @@ -78,7 +78,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - transition: all .1s; + transition: all 0.1s; border-style: none; // padding: 10px 0px 10px 0px; white-space: nowrap; @@ -89,7 +89,7 @@ } .contextMenu-item:hover { - border-width: .11px; + border-width: 0.11px; border-style: none; border-color: $medium-gray; // rgb(187, 186, 186); border-bottom-style: solid; @@ -116,8 +116,8 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - transition: all .1s; - border-width: .11px; + transition: all 0.1s; + border-width: 0.11px; border-style: none; border-color: $medium-gray; // rgb(187, 186, 186); // padding: 10px 0px 10px 0px; @@ -152,4 +152,4 @@ padding-left: 10px; border: solid black 1px; border-radius: 5px; -}
\ No newline at end of file +} diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index c9908b4b0..6a530e3ae 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -118,27 +118,11 @@ export class ContextMenu extends React.Component { static readonly buffer = 20; get pageX() { - const x = this._pageX; - if (x < 0) { - return 0; - } - const width = this._width; - if (x + width > window.innerWidth - ContextMenu.buffer) { - return window.innerWidth - ContextMenu.buffer - width; - } - return x; + return this._pageX + this._width > window.innerWidth - ContextMenu.buffer ? window.innerWidth - ContextMenu.buffer - this._width : Math.max(0, this._pageX); } get pageY() { - const y = this._pageY; - if (y < 0) { - return 0; - } - const height = this._height; - if (y + height > window.innerHeight - ContextMenu.buffer) { - return window.innerHeight - ContextMenu.buffer - height; - } - return y; + return this._pageY + this._height > window.innerHeight - ContextMenu.buffer ? window.innerHeight - ContextMenu.buffer - this._height : Math.max(0, this._pageY); } _onDisplay?: () => void = undefined; @@ -223,7 +207,15 @@ export class ContextMenu extends React.Component { render() { return !this._display ? null : ( - <div className="contextMenu-cont" style={{ left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }) }}> + <div + className="contextMenu-cont" + ref={action((r: any) => { + if (r) { + this._width = Number(getComputedStyle(r).width.replace('px', '')); + this._height = Number(getComputedStyle(r).height.replace('px', '')); + } + })} + style={{ left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }) }}> {!this.itemsNeedSearch ? null : ( <span className={'search-icon'}> <span className="icon-background"> diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 30073e21f..dc9c2eb6c 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,9 +1,9 @@ -import React = require("react"); -import { observable, action, runInAction } from "mobx"; -import { observer } from "mobx-react"; +import React = require('react'); +import { observable, action, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { UndoManager } from "../util/UndoManager"; +import { UndoManager } from '../util/UndoManager'; export interface OriginalMenuProps { description: string; @@ -37,7 +37,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select } handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { - if ("event" in this.props) { + if ('event' in this.props) { this.props.closeMenu?.(); let batch: UndoManager.Batch | undefined; if (this.props.undoable !== false) { @@ -46,7 +46,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select await this.props.event({ x: e.clientX, y: e.clientY }); batch?.end(); } - } + }; currentTimeout?: any; static readonly timeout = 300; @@ -62,8 +62,11 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select } this._overPosY = e.clientY; this._overPosX = e.clientX; - this.currentTimeout = setTimeout(action(() => this.overItem = true), ContextMenuItem.timeout); - } + this.currentTimeout = setTimeout( + action(() => (this.overItem = true)), + ContextMenuItem.timeout + ); + }; onPointerLeave = () => { if (this.currentTimeout) { @@ -73,57 +76,68 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select if (!this.overItem) { return; } - this.currentTimeout = setTimeout(action(() => this.overItem = false), ContextMenuItem.timeout); - } + this.currentTimeout = setTimeout( + action(() => (this.overItem = false)), + ContextMenuItem.timeout + ); + }; render() { - if ("event" in this.props) { + if ('event' in this.props) { return ( - <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onPointerDown={this.handleEvent}> + <div className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}> {this.props.icon ? ( - <span className="icon-background"> + <span className="contextMenu-item-icon-background"> <FontAwesomeIcon icon={this.props.icon} size="sm" /> </span> ) : null} - <div className="contextMenu-description"> - {this.props.description.replace(":", "")} - </div> + <div className="contextMenu-description">{this.props.description.replace(':', '')}</div> </div> ); - } else if ("subitems" in this.props) { - const where = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "flex-start" : this._overPosY > window.innerHeight * 2 / 3 ? "flex-end" : "center"; - const marginTop = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "20px" : this._overPosY > window.innerHeight * 2 / 3 ? "-20px" : ""; + } else if ('subitems' in this.props) { + const where = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? 'flex-start' : this._overPosY > (window.innerHeight * 2) / 3 ? 'flex-end' : 'center'; + const marginTop = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? '20px' : this._overPosY > (window.innerHeight * 2) / 3 ? '-20px' : ''; // here - const submenu = !this.overItem ? (null) : - <div className="contextMenu-subMenu-cont" + const submenu = !this.overItem ? null : ( + <div + className="contextMenu-subMenu-cont" style={{ - marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? "90%" : "20%", marginTop + marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? '90%' : '20%', + marginTop, }}> - {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} - </div>; + {this._items.map(prop => ( + <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> + ))} + </div> + ); if (!(this.props as SubmenuProps).noexpand) { - return <div className="contextMenu-inlineMenu"> - {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} - </div>; + return ( + <div className="contextMenu-inlineMenu"> + {this._items.map(prop => ( + <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> + ))} + </div> + ); } return ( - <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} - style={{ alignItems: where, borderTop: this.props.addDivider ? "solid 1px" : undefined }} - onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}> + <div + className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} + style={{ alignItems: where, borderTop: this.props.addDivider ? 'solid 1px' : undefined }} + onMouseLeave={this.onPointerLeave} + onMouseEnter={this.onPointerEnter}> {this.props.icon ? ( - <span className="icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: "center", alignSelf: "center" }}> + <span className="contextMenu-item-icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: 'center', alignSelf: 'center' }}> <FontAwesomeIcon icon={this.props.icon} size="sm" /> </span> ) : null} - <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} - style={{ alignItems: "center", alignSelf: "center" }} > + <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: 'center', alignSelf: 'center' }}> {this.props.description} - <FontAwesomeIcon icon={"angle-right"} size="lg" style={{ position: "absolute", right: "10px" }} /> + <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} /> </div> {submenu} </div> ); } } -}
\ No newline at end of file +} diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index bca80c342..478234eb0 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -196,7 +196,6 @@ export class DashboardView extends React.Component { closeOnExternalClick={this.abortCreateNewDashboard} dialogueBoxStyle={{ width: '500px', height: '300px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }} /> - ; </> ); } @@ -241,7 +240,7 @@ export class DashboardView extends React.Component { } else if (doc.readOnly) { DocServer.Control.makeReadOnly(); } else { - DocServer.Control.makeEditable(); + Doc.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); } } diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx index f4f96da8a..0bdcdc303 100644 --- a/src/client/views/DictationOverlay.tsx +++ b/src/client/views/DictationOverlay.tsx @@ -1,9 +1,8 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import "normalize.css"; import * as React from 'react'; import { DictationManager } from '../util/DictationManager'; -import "./Main.scss"; +import './Main.scss'; import { MainViewModal } from './MainViewModal'; @observer @@ -29,44 +28,53 @@ export class DictationOverlay extends React.Component { this.dictationOverlayVisible = false; this.dictationSuccess = undefined; DictationOverlay.Instance.hasActiveModal = false; - setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500); + setTimeout(() => (this.dictatedPhrase = DictationManager.placeholder), 500); }, duration); - } + }; public cancelDictationFade = () => { if (this.overlayTimeout) { clearTimeout(this.overlayTimeout); this.overlayTimeout = undefined; } - } + }; - @computed public get dictatedPhrase() { return this._dictationState; } - @computed public get dictationSuccess() { return this._dictationSuccessState; } - @computed public get dictationOverlayVisible() { return this._dictationDisplayState; } - @computed public get isListening() { return this._dictationListeningState; } + @computed public get dictatedPhrase() { + return this._dictationState; + } + @computed public get dictationSuccess() { + return this._dictationSuccessState; + } + @computed public get dictationOverlayVisible() { + return this._dictationDisplayState; + } + @computed public get isListening() { + return this._dictationListeningState; + } - public set dictatedPhrase(value: string) { runInAction(() => this._dictationState = value); } - public set dictationSuccess(value: boolean | undefined) { runInAction(() => this._dictationSuccessState = value); } - public set dictationOverlayVisible(value: boolean) { runInAction(() => this._dictationDisplayState = value); } - public set isListening(value: DictationManager.Controls.ListeningUIStatus) { runInAction(() => this._dictationListeningState = value); } + public set dictatedPhrase(value: string) { + runInAction(() => (this._dictationState = value)); + } + public set dictationSuccess(value: boolean | undefined) { + runInAction(() => (this._dictationSuccessState = value)); + } + public set dictationOverlayVisible(value: boolean) { + runInAction(() => (this._dictationDisplayState = value)); + } + public set isListening(value: DictationManager.Controls.ListeningUIStatus) { + runInAction(() => (this._dictationListeningState = value)); + } render() { const success = this.dictationSuccess; const result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`; const dialogueBoxStyle = { - background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red", - borderColor: this.isListening ? "red" : "black", - fontStyle: "italic" + background: success === undefined ? 'gainsboro' : success ? 'lawngreen' : 'red', + borderColor: this.isListening ? 'red' : 'black', + fontStyle: 'italic', }; const overlayStyle = { - backgroundColor: this.isListening ? "red" : "darkslategrey" + backgroundColor: this.isListening ? 'red' : 'darkslategrey', }; - return (<MainViewModal - contents={result} - isDisplayed={this.dictationOverlayVisible} - interactive={false} - dialogueBoxStyle={dialogueBoxStyle} - overlayStyle={overlayStyle} - closeOnExternalClick={this.initiateDictationFade} - />); + return <MainViewModal contents={result} isDisplayed={this.dictationOverlayVisible} interactive={false} dialogueBoxStyle={dialogueBoxStyle} overlayStyle={overlayStyle} closeOnExternalClick={this.initiateDictationFade} />; } -}
\ No newline at end of file +} diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 280ca8a8c..886dd974b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -132,20 +132,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; - styleFromLayoutString = (scale: number) => { - const style: { [key: string]: any } = {}; - const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'background', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; - const replacer = (match: any, expr: string, offset: any, string: any) => { - // bcz: this executes a script to convert a property expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? ''; - }; - divKeys.map((prop: string) => { - const p = (this.props as any)[prop]; - typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); - }); - return style; - }; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @computed public get annotationKey() { diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index bac51a11d..76e2d64a2 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -5,8 +5,8 @@ import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; -import { Cast, NumCast } from '../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; +import { Cast, DocCast, NumCast } from '../../fields/Types'; +import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { Docs } from '../documents/Documents'; @@ -21,11 +21,12 @@ import './DocumentButtonBar.scss'; import { Colors } from './global/globalEnums'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, DocumentViewInternal } from './nodes/DocumentView'; import { DashFieldView } from './nodes/formattedText/DashFieldView'; import { GoogleRef } from './nodes/formattedText/FormattedTextBox'; import { TemplateMenu } from './TemplateMenu'; import React = require('react'); +import { DocumentType } from '../documents/DocumentTypes'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -238,15 +239,13 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="documentButtonBar-icon" style={{ color: 'white' }} - onClick={e => - TabDocView.PinDoc( - this.props - .views() - .filter(v => v) - .map(dv => dv!.rootDoc), - { pinDocView: true } - ) - }> + onClick={e => { + const docs = this.props + .views() + .filter(v => v) + .map(dv => dv!.rootDoc); + TabDocView.PinDoc(docs, { pinDocView: !docs.some(doc => !e.shiftKey && doc.type === DocumentType.RTF), activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) }); + }}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div> </Tooltip> @@ -274,12 +273,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get menuButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? null : ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{`Open Context Menu`}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}> <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={e => this.openContextMenu(e)}> <FontAwesomeIcon className="documentdecorations-icon" icon="bars" /> </div> @@ -334,9 +328,37 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </Tooltip> ); } + + @observable _isRecording = false; + @computed + get recordButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? null : ( + <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}> + <div + className="documentButtonBar-icon" + style={{ backgroundColor: this._isRecording ? 'red' : targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }} + onClick={undoBatch( + action(e => { + this._isRecording = true; + this.props.views().map( + view => + view && + DocumentViewInternal.recordAudioAnnotation( + view.dataDoc, + view.LayoutFieldKey, + action(() => (this._isRecording = false)) + ) + ); + }) + )}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" /> + </div> + </Tooltip> + ); + } @observable _aliasDown = false; - onAliasButtonDown = action((e: React.PointerEvent): void => { - this.props.views()[0]?.select(false); + onTemplateButton = action((e: React.PointerEvent): void => { this._tooltipOpen = false; setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); }); @@ -374,7 +396,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </div> ) }> - <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onAliasButtonDown}> + <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}> {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} </div> </Flyout> @@ -412,15 +434,16 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> </div> ) : null} - {/*!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button"> - {this.templateButton} - </div> - /*<div className="documentButtonBar-button"> + <div className="documentButtonBar-button">{this.recordButton}</div> + { + Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div> + /*<div className="documentButtonBar-button"> {this.metadataButton} </div> <div className="documentButtonBar-button"> {this.contextButton} - </div> */} + </div> */ + } {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>} <div className="documentButtonBar-button">{this.pinButton}</div> {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>} diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index c55daca3f..832d0a35c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -157,6 +157,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const dragDocView = SelectionManager.Views()[0]; + if (DocListCast(Doc.MyOverlayDocs.data).includes(dragDocView.rootDoc)) return false; const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; const dragData = new DragManager.DocumentDragData( SelectionManager.Views().map(dv => dv.props.Document), @@ -200,7 +201,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const finished = action((force?: boolean) => { if ((force || --iconifyingCount === 0) && this._iconifyBatch) { if (this._deleteAfterIconify) { - views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); + views.forEach(iconView => { + Doc.setNativeView(iconView.props.Document); + if (iconView.props.Document.activeFrame) { + iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation. + } else { + iconView.props.removeDocument?.(iconView.props.Document); + } + }); SelectionManager.DeselectAll(); } this._iconifyBatch?.end(); @@ -350,8 +358,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerDown = (e: React.PointerEvent): void => { - DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc); - this._inkDragDocs = DragManager.docsBeingDragged + const views = SelectionManager.Views().map(dv => dv.rootDoc); + this._inkDragDocs = views .filter(doc => doc.type === DocumentType.INK) .map(doc => { if (InkStrokeProperties.Instance._lock) { @@ -468,6 +476,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = Document(docView.rootDoc); + if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) { + doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth; + } const nwidth = docView.nativeWidth; const nheight = docView.nativeHeight; let docheight = doc._height || 0; @@ -490,11 +501,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P let actualdW = Math.max(width + dW * scale, 20); let actualdH = Math.max(height + dH * scale, 20); const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen); - console.log(fixedAspect); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) { if (dragRight && modifyNativeDim) { - doc._nativeWidth = (actualdW / (doc._width || 1)) * Doc.NativeWidth(doc); + if (Doc.NativeWidth(doc)) { + doc._nativeWidth = (actualdW / (doc._width || 1)) * Doc.NativeWidth(doc); + } } else { if (!doc._fitWidth) { actualdH = (nheight / nwidth) * actualdW; @@ -519,7 +531,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } else doc._height = actualdH; } } else { - dH && (doc._height = actualdH); + const maxHeight = 0; //Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling(); + dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH); dW && (doc._width = actualdW); dH && (doc._autoHeight = false); } @@ -636,21 +649,21 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); const colorScheme = StrCast(Doc.ActiveDashboard?.colorScheme); - const titleArea = hideTitle ? null : this._editingTitle ? ( + const titleArea = this._editingTitle ? ( <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`} type="text" name="dynbox" autoComplete="on" - value={this._accumulatedTitle} - onBlur={e => this.titleBlur()} - onChange={action(e => (this._accumulatedTitle = e.target.value))} - onKeyDown={this.titleEntered} + value={hideTitle ? '' : this._accumulatedTitle} + onBlur={e => !hideTitle && this.titleBlur()} + onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} + onKeyDown={hideTitle ? emptyFunction : this.titleEntered} /> ) : ( <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}> - <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span> + <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span> </div> ); @@ -707,7 +720,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px', }}> {hideDeleteButton ? <div /> : topBtn('close', this.hasIcons ? 'times' : 'window-maximize', undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), 'Close')} - {hideTitle ? null : titleArea} + {titleArea} {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')} {hideResizers ? null : ( <> @@ -725,11 +738,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P </> )} - {useRotation && ( - <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> - {'⟲'} - </div> - )} + <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> + {'⟲'} + </div> {useRounding && ( <div diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index d7707a6fe..8036df471 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -3,7 +3,7 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; -import "./EditableView.scss"; +import './EditableView.scss'; export interface EditableProps { /** @@ -26,17 +26,16 @@ export interface EditableProps { contents: any; fontStyle?: string; fontSize?: number; - height?: number | "auto"; + height?: number | 'auto'; sizeToContent?: boolean; maxHeight?: number; display?: string; overflow?: string; autosuggestProps?: { resetValue: () => void; - value: string, - onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void, - autosuggestProps: Autosuggest.AutosuggestProps<string, any> - + value: string; + onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void; + autosuggestProps: Autosuggest.AutosuggestProps<string, any>; }; oneLine?: boolean; editing?: boolean; @@ -81,16 +80,16 @@ export class EditableView extends React.Component<EditableProps> { @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { switch (e.key) { - case "Tab": + case 'Tab': e.stopPropagation(); this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, false); this.props.OnTab?.(e.shiftKey); break; - case "Backspace": + case 'Backspace': e.stopPropagation(); if (!e.currentTarget.value) this.props.OnEmpty?.(); break; - case "Enter": + case 'Enter': e.stopPropagation(); if (!e.ctrlKey) { this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true); @@ -100,22 +99,26 @@ export class EditableView extends React.Component<EditableProps> { this.props.isEditingCallback?.(false); } break; - case "Escape": + case 'Escape': e.stopPropagation(); this._editing = false; this.props.isEditingCallback?.(false); break; - case ":": + case ':': this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; - case "Shift": case "Alt": case "Meta": case "Control": break; + case 'Shift': + case 'Alt': + case 'Meta': + case 'Control': + break; default: if (this.props.textCallback?.(e.key)) { this._editing = false; - this.props.isEditingCallback?.(false,); + this.props.isEditingCallback?.(false); } } - } + }; @action onClick = (e: React.MouseEvent) => { @@ -129,40 +132,44 @@ export class EditableView extends React.Component<EditableProps> { } e.stopPropagation(); } - } + }; @action finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean, enterKey: boolean) { if (this.props.SetValue(value, shiftDown, enterKey)) { this._editing = false; - this.props.isEditingCallback?.(false,); + this.props.isEditingCallback?.(false); } else { this._editing = false; this.props.isEditingCallback?.(false); - !lostFocus && setTimeout(action(() => { - this._editing = true; - this.props.isEditingCallback?.(true); - }), 0); + !lostFocus && + setTimeout( + action(() => { + this._editing = true; + this.props.isEditingCallback?.(true); + }), + 0 + ); } } - stopPropagation(e: React.SyntheticEvent) { e.stopPropagation(); } + stopPropagation(e: React.SyntheticEvent) { + e.stopPropagation(); + } @action setIsFocused = (value: boolean) => { const wasFocused = this._editing; this._editing = value; return wasFocused !== this._editing; - } - - + }; renderEditor() { - return this.props.autosuggestProps - ? <Autosuggest + return this.props.autosuggestProps ? ( + <Autosuggest {...this.props.autosuggestProps.autosuggestProps} inputProps={{ - className: "editableView-input", + className: 'editableView-input', onKeyDown: this.onKeyDown, autoFocus: true, onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true, false), @@ -171,39 +178,49 @@ export class EditableView extends React.Component<EditableProps> { onPointerUp: this.stopPropagation, onKeyPress: this.stopPropagation, value: this.props.autosuggestProps.value, - onChange: this.props.autosuggestProps.onChange + onChange: this.props.autosuggestProps.onChange, }} /> - : <input className="editableView-input" ref={this._inputref} - style={{ display: this.props.display, overflow: "auto", fontSize: this.props.fontSize, minWidth: 20, background: this.props.background }} + ) : ( + <input + className="editableView-input" + ref={this._inputref} + style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minWidth: 20, background: this.props.background }} placeholder={this.props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this.props.GetValue()} autoFocus={true} onKeyDown={this.onKeyDown} - onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} - />; + onKeyPress={this.stopPropagation} + onPointerDown={this.stopPropagation} + onClick={this.stopPropagation} + onPointerUp={this.stopPropagation} + /> + ); } render() { if (this._editing && this.props.GetValue() !== undefined) { - return this.props.sizeToContent ? - <div style={{ display: "grid", minWidth: 100 }}> - <div style={{ display: "inline-block", position: "relative", height: 0, width: "100%", overflow: "hidden" }}> - {this.props.GetValue()} - </div> + return this.props.sizeToContent ? ( + <div style={{ display: 'grid', minWidth: 100 }}> + <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{this.props.GetValue()}</div> {this.renderEditor()} - </div> : - this.renderEditor(); + </div> + ) : ( + this.renderEditor() + ); } setTimeout(() => this.props.autosuggestProps?.resetValue()); - return this.props.contents instanceof ObjectField ? (null) : - <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} - style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: "10px", whiteSpace: "nowrap", height: this.props.height || "auto", maxHeight: this.props.maxHeight }} - onClick={this.onClick} placeholder={this.props.placeholder}> - <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }} > - {this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} - </span> - </div>; + return this.props.contents instanceof ObjectField ? null : ( + <div + className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`} + ref={this._ref} + style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: 'nowrap', height: this.props.height || 'auto', maxHeight: this.props.maxHeight }} + onPointerDown={e => e.stopPropagation()} + onClick={this.onClick} + placeholder={this.props.placeholder}> + <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss index f5cbbffb1..bfe2d5c64 100644 --- a/src/client/views/GestureOverlay.scss +++ b/src/client/views/GestureOverlay.scss @@ -1,8 +1,9 @@ .gestureOverlay-cont { - width: 100vw; - height: 100vh; + width: 100%; + height: 100%; position: absolute; touch-action: none; + top: 0; .pointerBubbles { width: 100%; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 5ec4de953..850688e7e 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -1,7 +1,6 @@ import React = require('react'); import * as fitCurve from 'fit-curve'; -import { action, computed, observable, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; +import { action, computed, observable, runInAction, trace } from 'mobx'; import { Doc } from '../../fields/Doc'; import { InkData, InkTool } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -41,9 +40,13 @@ import { RadialMenu } from './nodes/RadialMenu'; import HorizontalPalette from './Palette'; import { Touchable } from './Touchable'; import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu'; +import { observer } from 'mobx-react'; +interface GestureOverlayProps { + isActive: boolean; +} @observer -export class GestureOverlay extends Touchable { +export class GestureOverlay extends Touchable<GestureOverlayProps> { static Instance: GestureOverlay; @observable public InkShape: string = ''; @@ -83,7 +86,7 @@ export class GestureOverlay extends Touchable { protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - constructor(props: Readonly<{}>) { + constructor(props: any) { super(props); GestureOverlay.Instance = this; @@ -495,7 +498,7 @@ export class GestureOverlay extends Touchable { } this._strokes = []; - this._points = []; + this._points.length = 0; this._possibilities = []; document.removeEventListener('touchend', this.handleHandUp); } @@ -582,7 +585,7 @@ export class GestureOverlay extends Touchable { }) ); } - if (!(e.target as any)?.className?.startsWith('lm_')) { + if (!(e.target as any)?.className?.toString().startsWith('lm_')) { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { Doc.ActiveTool = InkTool.Write; @@ -619,14 +622,14 @@ export class GestureOverlay extends Touchable { // get the two targets at the ends of the line const ep1 = this._points[0]; - const ep2 = this._points[this._points.length - 1]; + const ep2 = this._points.lastElement(); const target1 = document.elementFromPoint(ep1.X, ep1.Y); const target2 = document.elementFromPoint(ep2.X, ep2.Y); const ge = new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', { bubbles: true, detail: { - points: this._points, + points: this._points.slice(), gesture: GestureUtils.Gestures.Line, bounds: B, }, @@ -652,8 +655,8 @@ export class GestureOverlay extends Touchable { if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { switch (this.Tool) { case ToolglassTools.InkToText: - this._strokes.push(new Array(...this._points)); - this._points = []; + this._strokes.push(this._points.slice()); + this._points.length = 0; CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then(results => { const wordResults = results.filter((r: any) => r.category === 'line'); const possibilities: string[] = []; @@ -675,7 +678,7 @@ export class GestureOverlay extends Touchable { break; case ToolglassTools.IgnoreGesture: this.dispatchGesture(GestureUtils.Gestures.Stroke); - this._points = []; + this._points.length = 0; break; } } @@ -683,7 +686,7 @@ export class GestureOverlay extends Touchable { else if (this.InkShape) { this.makePolygon(this.InkShape, false); this.dispatchGesture(GestureUtils.Gestures.Stroke); - this._points = []; + this._points.length = 0; if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) { this.InkShape = ''; Doc.ActiveTool = InkTool.None; @@ -743,15 +746,16 @@ export class GestureOverlay extends Touchable { (controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y) ); if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0]; - this._points = controlPoints; + this._points.length = 0; + this._points.push(...controlPoints); this.dispatchGesture(GestureUtils.Gestures.Stroke); // TODO: nda - check inks to group here checkInksToGroup(); } - this._points = []; + this._points.length = 0; } } else { - this._points = []; + this._points.length = 0; } CollectionFreeFormViewChrome.Instance?.primCreated(); }; @@ -803,7 +807,7 @@ export class GestureOverlay extends Touchable { } } } - this._points = []; + this._points.length = 0; switch (shape) { //must push an extra point in the end so InteractionUtils knows pointer is up. //must be (points[0].X,points[0]-1) @@ -922,7 +926,7 @@ export class GestureOverlay extends Touchable { new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', { bubbles: true, detail: { - points: stroke ?? this._points, + points: stroke ?? this._points.slice(), gesture: gesture as any, bounds: this.getBounds(stroke ?? this._points), text: data, @@ -1067,8 +1071,8 @@ export class GestureOverlay extends Touchable { render() { return ( - <div className="gestureOverlay-cont" ref={this._overlayRef} onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}> - {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>} + <div className="gestureOverlay-cont" style={{ pointerEvents: this.props.isActive ? 'all' : 'none' }} ref={this._overlayRef} onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}> + {this.showMobileInkOverlay ? <MobileInkOverlay /> : null} {this.elements} <div @@ -1091,7 +1095,8 @@ export class GestureOverlay extends Touchable { pointerEvents: 'none', touchAction: 'none', display: this.showBounds ? 'unset' : 'none', - }}></div> + }} + /> <TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} /> </div> ); diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 73e0c9933..50518eec1 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -84,6 +84,7 @@ export class KeyManager { }); private unmodified = action((keyname: string, e: KeyboardEvent) => { + const hasFffView = SelectionManager.Views().some(dv => dv.props.CollectionFreeFormDocumentView?.()); switch (keyname) { case 'u': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { @@ -116,8 +117,8 @@ export class KeyManager { var doDeselect = true; if (SnappingManager.GetIsDragging()) { DragManager.AbortDrag(); - } else if (CollectionDockingView.Instance.HasFullScreen) { - CollectionDockingView.Instance.CloseFullScreen(); + } else if (CollectionDockingView.Instance?.HasFullScreen) { + CollectionDockingView.Instance?.CloseFullScreen(); } else if (CollectionStackedTimeline.SelectingRegion) { CollectionStackedTimeline.SelectingRegion = undefined; doDeselect = false; @@ -160,16 +161,16 @@ export class KeyManager { break; case 'arrowleft': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge(-1, 0)), 'nudge left'); - break; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowright': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(1, 0)), 'nudge right'); - break; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowup': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, -1)), 'nudge up'); - break; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowdown': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 1)), 'nudge down'); - break; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; } return { diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss index 664f2448b..bed7caf7f 100644 --- a/src/client/views/InkStroke.scss +++ b/src/client/views/InkStroke.scss @@ -1,22 +1,23 @@ .inkstroke-UI { - // transform-origin: top left; - position: absolute; - overflow: visible; - pointer-events: none; - z-index: 2001; // 1 higher than documentdecorations - - svg:not(:root) { - overflow: visible !important; + // transform-origin: top left; position: absolute; - left:0; - top:0; - } + overflow: visible; + pointer-events: none; + z-index: 2001; // 1 higher than documentdecorations + + svg:not(:root) { + overflow: visible !important; + position: absolute; + left: 0; + top: 0; + } } .inkStroke-wrapper { display: flex; align-items: center; height: 100%; + transition: inherit; .inkStroke { mix-blend-mode: multiply; stroke-linejoin: round; @@ -26,8 +27,10 @@ width: 100%; height: 100%; pointer-events: none; + transition: inherit; svg:not(:root) { overflow: visible !important; + transition: inherit; } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 821e2f739..d19a916f9 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -347,7 +347,6 @@ export class InkStrokeProperties { const deltaX = snapData.nearestPt.X - ink[controlIndex].X; const deltaY = snapData.nearestPt.Y - ink[controlIndex].Y; const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice()); - console.log('X = ' + snapData.nearestPt.X + ' ' + snapData.nearestPt.Y); return res; } } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 35d89d2b1..dae1c10bb 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -20,86 +20,95 @@ Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class */ -import React = require("react"); -import { action, IReactionDisposer, observable, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, WidthSym } from "../../fields/Doc"; -import { InkData, InkField, InkTool } from "../../fields/InkField"; -import { BoolCast, Cast, NumCast, RTFCast, StrCast } from "../../fields/Types"; -import { TraceMobx } from "../../fields/util"; -import { emptyFunction, OmitKeys, returnFalse, setupMoveUpEvents } from "../../Utils"; -import { CognitiveServices } from "../cognitive_services/CognitiveServices"; -import { InteractionUtils } from "../util/InteractionUtils"; -import { SnappingManager } from "../util/SnappingManager"; -import { Transform } from "../util/Transform"; -import { UndoManager } from "../util/UndoManager"; -import { ContextMenu } from "./ContextMenu"; -import { DocComponent, ViewBoxBaseComponent } from "./DocComponent"; -import { Colors } from "./global/globalEnums"; -import { InkControlPtHandles, InkEndPtHandles } from "./InkControlPtHandles"; -import "./InkStroke.scss"; -import { InkStrokeProperties } from "./InkStrokeProperties"; -import { InkTangentHandles } from "./InkTangentHandles"; -import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; -import Color = require("color"); -import { DocComponentView } from "./nodes/DocumentView"; +import React = require('react'); +import { action, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, HeightSym, WidthSym } from '../../fields/Doc'; +import { InkData, InkField, InkTool } from '../../fields/InkField'; +import { BoolCast, Cast, FieldValue, NumCast, RTFCast, StrCast } from '../../fields/Types'; +import { TraceMobx } from '../../fields/util'; +import { OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils'; +import { CognitiveServices } from '../cognitive_services/CognitiveServices'; +import { InteractionUtils } from '../util/InteractionUtils'; +import { SnappingManager } from '../util/SnappingManager'; +import { Transform } from '../util/Transform'; +import { UndoManager } from '../util/UndoManager'; +import { ContextMenu } from './ContextMenu'; +import { ViewBoxBaseComponent } from './DocComponent'; +import { Colors } from './global/globalEnums'; +import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles'; +import { InkStrokeProperties } from './InkStrokeProperties'; +import { InkTangentHandles } from './InkTangentHandles'; +import { DocComponentView } from './nodes/DocumentView'; +import { FieldView, FieldViewProps } from './nodes/FieldView'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { INK_MASK_SIZE } from './global/globalCssVariables.scss'; +import './InkStroke.scss'; +import Color = require('color'); +import { ComputedField } from '../../fields/ScriptField'; +import { listSpec } from '../../fields/Schema'; +import { List } from '../../fields/List'; +import { StyleProp } from './StyleProvider'; @observer export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { - static readonly MaskDim = 50000; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big) - public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } + static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big) + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(InkingStroke, fieldStr); + } public static IsClosed(inkData: InkData) { return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y; } private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated - private _selDisposer?: IReactionDisposer; + private _disposers: { [key: string]: IReactionDisposer } = {}; @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight) - @observable _nearestT?: number; // nearest t value within the nearest Bezier segment " - @observable _nearestScrPt?: { X: number, Y: number }; // nearst screen point on the ink stroke "" + @observable _nearestT?: number; // nearest t value within the nearest Bezier segment " + @observable _nearestScrPt?: { X: number; Y: number }; // nearst screen point on the ink stroke "" componentDidMount() { this.props.setContentView?.(this); - this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles - selected => !selected && (InkStrokeProperties.Instance._controlButton = false)); + this._disposers.selfDisper = reaction( + () => this.props.isSelected(), // react to stroke being deselected by turning off ink handles + selected => !selected && (InkStrokeProperties.Instance._controlButton = false) + ); } componentWillUnmount() { - this._selDisposer?.(); + Object.keys(this._disposers).forEach(key => this._disposers[key]()); } // transform is the inherited screentolocal xf plus any scaling that was done to make the stroke // fit within its panel (e.g., for content fitting views like Lightbox or multicolumn, etc) - screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1); + screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1); getAnchor = () => { console.log(document.activeElement); return this._subContentView?.getAnchor?.() || this.rootDoc; - } + }; scrollFocus = (textAnchor: Doc, smooth: boolean) => { return this._subContentView?.scrollFocus?.(textAnchor, smooth); - } + }; /** - * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space); - * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since - * the center of the ink stroke changes as the stroke is rotated. - */ + * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space); + * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since + * the center of the ink stroke changes as the stroke is rotated. + */ getCenter = (xf: Transform) => { const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); const angle = -NumCast(this.layoutDoc.rotation); const newPoints = inkData.map(pt => { - const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * inkScaleY / inkScaleX; - const newY = Math.sin(angle) * pt.X * inkScaleX / inkScaleY + Math.cos(angle) * pt.Y; + const newX = Math.cos(angle) * pt.X - (Math.sin(angle) * pt.Y * inkScaleY) / inkScaleX; + const newY = (Math.sin(angle) * pt.X * inkScaleX) / inkScaleY + Math.cos(angle) * pt.Y; return { X: newX, Y: newY }; }); const crx = (Math.max(...newPoints.map(np => np.X)) + Math.min(...newPoints.map(np => np.X))) / 2; const cry = (Math.max(...newPoints.map(np => np.Y)) + Math.min(...newPoints.map(np => np.Y))) / 2; - const cx = Math.cos(-angle) * crx - Math.sin(-angle) * cry * inkScaleY / inkScaleX; - const cy = Math.sin(-angle) * crx * inkScaleX / inkScaleY + Math.cos(-angle) * cry; + const cx = Math.cos(-angle) * crx - (Math.sin(-angle) * cry * inkScaleY) / inkScaleX; + const cy = (Math.sin(-angle) * crx * inkScaleX) / inkScaleY + Math.cos(-angle) * cry; const tc = xf.transformPoint((cx - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (cy - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2); return { X: tc[0], Y: tc[1] }; - } + }; /** * analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field, @@ -107,7 +116,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { */ analyzeStrokes() { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]); } /** @@ -115,12 +124,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { * When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will * appear to illuminate what it covers up. At the same time, all pixels that are not under the stroke will be * dimmed by a semi-opaque overlay mask. - */ + */ public static toggleMask = action((inkDoc: Doc) => { inkDoc.isInkMask = !inkDoc.isInkMask; - inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; - inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined; - inkDoc.color = "#9b9b9bff"; + inkDoc._backgroundColor = inkDoc.isInkMask ? 'rgba(0,0,0,0.7)' : undefined; + inkDoc.mixBlendMode = inkDoc.isInkMask ? 'hard-light' : undefined; + inkDoc.color = '#9b9b9bff'; inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; }); /** @@ -132,46 +141,60 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { this._handledClick = false; const inkView = this.props.docViewPath().lastElement(); const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); - const screenPts = inkData.map(point => this.screenToLocal().inverse().transformPoint( - (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, - (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const screenPts = inkData + .map(point => + this.screenToLocal() + .inverse() + .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2) + ) + .map(p => ({ X: p[0], Y: p[1] })); const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); const controlIndex = nearestSeg; const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex; var controlUndo: UndoManager.Batch | undefined; const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected(); - setupMoveUpEvents(this, e, - !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => { - if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); - const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] }); - const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 }); - InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); - InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3); - return false; - }), - !isEditing ? returnFalse : action(() => { - controlUndo?.end(); - controlUndo = undefined; - UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); - }), + setupMoveUpEvents( + this, + e, + !isEditing + ? returnFalse + : action((e: PointerEvent, down: number[], delta: number[]) => { + if (!controlUndo) controlUndo = UndoManager.StartBatch('drag ink ctrl pt'); + const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] }); + const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 }); + InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); + InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3); + return false; + }), + !isEditing + ? returnFalse + : action(() => { + controlUndo?.end(); + controlUndo = undefined; + UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']); + }), action((e: PointerEvent, doubleTap: boolean | undefined) => { doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick; if (doubleTap) { InkStrokeProperties.Instance._controlButton = true; InkStrokeProperties.Instance._currentPoint = -1; - this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView + this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView if (isEditing) { this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); } } - }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1))); - } + }), + isEditing, + isEditing, + action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1)) + ); + }; /** * @param scrPt a point in the screen coordinate space * @returns the point in the ink data's coordinate space. */ - ptFromScreen = (scrPt: { X: number, Y: number }) => { + ptFromScreen = (scrPt: { X: number; Y: number }) => { const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); const docPt = this.screenToLocal().transformPoint(scrPt.X, scrPt.Y); const inkPt = { @@ -179,39 +202,39 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { Y: (docPt[1] - inkStrokeWidth / 2) / inkScaleY + inkStrokeWidth / 2 + inkTop, }; return inkPt; - } + }; /** * @param inkPt a point in the ink data's coordinate space * @returns the screen point corresponding to the ink point */ - ptToScreen = (inkPt: { X: number, Y: number }) => { + ptToScreen = (inkPt: { X: number; Y: number }) => { const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); const docPt = { X: (inkPt.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, - Y: (inkPt.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2 + Y: (inkPt.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2, }; const scrPt = this.screenToLocal().inverse().transformPoint(docPt.X, docPt.Y); return { X: scrPt[0], Y: scrPt[1] }; - } + }; /** - * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs' - * @param scrPt - the point to snap to this stroke - * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments) - * - * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point - */ - snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => { + * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs' + * @param scrPt - the point to snap to this stroke + * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments) + * + * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point + */ + snapPt = (scrPt: { X: number; Y: number }, excludeSegs?: number[]) => { const { inkData } = this.inkScaledData(); const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []); return { nearestPt, distance: distance * this.screenToLocal().inverse().Scale }; - } + }; /** - * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale - * factor for converting between ink and screen space. - */ + * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale + * factor for converting between ink and screen space. + */ inkScaledData = () => { const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1); @@ -228,27 +251,31 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { inkLeft, inkWidth, inkHeight, - inkScaleX: ((this.props.PanelWidth() - inkStrokeWidth) / ((inkWidth - inkStrokeWidth) || 1) || 1), - inkScaleY: ((this.props.PanelHeight() - inkStrokeWidth) / ((inkHeight - inkStrokeWidth) || 1) || 1) + inkScaleX: (this.props.PanelWidth() - inkStrokeWidth) / (inkWidth - inkStrokeWidth || 1) || 1, + inkScaleY: (this.props.PanelHeight() - inkStrokeWidth) / (inkHeight - inkStrokeWidth || 1) || 1, }; - } + }; // - // this updates the highlight for the nearest point on the curve to the cursor. + // this updates the highlight for the nearest point on the curve to the cursor. // if the user double clicks, this highlighted point will be added as a control point in the curve. // @action onPointerMove = (e: React.PointerEvent) => { const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); - const screenPts = inkData.map(point => this.screenToLocal().inverse().transformPoint( - (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, - (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const screenPts = inkData + .map(point => + this.screenToLocal() + .inverse() + .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2) + ) + .map(p => ({ X: p[0], Y: p[1] })); const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); this._nearestT = nearestT; this._nearestSeg = nearestSeg; this._nearestScrPt = nearestPt; - } + }; /** * @returns the nearest screen point to the cursor (to render a highlight for the point to be added) @@ -263,50 +290,66 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { componentUI = (boundsLeft: number, boundsTop: number) => { const inkDoc = this.props.Document; const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); - const screenSpaceCenterlineStrokeWidth = Math.min(3, inkStrokeWidth * this.screenToLocal().inverse().Scale); // the width of the blue line widget that shows the centerline of the ink stroke + const screenSpaceCenterlineStrokeWidth = Math.min(3, inkStrokeWidth * this.screenToLocal().inverse().Scale); // the width of the blue line widget that shows the centerline of the ink stroke const screenInkWidth = this.screenToLocal().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth); - const screenPts = inkData.map(point => this.screenToLocal().inverse().transformPoint( - (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, - (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const screenPts = inkData + .map(point => + this.screenToLocal() + .inverse() + .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2) + ) + .map(p => ({ X: p[0], Y: p[1] })); const screenHdlPts = screenPts; const startMarker = StrCast(this.layoutDoc.strokeStartMarker); const endMarker = StrCast(this.layoutDoc.strokeEndMarker); const markerScale = NumCast(this.layoutDoc.strokeMarkerScale); - return SnappingManager.GetIsDragging() ? (null) : - !InkStrokeProperties.Instance._controlButton ? - (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) : - <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> - <InkEndPtHandles - inkView={this.props.docViewPath().lastElement()} - inkDoc={inkDoc} - startPt={screenPts[0]} - endPt={screenPts.lastElement()} - screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> - </div>) : + return SnappingManager.GetIsDragging() ? null : !InkStrokeProperties.Instance._controlButton ? ( + !this.props.isSelected() || InkingStroke.IsClosed(inkData) ? null : ( <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> - {InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, - StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier), - "none", startMarker, endMarker, markerScale * Math.min(screenSpaceCenterlineStrokeWidth, screenInkWidth[0] / screenSpaceCenterlineStrokeWidth), StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)} - <InkControlPtHandles - inkView={this.props.docViewPath().lastElement()} - inkDoc={inkDoc} - inkCtrlPoints={inkData} - screenCtrlPoints={screenHdlPts} - nearestScreenPt={this.nearestScreenPt} - screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> - <InkTangentHandles - inkView={this.props.docViewPath().lastElement()} - inkDoc={inkDoc} - screenCtrlPoints={screenHdlPts} - screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} - ScreenToLocalTransform={this.screenToLocal} /> - </div>; - } + <InkEndPtHandles inkView={this.props.docViewPath().lastElement()} inkDoc={inkDoc} startPt={screenPts[0]} endPt={screenPts.lastElement()} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> + </div> + ) + ) : ( + <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> + {InteractionUtils.CreatePolyline( + screenPts, + 0, + 0, + Colors.MEDIUM_BLUE, + screenInkWidth[0], + screenSpaceCenterlineStrokeWidth, + StrCast(inkDoc.strokeLineJoin), + StrCast(this.layoutDoc.strokeLineCap), + StrCast(inkDoc.strokeBezier), + 'none', + startMarker, + endMarker, + markerScale * Math.min(screenSpaceCenterlineStrokeWidth, screenInkWidth[0] / screenSpaceCenterlineStrokeWidth), + StrCast(inkDoc.strokeDash), + 1, + 1, + '', + 'none', + 1.0, + false + )} + <InkControlPtHandles + inkView={this.props.docViewPath().lastElement()} + inkDoc={inkDoc} + inkCtrlPoints={inkData} + screenCtrlPoints={screenHdlPts} + nearestScreenPt={this.nearestScreenPt} + screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} + /> + <InkTangentHandles inkView={this.props.docViewPath().lastElement()} inkDoc={inkDoc} screenCtrlPoints={screenHdlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} /> + </div> + ); + }; _subContentView: DocComponentView | undefined; - setSubContentView = (doc: DocComponentView) => this._subContentView = doc; + setSubContentView = (doc: DocComponentView) => (this._subContentView = doc); render() { TraceMobx(); const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData(); @@ -315,105 +358,196 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { const endMarker = StrCast(this.layoutDoc.strokeEndMarker); const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1); const closed = InkingStroke.IsClosed(inkData); - const fillColor = StrCast(this.layoutDoc.fillColor, "transparent"); - const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color); + const isInkMask = BoolCast(this.layoutDoc.isInkMask); + const fillColor = isInkMask ? '#aaaaaa' : StrCast(this.layoutDoc.fillColor, 'transparent'); + const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color); + + // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be. + if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) { + setTimeout(() => { + this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[WidthSym]())); + this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[HeightSym]())); + }); + } // Visually renders the polygonal line made by the user. - const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth, - StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), - StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker, - markerScale, StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false); + const inkLine = InteractionUtils.CreatePolyline( + inkData, + inkLeft, + inkTop, + strokeColor, + inkStrokeWidth, + inkStrokeWidth, + StrCast(this.layoutDoc.strokeLineJoin), + StrCast(this.layoutDoc.strokeLineCap), + StrCast(this.layoutDoc.strokeBezier), + !closed ? 'none' : fillColor === 'transparent' ? 'none' : fillColor, + startMarker, + endMarker, + markerScale, + StrCast(this.layoutDoc.strokeDash), + inkScaleX, + inkScaleY, + '', + 'none', + 1.0, + false + ); const highlightIndex = /*BoolCast(this.props.Document.isLinkButton) && */ Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString - const highlightColor = !highlightIndex ? - StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") : - ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex]; + const highlightColor = !highlightIndex + ? StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent') + : ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'yellow', 'magenta', 'cyan', 'orange'][highlightIndex]; // Invisible polygonal line that enables the ink to be selected by the user. - const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, suppressFill: boolean = false) => InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor, - inkStrokeWidth, fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? closed ? 0 : (highlightIndex + 2) : 0), - StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), - StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" || suppressFill ? "none" : fillColor, startMarker, endMarker, - markerScale, undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents?.() ?? (this.rootDoc._lockedPosition ? "none" : "visiblepainted"), 0.0, - false, downHdlr); - const fsize = +(StrCast(this.props.Document.fontSize, "12px").replace("px", "")); - // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size. + const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, suppressFill: boolean = false) => + InteractionUtils.CreatePolyline( + inkData, + inkLeft, + inkTop, + highlightColor, + inkStrokeWidth, + Math.max(5, fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? (closed ? 0 : highlightIndex + 2) : 0)), + StrCast(this.layoutDoc.strokeLineJoin), + StrCast(this.layoutDoc.strokeLineCap), + StrCast(this.layoutDoc.strokeBezier), + !closed ? 'none' : fillColor === 'transparent' || suppressFill ? 'none' : fillColor, + '', + '', + markerScale, + undefined, + inkScaleX, + inkScaleY, + '', + this.props.pointerEvents?.() ?? (this.rootDoc._lockedPosition ? 'none' : 'visiblepainted'), + 0.0, + false, + downHdlr + ); + const fsize = +StrCast(this.props.Document.fontSize, '12px').replace('px', ''); + // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size. // this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429 // see: https://bibwild.wordpress.com/2019/06/10/bootstrap-3-to-4-changes-in-how-font-size-line-height-and-spacing-is-done-or-what-happened-to-line-height-computed/ - const lineHeightGuess = (+getComputedStyle(document.body).lineHeight.replace("px", "")) / (+getComputedStyle(document.body).fontSize.replace("px", "")); + const lineHeightGuess = +getComputedStyle(document.body).lineHeight.replace('px', '') / +getComputedStyle(document.body).fontSize.replace('px', ''); const interactions = { - onPointerLeave: action(() => this._nearestScrPt = undefined), + onPointerLeave: action(() => (this._nearestScrPt = undefined)), onPointerMove: this.props.isSelected() ? this.onPointerMove : undefined, onClick: (e: React.MouseEvent) => this._handledClick && e.stopPropagation(), onContextMenu: () => { const cm = ContextMenu.Instance; - !Doc.noviceMode && cm?.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" }); - cm?.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" }); - cm?.addItem({ description: "Edit Points", event: action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton), icon: "paint-brush" }); - } + !Doc.noviceMode && cm?.addItem({ description: 'Recognize Writing', event: this.analyzeStrokes, icon: 'paint-brush' }); + cm?.addItem({ description: 'Toggle Mask', event: () => InkingStroke.toggleMask(this.rootDoc), icon: 'paint-brush' }); + cm?.addItem({ description: 'Edit Points', event: action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)), icon: 'paint-brush' }); + }, }; - return <div className="inkStroke-wrapper"> - <svg className="inkStroke" - style={{ - transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, - mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", - cursor: this.props.isSelected() ? "default" : undefined - }} - {...(!closed ? interactions : {})} - > - {closed ? inkLine : clickableLine(this.onPointerDown)} - {closed ? clickableLine(this.onPointerDown) : inkLine} - </svg> - {!closed || (!RTFCast(this.rootDoc.text)?.Text && !this.props.isSelected()) ? (null) : - <div className="inkStroke-text" style={{ - color: StrCast(this.layoutDoc.textColor, "black"), - pointerEvents: this.props.isDocumentActive?.() ? "all" : undefined, - width: this.layoutDoc[WidthSym](), - transform: `scale(${this.props.scaling?.() || 1})`, - transformOrigin: "top left", - top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.scaling?.() || 1)) / 2 - }}> - <FormattedTextBox - {...OmitKeys(this.props, ['children']).omit} - setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text) - yPadding={10} - xPadding={10} - fieldKey={"text"} - fontSize={fsize} - dontRegisterView={true} - noSidebar={true} - dontScale={true} - isContentActive={this.isContentActive} - /> - </div> - } - {!closed ? null : <svg className="inkStroke" - style={{ - transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, - mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", - cursor: this.props.isSelected() ? "default" : undefined, position: "absolute" - }} - {...interactions} - > - {clickableLine(this.onPointerDown, true)} - </svg>} - </div>; + return ( + <div className="inkStroke-wrapper"> + <svg + className="inkStroke" + style={{ + transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, + // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset', + cursor: this.props.isSelected() ? 'default' : undefined, + }} + {...(!closed ? interactions : {})}> + {closed ? inkLine : clickableLine(this.onPointerDown)} + {closed ? clickableLine(this.onPointerDown) : inkLine} + </svg> + {!closed || (!RTFCast(this.rootDoc.text)?.Text && !this.props.isSelected()) ? null : ( + <div + className="inkStroke-text" + style={{ + color: StrCast(this.layoutDoc.textColor, 'black'), + pointerEvents: this.props.isDocumentActive?.() ? 'all' : undefined, + width: this.layoutDoc[WidthSym](), + transform: `scale(${this.props.NativeDimScaling?.() || 1})`, + transformOrigin: 'top left', + top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2, + }}> + <FormattedTextBox + {...OmitKeys(this.props, ['children']).omit} + setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text) + yPadding={10} + xPadding={10} + fieldKey={'text'} + fontSize={fsize} + dontRegisterView={true} + noSidebar={true} + dontScale={true} + isContentActive={this.isContentActive} + /> + </div> + )} + {!closed ? null : ( + <svg + className="inkStroke" + style={{ + transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, + mixBlendMode: 'unset', + cursor: this.props.isSelected() ? 'default' : undefined, + position: 'absolute', + }} + {...interactions}> + {clickableLine(this.onPointerDown, true)} + </svg> + )} + </div> + ); } } - -export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); } -export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); } -export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); } -export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); } -export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); } -export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); } -export function SetActiveArrowScale(value: number) { ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); } -export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); } -export function ActiveInkPen(): Doc { return Doc.UserDoc(); } -export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); } -export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); } -export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); } -export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); } -export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } -export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); } -export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } -export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } +export function SetActiveInkWidth(width: string): void { + !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); +} +export function SetActiveBezierApprox(bezier: string): void { + ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier); +} +export function SetActiveInkColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeInkColor = value); +} +export function SetActiveIsInkMask(value: boolean) { + ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value); +} +export function SetActiveFillColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeFillColor = value); +} +export function SetActiveArrowStart(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); +} +export function SetActiveArrowEnd(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); +} +export function SetActiveArrowScale(value: number) { + ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); +} +export function SetActiveDash(dash: string): void { + !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); +} +export function ActiveInkPen(): Doc { + return Doc.UserDoc(); +} +export function ActiveInkColor(): string { + return StrCast(ActiveInkPen()?.activeInkColor, 'black'); +} +export function ActiveFillColor(): string { + return StrCast(ActiveInkPen()?.activeFillColor, ''); +} +export function ActiveIsInkMask(): boolean { + return BoolCast(ActiveInkPen()?.activeIsInkMask, false); +} +export function ActiveArrowStart(): string { + return StrCast(ActiveInkPen()?.activeArrowStart, ''); +} +export function ActiveArrowEnd(): string { + return StrCast(ActiveInkPen()?.activeArrowEnd, ''); +} +export function ActiveArrowScale(): number { + return NumCast(ActiveInkPen()?.activeArrowScale, 1); +} +export function ActiveDash(): string { + return StrCast(ActiveInkPen()?.activeDash, '0'); +} +export function ActiveInkWidth(): number { + return Number(ActiveInkPen()?.activeInkWidth); +} +export function ActiveInkBezierApprox(): string { + return StrCast(ActiveInkPen()?.activeInkBezier); +} diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss index 5d42cd97f..ae5dc9902 100644 --- a/src/client/views/LightboxView.scss +++ b/src/client/views/LightboxView.scss @@ -1,35 +1,55 @@ - - .lightboxView-navBtn { - margin: auto; - position: absolute; - right: 10; - top: 10; - background: transparent; - border-radius: 8; - color:white; - opacity: 0.7; - width: 35; - &:hover { - opacity: 1; - } +.lightboxView-navBtn { + margin: auto; + position: absolute; + right: 10; + top: 10; + background: transparent; + border-radius: 8; + color: white; + opacity: 0.7; + width: 25; + flex-direction: column; + display: flex; + &:hover { + opacity: 1; } - .lightboxView-tabBtn { - margin: auto; - position: absolute; - right: 35; - top: 10; - background: transparent; - border-radius: 8; - color:white; - opacity: 0.7; - width: 35; - &:hover { - opacity: 1; - } +} +.lightboxView-tabBtn { + margin: auto; + position: absolute; + right: 38; + top: 10; + background: transparent; + border-radius: 8; + color: white; + opacity: 0.7; + width: 25; + flex-direction: column; + display: flex; + &:hover { + opacity: 1; + } +} +.lightboxView-penBtn { + margin: auto; + position: absolute; + right: 70; + top: 10; + background: transparent; + border-radius: 8; + color: white; + opacity: 0.7; + width: 25; + flex-direction: column; + display: flex; + &:hover { + opacity: 1; } +} .lightboxView-frame { - position: absolute; - top: 0; left: 0; + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; background: #000000bb; @@ -51,4 +71,4 @@ } } } -}
\ No newline at end of file +} diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 99d50b4a2..cb5094f4b 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -1,9 +1,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; -import 'normalize.css'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { InkTool } from '../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils'; import { DocUtils } from '../documents/Documents'; @@ -12,7 +12,9 @@ import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { Transform } from '../util/Transform'; import { CollectionDockingView } from './collections/CollectionDockingView'; +import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline'; import { TabDocView } from './collections/TabDocView'; +import { GestureOverlay } from './GestureOverlay'; import './LightboxView.scss'; import { DocumentView } from './nodes/DocumentView'; import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; @@ -56,6 +58,11 @@ export class LightboxView extends React.Component<LightboxViewProps> { const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen'); } + CollectionStackedTimeline.CurrentlyPlaying?.forEach(doc => { + DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => { + dv.ComponentView?.Pause?.(); + }); + }); //TabDocView.PinDoc(doc, { hidePresBox: true }); this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]); if (doc !== LightboxView.LightboxDoc) { @@ -261,45 +268,48 @@ export class LightboxView extends React.Component<LightboxViewProps> { clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`, }}> {/* <CollectionMenu /> TODO:glr This is where it would go*/} - <DocumentView - ref={action((r: DocumentView | null) => { - LightboxView._docView = r !== null ? r : undefined; - r && - setTimeout( - action(() => { - const target = LightboxView._docTarget; - const doc = LightboxView._doc; - const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target); - if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.(); - //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here? it breaks smooth navigation in lightbox using 'next' button - }) - ); - })} - Document={LightboxView.LightboxDoc} - DataDoc={undefined} - LayoutTemplate={LightboxView.LightboxDocTemplate} - addDocument={undefined} - isDocumentActive={returnFalse} - isContentActive={returnTrue} - addDocTab={this.addDocTab} - pinToPres={TabDocView.PinDoc} - rootSelected={returnTrue} - docViewPath={returnEmptyDoclist} - docFilters={this.docFilters} - removeDocument={undefined} - styleProvider={DefaultStyleProvider} - ScreenToLocalTransform={this.lightboxScreenToLocal} - PanelWidth={this.lightboxWidth} - PanelHeight={this.lightboxHeight} - focus={DocUtils.DefaultFocus} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - renderDepth={0} - /> + + <GestureOverlay isActive={true}> + <DocumentView + ref={action((r: DocumentView | null) => { + LightboxView._docView = r !== null ? r : undefined; + r && + setTimeout( + action(() => { + const target = LightboxView._docTarget; + const doc = LightboxView._doc; + const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.(); + //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here? it breaks smooth navigation in lightbox using 'next' button + }) + ); + })} + Document={LightboxView.LightboxDoc} + DataDoc={undefined} + LayoutTemplate={LightboxView.LightboxDocTemplate} + addDocument={undefined} + isDocumentActive={returnFalse} + isContentActive={returnTrue} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + rootSelected={returnTrue} + docViewPath={returnEmptyDoclist} + docFilters={this.docFilters} + removeDocument={undefined} + styleProvider={DefaultStyleProvider} + ScreenToLocalTransform={this.lightboxScreenToLocal} + PanelWidth={this.lightboxWidth} + PanelHeight={this.lightboxHeight} + focus={DocUtils.DefaultFocus} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + renderDepth={0} + /> + </GestureOverlay> </div> {this.navBtn( @@ -327,6 +337,15 @@ export class LightboxView extends React.Component<LightboxViewProps> { )} <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} /> <div + className="lightboxView-navBtn" + title={'toggle fit width'} + onClick={e => { + e.stopPropagation(); + LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth; + }}> + <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" /> + </div> + <div className="lightboxView-tabBtn" title={'open in tab'} onClick={e => { @@ -339,13 +358,14 @@ export class LightboxView extends React.Component<LightboxViewProps> { <FontAwesomeIcon icon={'file-download'} size="2x" /> </div> <div - className="lightboxView-navBtn" - title={'toggle fit width'} + className="lightboxView-penBtn" + title="toggle pen annotation" + style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }} onClick={e => { e.stopPropagation(); - LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth; + Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; }}> - <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" /> + <FontAwesomeIcon color={Doc.ActiveTool === InkTool.Pen ? 'black' : 'white'} icon={'pen'} size="2x" /> </div> </div> ); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index e998f1fb9..4cb1183d0 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { AssignAllExtensions } from '../../extensions/General/Extensions'; +import { Utils } from '../../Utils'; +import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { LinkManager } from '../util/LinkManager'; // this must come before importing Docs and CurrentUserUtils @@ -19,31 +21,28 @@ AssignAllExtensions(); MainView.Live = window.location.search.includes('live'); window.location.search.includes('safe') && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); - if (info.id !== '__guest__') { - // a guest will not have an id registered - await CurrentUserUtils.loadUserDocument(info.id); - } else { - await Docs.Prototypes.initialize(); + if (info.email === 'guest') DocServer.Control.makeReadOnly(); + await CurrentUserUtils.loadUserDocument(info.id); + setTimeout(() => { + document.getElementById('root')!.addEventListener( + 'wheel', + event => { + if (event.ctrlKey) { + event.preventDefault(); + } + }, + true + ); + const startload = (document as any).startLoad; + const loading = Date.now() - (startload ? Number(startload) : Date.now() - 3000); + console.log('Loading Time = ' + loading); + const d = new Date(); + d.setTime(d.getTime() + 100 * 24 * 60 * 60 * 1000); + const expires = 'expires=' + d.toUTCString(); + document.cookie = `loadtime=${loading};${expires};path=/`; new LinkManager(); - } - document.getElementById('root')!.addEventListener( - 'wheel', - event => { - if (event.ctrlKey) { - event.preventDefault(); - } - }, - true - ); - const startload = (document as any).startLoad; - const loading = Date.now() - (startload ? Number(startload) : Date.now() - 3000); - console.log('Loading Time = ' + loading); - const d = new Date(); - d.setTime(d.getTime() + 100 * 24 * 60 * 60 * 1000); - const expires = 'expires=' + d.toUTCString(); - document.cookie = `loadtime=${loading};${expires};path=/`; - new LinkManager(); - new TrackMovements(); - new ReplayMovements(); - ReactDOM.render(<MainView />, document.getElementById('root')); + new TrackMovements(); + new ReplayMovements(); + ReactDOM.render(<MainView />, document.getElementById('root')); + }, 0); })(); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index ad87fb874..c5ac1cf52 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -1,5 +1,10 @@ @import 'global/globalCssVariables'; @import 'nodeModuleOverrides'; +h1, +.h1 { + // reverts change to h1 made by normalize.css + font-size: 36px; +} .dash-tooltip { font-size: 11px; @@ -133,7 +138,6 @@ position: absolute; right: 0; top: 0; - z-index: 3000; } .mainView-propertiesDragger { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 84768a6f0..79f83b386 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -3,8 +3,9 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons'; import * as far from '@fortawesome/free-regular-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, configure, observable, reaction } from 'mobx'; +import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import 'normalize.css'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; @@ -21,6 +22,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; import { Hypothesis } from '../util/HypothesisUtils'; +import { ReportManager } from '../util/ReportManager'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SelectionManager } from '../util/SelectionManager'; import { ColorScheme, SettingsManager } from '../util/SettingsManager'; @@ -78,12 +80,15 @@ export class MainView extends React.Component { @observable private _panelContent: string = 'none'; @observable private _sidebarContent: any = Doc.MyLeftSidebarPanel; @observable private _leftMenuFlyoutWidth: number = 0; + @computed get _hideUI() { + return this.mainDoc && this.mainDoc._viewType !== CollectionViewType.Docking; + } @computed private get dashboardTabHeight() { - return 27; + return this._hideUI ? 0 : 27; } // 27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js @computed private get topOfDashUI() { - return Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', '')); + return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', '')); } @computed private get topOfHeaderBarDoc() { return this.topOfDashUI; @@ -106,7 +111,12 @@ export class MainView extends React.Component { @computed private get colorScheme() { return StrCast(Doc.ActiveDashboard?.colorScheme); } + @observable mainDoc: Opt<Doc>; @computed private get mainContainer() { + if (window.location.pathname.startsWith('/doc/')) { + DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main => runInAction(() => (this.mainDoc = main as Doc))); + return this.mainDoc; + } return this.userDoc ? Doc.ActiveDashboard : Doc.GuestDashboard; } @computed private get headerBarDoc() { @@ -117,10 +127,10 @@ export class MainView extends React.Component { } headerBarDocWidth = () => this.mainDocViewWidth(); - headerBarDocHeight = () => SettingsManager.headerBarHeight ?? 0; - topMenuHeight = () => 35; + headerBarDocHeight = () => (this._hideUI ? 0 : SettingsManager.headerBarHeight ?? 0); + topMenuHeight = () => (this._hideUI ? 0 : 35); topMenuWidth = returnZero; // value is ignored ... - leftMenuWidth = () => Number(LEFT_MENU_WIDTH.replace('px', '')); + leftMenuWidth = () => (this._hideUI ? 0 : Number(LEFT_MENU_WIDTH.replace('px', ''))); leftMenuHeight = () => this._dashUIHeight; leftMenuFlyoutWidth = () => this._leftMenuFlyoutWidth; leftMenuFlyoutHeight = () => this._dashUIHeight; @@ -146,12 +156,15 @@ export class MainView extends React.Component { if (!MainView.Live) { DocServer.setPlaygroundFields([ 'dataTransition', + 'viewTransition', 'treeViewOpen', 'showSidebar', 'sidebarWidthPercent', - 'viewTransition', + 'currentTimecode', + 'timelineHeightPercent', 'panX', 'panY', + 'fitWidth', 'nativeWidth', 'nativeHeight', 'text-scrollHeight', @@ -163,6 +176,8 @@ export class MainView extends React.Component { 'curPage', 'viewType', 'chromeHidden', + 'currentFrame', + 'width', 'nativeWidth', ]); // can play with these fields on someone else's } @@ -196,7 +211,7 @@ export class MainView extends React.Component { componentWillUnMount() { window.removeEventListener('keyup', KeyManager.Instance.unhandle); window.removeEventListener('keydown', KeyManager.Instance.handle); - window.removeEventListener('pointerdown', this.globalPointerDown); + window.removeEventListener('pointerdown', this.globalPointerDown, true); window.removeEventListener('paste', KeyManager.Instance.paste as any); document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener); } @@ -213,7 +228,8 @@ export class MainView extends React.Component { const pathname = window.location.pathname.substr(1).split('/'); if (pathname.length > 1 && pathname[0] === 'doc') { Doc.MainDocId = pathname[1]; - !this.userDoc && DocServer.GetRefField(pathname[1]).then(action(field => field instanceof Doc && (Doc.GuestTarget = field))); + //!this.userDoc && + DocServer.GetRefField(pathname[1]).then(action(field => field instanceof Doc && (Doc.GuestTarget = field))); } } @@ -447,6 +463,7 @@ export class MainView extends React.Component { fa.faVolumeDown, fa.faSquareRootAlt, fa.faVolumeMute, + fa.faUserCircle, ] ); this.initAuthenticationRouters(); @@ -456,7 +473,11 @@ export class MainView extends React.Component { AudioBox.Enabled = true; const targets = document.elementsFromPoint(e.x, e.y); if (targets.length) { - const targClass = targets[0].className.toString(); + let targClass = targets[0].className.toString(); + for (let i = 0; i < targets.length - 1; i++) { + if (typeof targets[i].className === 'object') targClass = targets[i + 1].className.toString(); + else break; + } !targClass.includes('contextMenu') && ContextMenu.Instance.closeMenu(); !['timeline-menu-desc', 'timeline-menu-item', 'timeline-menu-input'].includes(targClass) && TimelineMenu.Instance.closeMenu(); } @@ -529,7 +550,7 @@ export class MainView extends React.Component { return () => (this._exploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); } headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1); - + mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1); @computed get headerBarDocView() { return ( <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}> @@ -569,7 +590,7 @@ export class MainView extends React.Component { @computed get mainDocView() { return ( <> - {this.headerBarDocView} + {this._hideUI ? null : this.headerBarDocView} <DocumentView key="main" Document={this.mainContainer!} @@ -578,11 +599,11 @@ export class MainView extends React.Component { addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} docViewPath={returnEmptyDoclist} - styleProvider={undefined} + styleProvider={this._hideUI ? DefaultStyleProvider : undefined} rootSelected={returnTrue} isContentActive={returnTrue} removeDocument={undefined} - ScreenToLocalTransform={Transform.Identity} + ScreenToLocalTransform={this._hideUI ? this.mainScreenToLocalXf : Transform.Identity} PanelWidth={this.mainDocViewWidth} PanelHeight={this.mainDocViewHeight} focus={DocUtils.DefaultFocus} @@ -594,7 +615,7 @@ export class MainView extends React.Component { ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} suppressSetHeight={true} - renderDepth={-1} + renderDepth={this._hideUI ? 0 : -1} /> </> ); @@ -602,7 +623,7 @@ export class MainView extends React.Component { @computed get dockingContent() { return ( - <GestureOverlay> + <GestureOverlay isActive={LightboxView.LightboxDoc ? false : true}> <div key="docking" className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`} @@ -649,21 +670,16 @@ export class MainView extends React.Component { const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':'); const locationParams = locationFields.length > 1 ? locationFields[1] : ''; if (doc.dockingConfig) return DashboardView.openDashboard(doc); + // prettier-ignore switch (locationFields[0]) { - case 'dashboard': - return DashboardView.openDashboard(doc); - case 'close': - return CollectionDockingView.CloseSplit(doc, locationParams); - case 'fullScreen': - return CollectionDockingView.OpenFullScreen(doc); - case 'lightbox': - return LightboxView.AddDocTab(doc, location); - case 'toggle': - return CollectionDockingView.ToggleSplit(doc, locationParams); - case 'inPlace': - case 'add': default: - return CollectionDockingView.AddSplit(doc, locationParams); + case 'inPlace': + case 'add': return CollectionDockingView.AddSplit(doc, locationParams); + case 'dashboard': return DashboardView.openDashboard(doc); + case 'close': return CollectionDockingView.CloseSplit(doc, locationParams); + case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc); + case 'lightbox': return LightboxView.AddDocTab(doc, location); + case 'toggle': return CollectionDockingView.ToggleSplit(doc, locationParams); } }; @@ -708,7 +724,7 @@ export class MainView extends React.Component { @computed get leftMenuPanel() { return ( - <div key="menu" className="mainView-leftMenuPanel"> + <div key="menu" className="mainView-leftMenuPanel" style={{ display: LightboxView.LightboxDoc ? 'none' : undefined }}> <DocumentView Document={Doc.MyLeftSidebarMenu} DataDoc={undefined} @@ -762,7 +778,7 @@ export class MainView extends React.Component { const width = this.propertiesWidth() + leftMenuFlyoutWidth; return ( <> - {this.leftMenuPanel} + {this._hideUI ? null : this.leftMenuPanel} <div key="inner" className={`mainView-innerContent${this.colorScheme}`}> {this.flyout} <div className="mainView-libraryHandle" style={{ left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} onPointerDown={this.onFlyoutPointerDown}> @@ -771,9 +787,11 @@ export class MainView extends React.Component { <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}> {this.dockingContent} - <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}> - <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" /> - </div> + {this._hideUI ? null : ( + <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" /> + </div> + )} <div className="properties-container" style={{ width: this.propertiesWidth() }}> {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={this.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />} </div> @@ -969,30 +987,28 @@ export class MainView extends React.Component { <DictationOverlay /> <SharingManager /> <SettingsManager /> + <ReportManager /> <CaptureManager /> <GroupManager /> <GoogleAuthenticationManager /> - <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfHeaderBarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} /> + <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfSidebarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} /> <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} /> - <TopBar /> + {this._hideUI ? null : <TopBar />} {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.LinkEditorDocView ? <LinkMenu clearLinkEditor={action(() => (DocumentLinksButton.LinkEditorDocView = undefined))} docView={DocumentLinksButton.LinkEditorDocView} /> : null} {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null} {((page: string) => { + // prettier-ignore switch (page) { - case 'dashboard': default: - return ( - <> - <div style={{ position: 'relative', display: LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 1999 }}> - <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} /> - </div> - {this.mainDashboardArea} - </> - ); - case 'home': - return <DashboardView />; + case 'dashboard': return (<> + <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}> + <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} /> + </div> + {this.mainDashboardArea} + </> ); + case 'home': return <DashboardView />; } })(Doc.ActivePage)} @@ -1009,7 +1025,7 @@ export class MainView extends React.Component { <InkTranscription /> {this.snapLines} <div className="mainView-webRef" ref={this.makeWebRef} /> - <LightboxView PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> + <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> </div> ); } diff --git a/src/client/views/MarqueeAnnotator.scss b/src/client/views/MarqueeAnnotator.scss index c90d48a65..5c65f35e9 100644 --- a/src/client/views/MarqueeAnnotator.scss +++ b/src/client/views/MarqueeAnnotator.scss @@ -1,12 +1,11 @@ - .marqueeAnnotator-annotationBox { position: absolute; background-color: rgba(245, 230, 95, 0.616); } - .marqueeAnnotator-dragBox { - position:absolute; + position: absolute; background-color: transparent; - opacity: 0.1; -}
\ No newline at end of file + opacity: 0.2; + cursor: default; +} diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index b01ee5f42..d9a989309 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -65,6 +65,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('rgba(173, 216, 230, 0.75)', true), true); AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight('rgba(173, 216, 230, 0.75)', true)); + AnchorMenu.Instance.OnAudio = unimplementedFunction; AnchorMenu.Instance.Highlight = this.highlight; AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations); AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor; @@ -270,8 +271,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { width: `${this._width}px`, height: `${this._height}px`, border: `${this._width === 0 ? '' : '2px dashed black'}`, - opacity: 0.2, - }}></div> + }}/> ); } } diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss index 60b7d14a0..7be765ea9 100644 --- a/src/client/views/PreviewCursor.scss +++ b/src/client/views/PreviewCursor.scss @@ -1,11 +1,10 @@ - .previewCursor-Dark, .previewCursor { color: black; position: absolute; transform-origin: left top; top: 0; - left:0; + left: 0; pointer-events: none; opacity: 1; z-index: 1001; @@ -13,4 +12,4 @@ .previewCursor-Dark { color: white; -}
\ No newline at end of file +} diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 68f5f072d..d56d2a310 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -1,12 +1,11 @@ import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import 'normalize.css'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; import { DocServer } from '../DocServer'; -import { Docs, DocUtils } from '../documents/Documents'; +import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { Transform } from '../util/Transform'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @@ -16,15 +15,25 @@ import './PreviewCursor.scss'; export class PreviewCursor extends React.Component<{}> { static _onKeyPress?: (e: KeyboardEvent) => void; static _getTransform: () => Transform; - static _addDocument: (doc: Doc | Doc[]) => void; + static _addDocument: (doc: Doc | Doc[]) => boolean; static _addLiveTextDoc: (doc: Doc) => void; static _nudge?: undefined | ((x: number, y: number) => boolean); + static _slowLoadDocuments?: ( + files: File[] | string, + options: DocumentOptions, + generatedDocuments: Doc[], + text: string, + completed: ((doc: Doc[]) => void) | undefined, + clientX: number, + clientY: number, + addDocument: (doc: Doc | Doc[]) => boolean + ) => Promise<void>; @observable static _clickPoint = [0, 0]; @observable public static Visible = false; constructor(props: any) { super(props); document.addEventListener('keydown', this.onKeyPress); - document.addEventListener('paste', this.paste); + document.addEventListener('paste', this.paste, true); } paste = async (e: ClipboardEvent) => { @@ -38,20 +47,16 @@ export class PreviewCursor extends React.Component<{}> { if (plain) { // tests for youtube and makes video document if (plain.indexOf('www.youtube.com/watch') !== -1) { - const url = plain.replace('youtube.com/watch?v=', 'youtube.com/embed/'); - undoBatch(() => - PreviewCursor._addDocument( - Docs.Create.VideoDocument(url, { - title: url, - _width: 400, - _height: 315, - _nativeWidth: 600, - _nativeHeight: 472.5, - x: newPoint[0], - y: newPoint[1], - }) - ) - )(); + const batch = UndoManager.StartBatch('youtube upload'); + const generatedDocuments: Doc[] = []; + const options = { + title: plain, + _width: 400, + _height: 315, + x: newPoint[0], + y: newPoint[1], + }; + PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, newPoint[0], newPoint[1], PreviewCursor._addDocument).then(batch.end); } else if (re.test(plain)) { const url = plain; undoBatch(() => @@ -184,7 +189,17 @@ export class PreviewCursor extends React.Component<{}> { addLiveText: (doc: Doc) => void, getTransform: () => Transform, addDocument: undefined | ((doc: Doc | Doc[]) => boolean), - nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean) + nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean), + slowLoadDocuments: ( + files: File[] | string, + options: DocumentOptions, + generatedDocuments: Doc[], + text: string, + completed: ((doc: Doc[]) => void) | undefined, + clientX: number, + clientY: number, + addDocument: (doc: Doc | Doc[]) => boolean + ) => Promise<void> ) { this._clickPoint = [x, y]; this._onKeyPress = onKeyPress; @@ -192,6 +207,7 @@ export class PreviewCursor extends React.Component<{}> { this._getTransform = getTransform; this._addDocument = addDocument || returnFalse; this._nudge = nudge; + this._slowLoadDocuments = slowLoadDocuments; this.Visible = true; } render() { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 8c4c1d00b..80c2c7705 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -68,14 +68,6 @@ export class PropertiesButtons extends React.Component<{}, {}> { on => 'thumbtack' ); } - @computed get dictationButton() { - return this.propertyToggleBtn( - 'Dictate', - '_showAudio', - on => `${on ? 'Hide' : 'Show'} dictation/recording controls`, - on => 'microphone' - ); - } @computed get maskButton() { return this.propertyToggleBtn( 'Mask', @@ -348,7 +340,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { const isInk = layoutField instanceof InkField; const isMap = this.selectedDoc?.type === DocumentType.MAP; const isCollection = this.selectedDoc?.type === DocumentType.COL; - const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking; + //TODO: will likely need to create separate note-taking view type here + const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking || this.selectedDoc?._viewType === CollectionViewType.NoteTaking; const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform; const isTree = this.selectedDoc?._viewType === CollectionViewType.Tree; const isTabView = this.selectedTabView; @@ -364,7 +357,6 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.titleButton)} {toggle(this.captionButton)} {toggle(this.lockButton)} - {toggle(this.dictationButton, { display: isNovice ? 'none' : '' })} {toggle(this.onClickButton)} {toggle(this.fitWidthButton)} {toggle(this.freezeThumb)} diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index 0f63ebc1d..9d89ee036 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -40,7 +40,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC .keys() ); return doclayouts - .filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)) + .filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance?.props.Document)) .filter(doc => !Doc.IsSystem(doc)) .map(doc => ({ col: doc, target })); } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index aecbc4255..2708c561d 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -15,7 +15,7 @@ import { ComputedField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from '../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; -import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; +import { DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; @@ -58,7 +58,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; - if (PresBox.Instance?._selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); + if (PresBox.Instance?.selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); return undefined; } @computed get isPres(): boolean { @@ -1612,11 +1612,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ); } if (this.isPres) { - const selectedItem: boolean = PresBox.Instance?._selectedArray.size > 0; + const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0; const type = PresBox.Instance.activeItem?.type; - const viewType = PresBox.Instance.activeItem?._viewType; - const pannable: boolean = (type === DocumentType.COL && viewType === CollectionViewType.Freeform) || type === DocumentType.IMG; - const scrollable: boolean = type === DocumentType.PDF || type === DocumentType.WEB || type === DocumentType.RTF || viewType === CollectionViewType.Stacking; return ( <div className="propertiesView" style={{ width: this.props.width }}> <div className="propertiesView-title" style={{ width: this.props.width }}> @@ -1625,7 +1622,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-name" style={{ borderBottom: 0 }}> {this.editableTitle} <div className="propertiesView-presSelected"> - <div className="propertiesView-selectedCount">{PresBox.Instance?._selectedArray.size} selected</div> + <div className="propertiesView-selectedCount">{PresBox.Instance?.selectedArray.size} selected</div> <div className="propertiesView-selectedList">{PresBox.Instance?.listOfSelected}</div> </div> </div> diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index b7ea124b9..416162aeb 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -1,17 +1,16 @@ -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, Opt } from "../../fields/Doc"; -import { ScriptField } from "../../fields/ScriptField"; -import { ScriptCast } from "../../fields/Types"; -import { emptyFunction } from "../../Utils"; -import { DragManager } from "../util/DragManager"; -import { CompileScript } from "../util/Scripting"; -import { EditableView } from "./EditableView"; -import { DocumentIconContainer } from "./nodes/DocumentIcon"; -import { OverlayView } from "./OverlayView"; -import "./ScriptBox.scss"; - +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, Opt } from '../../fields/Doc'; +import { ScriptField } from '../../fields/ScriptField'; +import { ScriptCast } from '../../fields/Types'; +import { emptyFunction } from '../../Utils'; +import { DragManager } from '../util/DragManager'; +import { CompileScript } from '../util/Scripting'; +import { EditableView } from './EditableView'; +import { DocumentIconContainer } from './nodes/DocumentIcon'; +import { OverlayView } from './OverlayView'; +import './ScriptBox.scss'; export interface ScriptBoxProps { onSave: (text: string, onError: (error: string) => void) => void; @@ -28,53 +27,58 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { constructor(props: ScriptBoxProps) { super(props); - this._scriptText = props.initialText || ""; + this._scriptText = props.initialText || ''; } @action onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { this._scriptText = e.target.value; - } + }; @action onError = (error: string) => { - console.log("ScriptBox: " + error); - } + console.log('ScriptBox: ' + error); + }; overlayDisposer?: () => void; onFocus = () => { this.overlayDisposer?.(); this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); - } + }; onBlur = () => { this.overlayDisposer?.(); - } + }; render() { - let onFocus: Opt<() => void> = undefined, onBlur: Opt<() => void> = undefined; + let onFocus: Opt<() => void> = undefined, + onBlur: Opt<() => void> = undefined; if (this.props.showDocumentIcons) { onFocus = this.onFocus; onBlur = this.onBlur; } - const params = <EditableView - contents={""} - display={"block"} - maxHeight={72} - height={35} - fontSize={28} - GetValue={() => ""} - SetValue={(value: string) => this.props.setParams && this.props.setParams(value.split(" ").filter(s => s !== " ")) ? true : true} - />; + const params = <EditableView contents={''} display={'block'} maxHeight={72} height={35} fontSize={28} GetValue={() => ''} SetValue={(value: string) => (this.props.setParams?.(value.split(' ').filter(s => s !== ' ')) ? true : true)} />; return ( <div className="scriptBox-outerDiv"> - <div style={{ display: "flex", flexDirection: "column", height: "100%" }}> + <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea> - <div style={{ background: "beige" }} >{params}</div> + <div style={{ background: 'beige' }}>{params}</div> </div> <div className="scriptBox-toolbar"> - <button onClick={e => { this.props.onSave(this._scriptText, this.onError); e.stopPropagation(); }}>Save</button> - <button onClick={e => { this.props.onCancel && this.props.onCancel(); e.stopPropagation(); }}>Cancel</button> + <button + onClick={e => { + this.props.onSave(this._scriptText, this.onError); + e.stopPropagation(); + }}> + Save + </button> + <button + onClick={e => { + this.props.onCancel && this.props.onCancel(); + e.stopPropagation(); + }}> + Cancel + </button> </div> </div> ); @@ -90,35 +94,43 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { // tslint:disable-next-line: no-unnecessary-callback-wrapper const params: string[] = []; const setParams = (p: string[]) => params.splice(0, params.length, ...p); - const scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => { - if (!text) { - Doc.GetProto(doc)[fieldKey] = undefined; - } else { - const script = CompileScript(text, { - params: { this: Doc.name, ...contextParams }, - typecheck: false, - editable: true, - transformer: DocumentIconContainer.getTransformer() - }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - return; - } + const scriptingBox = ( + <ScriptBox + initialText={originalText} + setParams={setParams} + onCancel={overlayDisposer} + onSave={(text, onError) => { + if (!text) { + Doc.GetProto(doc)[fieldKey] = undefined; + } else { + const script = CompileScript(text, { + params: { this: Doc.name, ...contextParams }, + typecheck: false, + editable: true, + transformer: DocumentIconContainer.getTransformer(), + }); + if (!script.compiled) { + onError(script.errors.map(error => error.messageText).join('\n')); + return; + } - const div = document.createElement("div"); - div.style.width = "90"; - div.style.height = "20"; - div.style.background = "gray"; - div.style.position = "absolute"; - div.style.display = "inline-block"; - div.style.transform = `translate(${clientX}px, ${clientY}px)`; - div.innerHTML = "button"; - params.length && DragManager.StartButtonDrag([div], text, doc.title + "-instance", {}, params, (button: Doc) => { }, clientX, clientY); + const div = document.createElement('div'); + div.style.width = '90'; + div.style.height = '20'; + div.style.background = 'gray'; + div.style.position = 'absolute'; + div.style.display = 'inline-block'; + div.style.transform = `translate(${clientX}px, ${clientY}px)`; + div.innerHTML = 'button'; + params.length && DragManager.StartButtonDrag([div], text, doc.title + '-instance', {}, params, (button: Doc) => {}, clientX, clientY); - Doc.GetProto(doc)[fieldKey] = new ScriptField(script); - overlayDisposer(); - } - }} showDocumentIcons />; + Doc.GetProto(doc)[fieldKey] = new ScriptField(script); + overlayDisposer(); + } + }} + showDocumentIcons + /> + ); overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title }); } } diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index e81a9c40f..90d9c3c43 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -74,6 +74,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on'); this.addDocument(target); this._stackRef.current?.focusDocument(target); + return target; }; makeDocUnfiltered = (doc: Doc) => { if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) { @@ -93,7 +94,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { this.props .ScreenToLocalTransform() .translate(Doc.NativeWidth(this.props.dataDoc), 0) - .scale(this.props.scaling?.() || 1); + .scale(this.props.NativeDimScaling?.() || 1); // panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 : // this.props.usePanelWidth ? this.props.PanelWidth() : // (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth); @@ -101,7 +102,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF || this.props.layoutDoc.type === DocumentType.MAP - ? this.props.PanelWidth() + ? this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) : ((NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth()) / NumCast(this.props.nativeWidth); panelHeight = () => this.props.PanelHeight() - this.filtersHeight(); addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey); @@ -144,14 +145,14 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { top: this.props.rootDoc.type !== DocumentType.RTF && StrCast(this.props.rootDoc._showTitle) === 'title' ? 15 : 0, right: 0, background: this.props.styleProvider?.(this.props.rootDoc, this.props, StyleProp.WidgetColor), - width: `${this.panelWidth()}px`, + width: `100%`, height: '100%', }}> - <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight(), width: this.panelWidth() }} onWheel={e => e.stopPropagation()}> + <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> {this.allUsers.map(renderUsers)} {this.allMetadata.map(renderMeta)} </div> - <div style={{ width: '100%', height: this.panelHeight(), position: 'relative' }}> + <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}> <CollectionStackingView {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} ref={this._stackRef} @@ -164,7 +165,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { setHeight={this.setHeightCallback} isAnnotationOverlay={false} select={emptyFunction} - scaling={returnOne} + NativeDimScaling={returnOne} childShowTitle={this.showTitle} childDocumentsActive={this.props.isContentActive} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 340a5df45..c0ba170c6 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -40,7 +40,8 @@ export enum StyleProp { JitterRotation = 'jitterRotation', // whether documents should be randomly rotated BorderPath = 'customBorder', // border path for document view FontSize = 'fontSize', // size of text font - FontFamily = 'fontFamily', // size of text font + FontFamily = 'fontFamily', // font family of text + FontWeight = 'fontWeight', // font weight of text } function darkScheme() { @@ -110,18 +111,23 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return undefined; case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey'; + // return doc = dragManager.dragDocument ? props.dragEffects.opacity??CastofOpacityonline94 : Cast()) + // same idea for background Color case StyleProp.Opacity: return Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null)); case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); case StyleProp.FontSize: - return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize))); + return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize))); case StyleProp.FontFamily: - return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily))); + return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._fontFamily, StrCast(Doc.UserDoc().fontFamily))); + case StyleProp.FontWeight: + return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._fontWeight, StrCast(Doc.UserDoc().fontWeight))); case StyleProp.ShowTitle: return ( (doc && !doc.presentationTargetDoc && + props?.showTitle?.() !== '' && StrCast( doc._showTitle, props?.showTitle?.() || @@ -154,7 +160,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.JitterRotation: return comicStyle() ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0; case StyleProp.HeaderMargin: - return ([CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) || doc?.type === DocumentType.LABEL) && + return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || + (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) || + doc?.type === DocumentType.LABEL) && showTitle() && !StrCast(doc?.showTitle).includes(':hover') ? 15 @@ -250,9 +258,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return doc.z ? `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow : props?.ContainingCollectionDoc?._useClusters && doc.type !== DocumentType.INK - ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.ContentScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : NumCast(doc.group, -1) !== -1 && doc.type !== DocumentType.INK - ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.ContentScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : isBackground() ? undefined // if it's a background & has a cluster color, make the shadow spread really big : StrCast(doc.boxShadow, ''); diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 156513f47..863829a51 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -55,16 +55,12 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @observable private _hidden: boolean = true; toggleLayout = (e: React.ChangeEvent<HTMLInputElement>, layout: string): void => { - this.props.docViews.map(dv => dv.switchViews(e.target.checked, layout)); + this.props.docViews.map(dv => dv.switchViews(e.target.checked, layout, undefined, true)); }; toggleDefault = (e: React.ChangeEvent<HTMLInputElement>): void => { this.props.docViews.map(dv => dv.switchViews(false, 'layout')); }; - toggleAudio = (e: React.ChangeEvent<HTMLInputElement>): void => { - this.props.docViews.map(dv => (dv.props.Document._showAudio = e.target.checked)); - }; - @undoBatch @action toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: string): void => { @@ -76,12 +72,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { this._hidden = !this._hidden; }; - @undoBatch - @action - toggleChrome = (): void => { - this.props.docViews.map(dv => Doc.Layout(dv.layoutDoc)).forEach(layout => (layout._chromeHidden = !layout._chromeHidden)); - }; - // todo: add brushes to brushMap to save with a style name onCustomKeypress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { @@ -95,13 +85,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', '')))); } - return100 = () => 100; + return100 = () => 300; @computed get scriptField() { - const script = ScriptField.MakeScript( - 'docs.map(d => switchView(d, this))', - { this: Doc.name, heading: 'string', checked: 'string', containingTreeView: Doc.name, firstDoc: Doc.name }, - { docs: new List<Doc>(this.props.docViews.map(dv => dv.props.Document)) } - ); + const script = ScriptField.MakeScript('docs.map(d => switchView(d, this))', { this: Doc.name }, { docs: this.props.docViews.map(dv => dv.props.Document) as any }); return script ? () => script : undefined; } templateIsUsed = (selDoc: Doc, templateDoc: Doc) => { @@ -113,13 +99,10 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const firstDoc = this.props.docViews[0].props.Document; const templateName = StrCast(firstDoc.layoutKey, 'layout').replace('layout_', ''); const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data); - const addedTypes = Doc.noviceMode ? [] : DocListCast(Cast(Doc.UserDoc()['template-buttons'], Doc, null)?.data); - const layout = Doc.Layout(firstDoc); + const addedTypes = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data); const templateMenu: Array<JSX.Element> = []; this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />)); - templateMenu.push(<OtherToggle key={'audio'} name={'Audio'} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />); templateMenu.push(<OtherToggle key={'default'} name={'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />); - !Doc.noviceMode && templateMenu.push(<OtherToggle key={'chrome'} name={'Chrome'} checked={!layout._chromeHidden} toggle={this.toggleChrome} />); addedTypes.concat(noteTypes).map(template => (template.treeViewChecked = this.templateIsUsed(firstDoc, template))); this._addedKeys && Array.from(this._addedKeys) @@ -129,43 +112,42 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { <ul className="template-list" style={{ display: 'block' }}> {Doc.noviceMode ? null : <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} />} {templateMenu} - {Doc.noviceMode ? null : ( - <CollectionTreeView - Document={Doc.MyTemplates} - CollectionView={undefined} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - styleProvider={DefaultStyleProvider} - setHeight={returnFalse} - docViewPath={returnEmptyDoclist} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - rootSelected={returnFalse} - onCheckedClick={this.scriptField} - onChildClick={this.scriptField} - dropAction={undefined} - isAnyChildContentActive={returnFalse} - isContentActive={returnTrue} - bringToFront={emptyFunction} - focus={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} - ScreenToLocalTransform={Transform.Identity} - isSelected={returnFalse} - pinToPres={emptyFunction} - select={emptyFunction} - renderDepth={1} - addDocTab={returnFalse} - PanelWidth={this.return100} - PanelHeight={this.return100} - treeViewHideHeaderFields={true} - dontRegisterView={true} - fieldKey={'data'} - moveDocument={returnFalse} - removeDocument={returnFalse} - addDocument={returnFalse} - /> - )} + <CollectionTreeView + Document={Doc.MyTemplates} + CollectionView={undefined} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + styleProvider={DefaultStyleProvider} + setHeight={returnFalse} + docViewPath={returnEmptyDoclist} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + rootSelected={returnFalse} + onCheckedClick={this.scriptField} + onChildClick={this.scriptField} + dropAction={undefined} + isAnyChildContentActive={returnFalse} + isContentActive={returnTrue} + bringToFront={emptyFunction} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + ScreenToLocalTransform={Transform.Identity} + isSelected={returnFalse} + pinToPres={emptyFunction} + select={emptyFunction} + renderDepth={1} + addDocTab={returnFalse} + PanelWidth={this.return100} + PanelHeight={this.return100} + treeViewHideHeaderFields={true} + treeViewHideTitle={true} + dontRegisterView={true} + fieldKey={'data'} + moveDocument={returnFalse} + removeDocument={returnFalse} + addDocument={returnFalse} + /> </ul> ); } diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss index 17eff022f..b1cce8705 100644 --- a/src/client/views/_nodeModuleOverrides.scss +++ b/src/client/views/_nodeModuleOverrides.scss @@ -44,7 +44,8 @@ div .lm_header { position: absolute; width: calc(100% - 60px); overflow: scroll; - background: $dark-gray; + background: #6b6b6b6b; //$dark-gray; + border-radius: 5px; } .lm_tab { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 42f9bb981..6d70cc0d2 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -29,7 +29,7 @@ const _global = (window /* browser */ || global) /* node */ as any; @observer export class CollectionDockingView extends CollectionSubView() { - @observable public static Instance: CollectionDockingView; + @observable public static Instance: CollectionDockingView | undefined; public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) { return { type: 'react-component', @@ -103,12 +103,14 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch public static CloseSplit(document: Opt<Doc>, panelName?: string): boolean { - const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tab => (panelName ? tab.contentItem.config.props.panelName === panelName : tab.DashDoc === document)); - if (tab) { - const j = tab.header.parent.contentItems.indexOf(tab.contentItem); - if (j !== -1) { - tab.header.parent.contentItems[j].remove(); - return CollectionDockingView.Instance.layoutChanged(); + if (CollectionDockingView.Instance) { + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tab => (panelName ? tab.contentItem.config.props.panelName === panelName : tab.DashDoc === document)); + if (tab) { + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + if (j !== -1) { + tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); + } } } @@ -119,19 +121,21 @@ export class CollectionDockingView extends CollectionSubView() { public static OpenFullScreen(doc: Doc) { SelectionManager.DeselectAll(); const instance = CollectionDockingView.Instance; - if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === 'layout') { - return DashboardView.openDashboard(doc); + if (instance) { + if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === 'layout') { + return DashboardView.openDashboard(doc); + } + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))], + }; + const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + instance._goldenLayout.root.contentItems[0].addChild(docconfig); + docconfig.callDownwards('_$init'); + instance._goldenLayout._$maximiseItem(docconfig); + instance._goldenLayout.emit('stateChanged'); + instance.stateChanged(); } - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))], - }; - const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - instance._goldenLayout.root.contentItems[0].addChild(docconfig); - docconfig.callDownwards('_$init'); - instance._goldenLayout._$maximiseItem(docconfig); - instance._goldenLayout.emit('stateChanged'); - instance.stateChanged(); return true; } @@ -146,21 +150,23 @@ export class CollectionDockingView extends CollectionSubView() { const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); stack.addChild(newContentItem.contentItems[0], undefined); stack.contentItems[activeContentItemIndex].remove(); - return CollectionDockingView.Instance.layoutChanged(); + return instance.layoutChanged(); } - const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tab => tab.contentItem.config.props.panelName === panelName); + const tab = Array.from(instance.tabMap.keys()).find(tab => tab.contentItem.config.props.panelName === panelName); if (tab) { tab.header.parent.addChild(newConfig, undefined); const j = tab.header.parent.contentItems.indexOf(tab.contentItem); !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove(); - return CollectionDockingView.Instance.layoutChanged(); + return instance.layoutChanged(); } return CollectionDockingView.AddSplit(document, panelName, stack, panelName); } @undoBatch public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) { - return Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName); + return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 + ? CollectionDockingView.CloseSplit(doc) + : CollectionDockingView.AddSplit(doc, location, stack, panelName); } // @@ -169,8 +175,8 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch @action public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { - if (document._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); - + if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); + if (!CollectionDockingView.Instance) return false; const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document); if (tab) { tab.header.parent.setActiveContentItem(tab.contentItem); @@ -453,13 +459,15 @@ export class CollectionDockingView extends CollectionSubView() { Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc); Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); } - const dview = CollectionDockingView.Instance.props.Document; - const fieldKey = CollectionDockingView.Instance.props.fieldKey; - Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); - this.tabMap.delete(tab); - tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); - tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); - this.stateChanged(); + if (CollectionDockingView.Instance) { + const dview = CollectionDockingView.Instance.props.Document; + const fieldKey = CollectionDockingView.Instance.props.fieldKey; + Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); + this.tabMap.delete(tab); + tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); + setTimeout(this.stateChanged); + } }; tabCreated = (tab: any) => { this.tabMap.add(tab); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 2c0e44bc3..46e8494ab 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -34,7 +34,6 @@ import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocum import { DocumentView } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; -import { PresBox } from '../nodes/trails/PresBox'; import { DefaultStyleProvider } from '../StyleProvider'; import { CollectionDockingView } from './CollectionDockingView'; import { CollectionLinearView } from './collectionLinear'; @@ -317,6 +316,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @computed get _stacking_commands() { return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } + @computed get _notetaking_commands() { + return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand]; + } @computed get _masonry_commands() { return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @@ -341,6 +343,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu return this._schema_commands; case CollectionViewType.Stacking: return this._stacking_commands; + case CollectionViewType.NoteTaking: + return this._notetaking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Time: @@ -386,6 +390,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu return <CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />; case CollectionViewType.Stacking: return <CollectionStackingViewChrome key="collchrome" {...this.props} />; + case CollectionViewType.NoteTaking: + return <CollectionNoteTakingViewChrome key="collchrome" {...this.props} />; case CollectionViewType.Schema: return <CollectionSchemaViewChrome key="collchrome" {...this.props} />; case CollectionViewType.Tree: @@ -580,49 +586,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu } @undoBatch - @action - pinWithView = (targetDoc: Opt<Doc>) => { - if (targetDoc) { - TabDocView.PinDoc(targetDoc); - const presArray: Doc[] = PresBox.Instance?.sortArray(); - const size: number = PresBox.Instance?._selectedArray.size; - const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; - const activeDoc = presSelected ? PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.indexOf(presSelected) + 1] : PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.length - 1]; - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) { - const scroll = targetDoc._scrollTop; - activeDoc.presPinView = true; - activeDoc.presPinViewScroll = scroll; - } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.MAP) { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeDoc.presPinView = true; - activeDoc.presPinViewX = x; - activeDoc.presPinViewY = y; - activeDoc.presPinViewScale = scale; - } else if (targetDoc.type === DocumentType.VID) { - activeDoc.presPinView = true; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const width = targetDoc._clipWidth; - activeDoc.presPinClipWidth = width; - activeDoc.presPinView = true; - } - } - }; - - @computed - get pinWithViewButton() { - const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: 'auto', width: 19 }} />; - return !this.selectedDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">{'Pin with current view'}</div>} placement="top"> - <button className="antimodeMenu-button" style={{ justifyContent: 'center' }} onClick={() => this.pinWithView(this.selectedDoc)}> - {presPinWithViewIcon} - </button> - </Tooltip> - ); - } - - @undoBatch onAlias = () => { if (this.selectedDoc && this.selectedDocumentView) { // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); @@ -715,7 +678,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu {this.aliasButton} {/* {this.pinButton} */} {this.toggleOverlayButton} - {this.pinWithViewButton} <div className="collectionMenu-divider" key="divider2"></div> {this.subChrome} <div className="collectionMenu-divider" key="divider3"></div> @@ -762,6 +724,19 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; } + public static gotoKeyFrame(doc: Doc, newFrame: number) { + if (doc) { + const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); + const currentFrame = Cast(doc._currentFrame, 'number', null); + if (currentFrame === undefined) { + doc._currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); + } + } + @undoBatch @action nextKeyframe = (): void => { @@ -1003,7 +978,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView <FontAwesomeIcon icon={'caret-left'} size={'lg'} /> </div> </Tooltip> - <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> + <Tooltip key="num" title={<div className="dash-tooltip">Frame number</div>} placement="bottom"> <div className="numKeyframe" style={{ color: this.props.docView.ComponentView?.getKeyFrameEditing?.() ? 'white' : 'black', backgroundColor: this.props.docView.ComponentView?.getKeyFrameEditing?.() ? '#5B9FDD' : '#AEDDF8' }} @@ -1146,6 +1121,126 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView } @observer +export class CollectionNoteTakingViewChrome extends React.Component<CollectionViewMenuProps> { + @observable private _currentKey: string = ''; + @observable private suggestions: string[] = []; + + get document() { + return this.props.docView.props.Document; + } + + @computed private get descending() { + return StrCast(this.document._columnsSort) === 'descending'; + } + @computed get pivotField() { + return StrCast(this.document._pivotField); + } + + getKeySuggestions = async (value: string): Promise<string[]> => { + const val = value.toLowerCase(); + const docs = DocListCast(this.document[this.props.fieldKey]); + + if (Doc.UserDoc().noviceMode) { + if (docs instanceof Doc) { + const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); + return keys.filter(key => key.toLowerCase().indexOf(val) > -1); + } else { + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + const noviceKeys = Array.from(keys).filter( + key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') + ); + return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); + } + } + + if (docs instanceof Doc) { + return Object.keys(docs).filter(key => key.toLowerCase().indexOf(val) > -1); + } else { + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + return Array.from(keys).filter(key => key.toLowerCase().indexOf(val) > -1); + } + }; + + @action + onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { + this._currentKey = newValue; + }; + + getSuggestionValue = (suggestion: string) => suggestion; + + renderSuggestion = (suggestion: string) => { + return <p>{suggestion}</p>; + }; + + onSuggestionFetch = async ({ value }: { value: string }) => { + const sugg = await this.getKeySuggestions(value); + runInAction(() => { + this.suggestions = sugg; + }); + }; + + @action + onSuggestionClear = () => { + this.suggestions = []; + }; + + @action + setValue = (value: string) => { + this.document._pivotField = value; + return true; + }; + + @action toggleSort = () => { + this.document._columnsSort = this.document._columnsSort === 'descending' ? 'ascending' : this.document._columnsSort === 'ascending' ? undefined : 'descending'; + }; + @action resetValue = () => { + this._currentKey = this.pivotField; + }; + + render() { + const doctype = this.props.docView.Document.type; + const isPres: boolean = doctype === DocumentType.PRES; + return isPres ? null : ( + <div className="collectionStackingViewChrome-cont"> + <div className="collectionStackingViewChrome-pivotField-cont"> + <div className="collectionStackingViewChrome-pivotField-label">GROUP BY:</div> + <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? '180' : '0'}deg)` }}> + <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> + </div> + <div className="collectionStackingViewChrome-pivotField"> + <EditableView + GetValue={() => this.pivotField} + autosuggestProps={{ + resetValue: this.resetValue, + value: this._currentKey, + onChange: this.onKeyChange, + autosuggestProps: { + inputProps: { + value: this._currentKey, + onChange: this.onKeyChange, + }, + getSuggestionValue: this.getSuggestionValue, + suggestions: this.suggestions, + alwaysRenderSuggestions: true, + renderSuggestion: this.renderSuggestion, + onSuggestionsFetchRequested: this.onSuggestionFetch, + onSuggestionsClearRequested: this.onSuggestionClear, + }, + }} + oneLine + SetValue={this.setValue} + contents={this.pivotField ? this.pivotField : 'N/A'} + /> + </div> + </div> + </div> + ); + } +} + +@observer export class CollectionSchemaViewChrome extends React.Component<CollectionViewMenuProps> { // private _textwrapAllRows: boolean = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0; get document() { @@ -1442,13 +1537,5 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu } } ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) { - const dataField = doc[Doc.LayoutFieldKey(doc)]; - const childDocs = DocListCast(dataField); - const currentFrame = Cast(doc._currentFrame, 'number', null); - if (currentFrame === undefined) { - doc._currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); - } - CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); - doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); + CollectionFreeFormViewChrome.gotoKeyFrame(doc, newFrame); }); diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss new file mode 100644 index 000000000..08b13fd50 --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingView.scss @@ -0,0 +1,504 @@ +@import '../global/globalCssVariables'; + +.collectionNoteTakingView-DocumentButtons { + display: none; + justify-content: space-between; + margin: auto; +} + +.collectionNoteTakingView-addDocumentButton { + display: flex; + overflow: hidden; + margin: auto; + width: 100%; + overflow: ellipses; + + .editableView-container-editing-oneLine, + .editableView-container-editing { + color: grey; + padding: 10px; + width: 100%; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } + + font-size: 75%; + letter-spacing: 2px; + cursor: pointer; + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } +} + +.collectionNoteTakingView { + display: flex; +} + +.collectionNoteTakingViewFieldColumn { + height: 100%; + display: flex; + overflow: hidden; +} +.collectionNoteTakingViewFieldColumn:hover { + .collectionNoteTakingView-DocumentButtons { + display: flex; + } +} + +// TODO:glr Turn this into a seperate class +.documentButtonMenu { + position: relative; + height: fit-content; + border-bottom: $standard-border; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + align-content: center; + padding: 5px 0 5px 0; + + .documentExplanation { + width: 90%; + margin: 5px; + font-size: 11px; + background-color: $light-blue; + color: $medium-blue; + padding: 10px; + border-radius: 10px; + border: solid 2px $medium-blue; + } +} + +.collectionNoteTakingView { + height: 100%; + width: 100%; + position: absolute; + top: 0; + overflow-y: auto; + overflow-x: hidden; + transition: top 0.5s; + + > div { + position: relative; + display: block; + } + + .collectionSchemaView-previewDoc { + height: 100%; + position: absolute; + } + + .collectionNoteTakingView-docView-container { + width: 45%; + margin: 5% 2.5%; + padding-left: 2.5%; + height: auto; + } + + .collectionNoteTakingView-columnStack { + height: 100%; + width: 100%; + display: inline-block; + } + .collectionNoteTakingView-Nodes { + width: 100%; + height: 100%; + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + top: 0; + left: 0; + width: 100%; + position: absolute; + margin: auto; + width: max-content; + height: max-content; + position: relative; + grid-auto-rows: 0px; + } + + .collectionNoteTakingView-description { + font-size: 100%; + margin-bottom: 1vw; + padding: 10px; + height: 2vw; + width: 100%; + font-family: $sans-serif; + background: $dark-gray; + color: $white; + } + + .collectionNoteTakingView-columnDragger { + width: 15; + height: 15; + position: absolute; + margin-left: -5; + } + + .collectionNoteTakingView-sectionDelete { + display: none; + position: absolute; + right: 0; + width: max-content; + height: max-content; + top: 10; + padding: 2; + } + + // Documents in NoteTaking view + .collectionNoteTakingView-columnDoc { + display: flex; + // margin: auto; // Removed auto so that it is no longer center aligned - this could be something we change + position: relative; + + &:hover { + .hoverButtons { + opacity: 1; + } + } + + .hoverButtons { + display: flex; + opacity: 0; + position: absolute; + height: 100%; + left: -35px; + justify-content: center; + align-items: center; + padding: 0px 10px; + + .buttonWrapper { + padding: 3px; + border-radius: 3px; + + &:hover { + background: rgba(0, 0, 0, 0.26); + } + } + } + } + + .collectionNoteTakingView-collapseBar { + margin-left: 2px; + margin-right: 2px; + margin-top: 2px; + background: $medium-gray; + height: 5px; + + &.active { + margin-left: 0; + margin-right: 0; + background: red; + } + } + + .collectionNoteTakingView-miniHeader { + width: 100%; + + .editableView-container-editing-oneLine { + min-height: 20px; + display: flex; + align-items: center; + flex-direction: row; + } + + span::before, + span::after { + content: ''; + width: 50%; + border-top: dashed gray 1px; + position: relative; + display: inline-block; + } + + span::before { + margin-right: 10px; + } + + span::after { + margin-left: 10px; + } + + span { + position: relative; + text-align: center; + white-space: nowrap; + overflow: visible; + width: 100%; + display: flex; + color: gray; + align-items: center; + } + } + + .collectionNoteTakingView-sectionHeader { + text-align: center; + margin: auto; + margin-bottom: 10px; + background: $medium-gray; + // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong + + .editableView-input { + color: $dark-gray; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .collectionNoteTakingView-sectionHeader-subCont { + outline: none; + border: 0px; + width: 100%; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + position: relative; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + color: $dark-gray; + + .editableView-container-editing-oneLine, + .editableView-container-editing { + color: grey; + padding: 10px; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .editableView-input { + padding: 12px 10px 11px 10px; + border: 0px; + color: grey; + text-align: center; + letter-spacing: 2px; + outline-color: black; + } + } + + .collectionNoteTakingView-sectionColor { + position: absolute; + left: 0; + top: 0; + height: 100%; + display: none; + + [class*='css'] { + max-width: 102px; + } + + .collectionNoteTakingView-sectionColorButton { + height: 30px; + display: inherit; + } + + .collectionNoteTakingView-colorPicker { + width: 78px; + z-index: 10; + position: relative; + background: white; + + .colorOptions { + display: flex; + flex-wrap: wrap; + } + + .colorPicker { + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 10px; + margin: 3px; + + &.active { + border: 2px solid white; + box-shadow: 0 0 0 2px lightgray; + } + } + } + } + + .collectionNoteTakingView-sectionOptions { + position: absolute; + right: 0; + top: 0; + height: 100%; + display: none; + + [class*='css'] { + max-width: 102px; + } + + .collectionNoteTakingView-sectionOptionButton { + height: 30px; + } + + .collectionNoteTakingView-optionPicker { + .optionOptions { + display: inline; + } + + .optionPicker { + cursor: pointer; + height: 20px; + border-radius: 10px; + margin: 3px; + width: max-content; + + &.active { + color: red; + } + } + } + } + } + + .collectionNoteTakingView-sectionHeader:hover { + .collectionNoteTakingView-sectionColor { + display: unset; + } + + .collectionNoteTakingView-sectionOptions { + display: unset; + } + + .collectionNoteTakingView-sectionDelete { + display: unset; + } + } + + .collectionNoteTakingView-addDocumentButton, + .collectionNoteTakingView-addGroupButton { + display: flex; + overflow: hidden; + margin: auto; + width: 90%; + overflow: ellipses; + + .editableView-container-editing-oneLine, + .editableView-container-editing { + color: grey; + padding: 10px; + width: 100%; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } + } + + .collectionNoteTakingView-addDocumentButton { + font-size: 75%; + letter-spacing: 2px; + cursor: pointer; + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } + } + + .collectionNoteTakingView-addGroupButton { + background: rgb(238, 238, 238); + font-size: 75%; + text-align: center; + letter-spacing: 2px; + height: fit-content; + } + + .rc-switch { + position: absolute; + display: inline-block; + bottom: 4px; + right: 4px; + width: 70px; + height: 30px; + border-radius: 40px 40px; + background-color: lightslategrey; + } + + .rc-switch:after { + position: absolute; + width: 22px; + height: 22px; + left: 3px; + top: 4px; + border-radius: 50% 50%; + background-color: #fff; + content: ' '; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); + -webkit-transform: scale(1); + transform: scale(1); + transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1); + -webkit-animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); + animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); + -webkit-animation-duration: 0.3s; + animation-duration: 0.3s; + } + + .rc-switch-checked:after { + left: 44px; + } + + .rc-switch-inner { + color: #fff; + font-size: 12px; + position: absolute; + left: 28px; + top: 8px; + } + + .rc-switch-checked .rc-switch-inner { + left: 8px; + } +} + +@media only screen and (max-device-width: 480px) { + .collectionNoteTakingView .collectionNoteTakingView-columnDragger, + .collectionNoteTakingView-columnDragger { + width: 0.1; + height: 0.1; + opacity: 0; + font-size: 0; + } +} diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx new file mode 100644 index 000000000..92c0bc341 --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -0,0 +1,707 @@ +import React = require('react'); +import { CursorProperty } from 'csstype'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Copy, Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager, dropActionType } from '../../util/DragManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { LightboxView } from '../LightboxView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView'; +import { FieldViewProps } from '../nodes/FieldView'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { StyleProp } from '../StyleProvider'; +import './CollectionNoteTakingView.scss'; +import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn'; +import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider'; +import { CollectionSubView } from './CollectionSubView'; +const _global = (window /* browser */ || global) /* node */ as any; + +/** + * CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1) + * add and remove columns (2) change column sizes and (3) move documents within and between columns. This + * view is reminiscent of Kanban-style web apps like Trello, or the 'Board' view in Notion. Each column is + * headed by a SchemaHeaderField followed by the column's documents. SchemaHeaderFields are NOT present in + * the rest of Dash, so it may be worthwhile to transition the headers to simple documents. + */ +@observer +export class CollectionNoteTakingView extends CollectionSubView() { + _disposers: { [key: string]: IReactionDisposer } = {}; + _masonryGridRef: HTMLDivElement | null = null; + _draggerRef = React.createRef<HTMLDivElement>(); + notetakingCategoryField = 'NotetakingCategory'; + public DividerWidth = 16; + @observable docsDraggedRowCol: number[] = []; + @observable _cursor: CursorProperty = 'grab'; + @observable _scroll = 0; + @computed get chromeHidden() { + return BoolCast(this.layoutDoc.chromeHidden); + } + // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns + @computed get columnHeaders() { + const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null); + const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset')); + if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) { + setTimeout(() => { + const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null); + const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset')); + if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) { + if (columnHeaders) columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)); + else this.dataDoc.columnHeaders = new List<SchemaHeaderField>(); + } + }); + } + return columnHeaders ?? ([] as SchemaHeaderField[]); + } + @computed get headerMargin() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); + } + @computed get xMargin() { + return NumCast(this.layoutDoc._xMargin, 5); + } + @computed get yMargin() { + return NumCast(this.layoutDoc._yMargin, 5); + } + @computed get gridGap() { + return NumCast(this.layoutDoc._gridGap, 10); + } + // numGroupColumns returns the number of columns + @computed get numGroupColumns() { + return this.columnHeaders.length; + } + // PanelWidth returns the size of the total available space the view occupies + @computed get PanelWidth() { + return this.props.PanelWidth(); + } + // maxColWidth returns the maximum column width, which is slightly less than the total available space. + @computed get maxColWidth() { + return this.props.PanelWidth(); + } + // availableWidth is the total amount of non-divider width. Since widths are stored relatively, + // we use availableWidth to convert from a percentage to a pixel count. + @computed get availableWidth() { + const numDividers = this.numGroupColumns - 1; + return this.maxColWidth - numDividers * this.DividerWidth; + } + + // children is passed as a prop to the NoteTakingField, which uses this function + // to render the docs you see within an individual column. + children = (docs: Doc[]) => { + TraceMobx(); + return docs.map((d, i) => { + const height = () => this.getDocHeight(d); + const width = () => this.getDocWidth(d); + const style = { width: width(), marginTop: this.gridGap, height: height() }; + return ( + <div className={`collectionNoteTakingView-columnDoc`} key={d[Id]} style={style}> + {this.getDisplayDoc(d, width)} + </div> + ); + }); + }; + + // Sections is one of the more important functions in this file, rendering the the documents + // for the UI. It properly renders documents being dragged between columns. + // [CAVEATS] (1) keep track of the offsetting + // (2) documentView gets unmounted as you remove it from the list + @computed get Sections() { + TraceMobx(); + const columnHeaders = this.columnHeaders; + // filter out the currently dragged docs from the child docs, since we will insert them later + const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d)); + const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); + const rowCol = this.docsDraggedRowCol; + // this will sort the docs into the correct columns (minus the ones you're currently dragging) + docs.map(d => { + const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; + // look for if header exists already + const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); + if (existingHeader) { + sections.get(existingHeader)!.push(d); + } + }); + // now we add back in the docs that we're dragging + if (rowCol.length && columnHeaders.length > rowCol[1]) { + const offset = 0; + sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged); + } + return sections; + } + + removeDocDragHighlight = () => { + setTimeout( + action(() => (this.docsDraggedRowCol.length = 0)), + 100 + ); + }; + + componentDidMount() { + super.componentDidMount?.(); + document.addEventListener('pointerup', this.removeDocDragHighlight, true); + this._disposers.autoHeight = reaction( + () => this.layoutDoc._autoHeight, + autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))))) + ); + } + + componentWillUnmount() { + document.removeEventListener('pointerup', this.removeDocDragHighlight, true); + super.componentWillUnmount(); + Object.keys(this._disposers).forEach(key => this._disposers[key]()); + } + + @action + moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => { + return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false; + }; + + createRef = (ele: HTMLDivElement | null) => { + this._masonryGridRef = ele; + this.createDashEventsTarget(ele!); //so the whole grid is the drop target? + }; + + @computed get onChildClickHandler() { + return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); + } + + @computed get onChildDoubleClickHandler() { + return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); + } + + addDocTab = (doc: Doc, where: string) => { + if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + }; + + scrollToBottom = () => { + smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); + }; + + // let's dive in and get the actual document we want to drag/move around + focusDocument = (doc: Doc, options?: DocFocusOptions) => { + Doc.BrushDoc(doc); + let focusSpeed = 0; + const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); + if (found) { + const top = found.getBoundingClientRect().top; + const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); + if (Math.floor(localTop[1]) !== 0) { + smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + } + } + const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing); + this.props.focus(this.rootDoc, { + willZoom: options?.willZoom, + scale: options?.scale, + afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)), + }); + }; + + styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { + if (property === StyleProp.BoxShadow && doc && DragManager.docsBeingDragged.includes(doc)) { + return `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}`; + } + if (property === StyleProp.Opacity && doc) { + if (this.props.childOpacity) { + return this.props.childOpacity(); + } + } + return this.props.styleProvider?.(doc, props, property); + }; + + isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + + // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView) + getDisplayDoc(doc: Doc, width: () => number) { + const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc; + const height = () => this.getDocHeight(doc); + let dref: Opt<DocumentView>; + const noteTakingDocTransform = () => this.getDocTransform(doc, dref); + return ( + <DocumentView + ref={r => (dref = r || undefined)} + Document={doc} + DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} + renderDepth={this.props.renderDepth + 1} + PanelWidth={width} + PanelHeight={height} + styleProvider={this.styleProvider} + docViewPath={this.props.docViewPath} + fitWidth={this.props.childFitWidth} + isContentActive={emptyFunction} + onKey={this.onKeyDown} + //TODO: change this from a prop to a parameter passed into a function + dontHideOnDrag={true} + isDocumentActive={this.isContentActive} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={this.props.childLayoutString} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} + dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined} + dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} + rootSelected={this.rootSelected} + showTitle={this.props.childShowTitle} + dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + ScreenToLocalTransform={noteTakingDocTransform} + focus={this.focusDocument} + docFilters={this.childDocFilters} + hideDecorationTitle={this.props.childHideDecorationTitle?.()} + hideResizeHandles={this.props.childHideResizeHandles?.()} + hideTitle={this.props.childHideTitle?.()} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.addDocTab} + bringToFront={returnFalse} + scriptContext={this.props.scriptContext} + pinToPres={this.props.pinToPres} + /> + ); + } + + // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns + getDocTransform(doc: Doc, dref?: DocumentView) { + const y = this._scroll; // required for document decorations to update when the text box container is scrolled + const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined); + // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off + return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale); + } + + // how to get the width of a document. Currently returns the width of the column (minus margins) + // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...) + getDocWidth(d: Doc) { + const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field); + const existingHeader = this.columnHeaders.find(sh => sh.heading === heading); + const existingWidth = existingHeader?.width ? existingHeader.width : 0; + const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth; + const width = d.fitWidth ? maxWidth : d[WidthSym](); + return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth); + } + + // how to get the height of a document. Nothing special here. + getDocHeight(d?: Doc) { + if (!d || d.hidden) return 0; + const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); + const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS ? undefined : this.props.DataDoc; + const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0); + if (nw && nh) { + const docWid = this.getDocWidth(d); + return Math.min(maxHeight, (docWid * nh) / nw); + } + const childHeight = NumCast(childLayoutDoc._height); + const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; + return Math.min(childHeight, maxHeight, panelHeight); + } + + // resizeColumns is called whenever a user adds or removes a column. When removing, + // this function renormalizes the column widths to fill the newly available space + // in the panel. When adding, this function renormalizes the existing columns to take up + // (n - 1)/n space, since the new column will be allocated 1/n of the total space. + // Column widths are relative (portion of available space) and stored in the 'width' + // field of SchemaHeaderFields. + // + // Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375]. + // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33] + @action + resizeColumns = (isAdd: boolean, colWidth: number, colIndex: number) => { + const n = this.columnHeaders.length; + if (n == 1) { + this.columnHeaders[0].setWidth(1); + return true; + } + const scaleFactor = isAdd ? 1 - colWidth : 1 / (1 - colWidth); + this.columnHeaders.forEach((h, i) => { + if (!(isAdd && i == colIndex)) { + h.width < 0 ? h.setWidth(1 / n) : h.setWidth(h.width * scaleFactor); + } + }); + return true; + }; + + // onPointerMove is used to preview where a document will drop in a column once a drag is complete. + @action + onPointerMove = (force: boolean, ex: number, ey: number) => { + if (this.childDocList && (this.childDocList.includes(DragManager.DocDragData?.draggedDocuments.lastElement()!) || force || this.isContentActive())) { + // get the current docs for the column based on the mouse's x coordinate + const xCoord = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[0] - 2 * this.gridGap; + const colDocs = this.getDocsFromXCoord(xCoord); + // get the index for where you need to insert the doc you are currently dragging + const clientY = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[1]; + let dropInd = -1; + let pos0 = (this.refList.lastElement() as HTMLDivElement).children[0].getBoundingClientRect().height + this.yMargin * 2; + colDocs.forEach((doc, i) => { + let pos1 = this.getDocHeight(doc) + 2 * this.gridGap; + pos1 += pos0; + // updating drop position based on y coordinates + const yCoordInBetween = clientY > pos0 && clientY < pos1; + if (yCoordInBetween || (clientY < pos0 && i === 0)) { + dropInd = i; + } else if (i === colDocs.length - 1 && dropInd === -1) { + dropInd = !colDocs.includes(DragManager.docsBeingDragged.lastElement()) ? i + 1 : i; + } + pos0 = pos1; + }); + // we alter the pivot fields of the docs in case they are moved to a new column. + const colIndex = this.getColumnFromXCoord(xCoord); + const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading); + DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader)); + // used to notify sections to re-render + this.docsDraggedRowCol.length = 0; + const columnFromCoord = this.getColumnFromXCoord(xCoord); + columnFromCoord !== undefined && this.docsDraggedRowCol.push(dropInd, columnFromCoord); + } + }; + + // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate). + // This function is used to know which document a column SHOULD be in while it is being dragged. + getColumnFromXCoord = (xCoord: number): number | undefined => { + let colIndex: number | undefined = undefined; + const numColumns = this.columnHeaders.length; + const coords = []; + let colStartXCoord = 0; + for (let i = 0; i < numColumns; i++) { + coords.push(colStartXCoord); + colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.DividerWidth; + } + coords.push(this.PanelWidth); + for (let i = 0; i < numColumns; i++) { + if (xCoord > coords[i] && xCoord < coords[i + 1]) { + colIndex = i; + break; + } + } + return colIndex; + }; + + // getDocsFromXCoord returns the docs of a column based on the x-coordinate provided. + getDocsFromXCoord = (xCoord: number): Doc[] => { + const docsMatchingHeader: Doc[] = []; + const colIndex = this.getColumnFromXCoord(xCoord); + const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading); + this.childDocs?.map(d => { + if (d instanceof Promise) return; + const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset'; + if (sectionValue.toString() == colHeader) { + docsMatchingHeader.push(d); + } + }); + return docsMatchingHeader; + }; + + @undoBatch + @action + onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { + const docView = fieldProps.DocumentView?.(); + if (docView && (e.ctrlKey || docView.rootDoc._singleLine) && ['Enter'].includes(e.key)) { + e.stopPropagation?.(); + const newDoc = Doc.MakeCopy(docView.rootDoc, true); + Doc.GetProto(newDoc).text = undefined; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + return this.addDocument?.(newDoc); + } + }; + + // onInternalDrop is used when dragging and dropping a document within the view, such as dragging + // a document to a new column or changing its order within the column. + @undoBatch + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + if (super.onInternalDrop(e, de)) { + // filter out the currently dragged docs from the child docs, since we will insert them later + const rowCol = this.docsDraggedRowCol; + const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). + const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; + const docs = this.childDocList; + if (docs && newDocs.length) { + // remove the dragged documents from the childDocList + newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1)); + // if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly + if (rowCol[0] <= 0) { + docs.splice(0, 0, ...newDocs); + } else { + const colDocs = this.getDocsFromXCoord(this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]); + const previousDoc = colDocs[rowCol[0] - 1]; + const previousDocIndex = docs.indexOf(previousDoc); + docs.splice(previousDocIndex + 1, 0, ...newDocs); + } + } + } + } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { + const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' }); + this.props.addDocument?.(source); + de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed + e.stopPropagation(); + } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + return false; + }; + + @undoBatch + internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) { + const dropCreator = annoDragData.dropDocCreator; + annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { + const dropDoc = dropCreator(annotationOn); + return dropDoc || this.rootDoc; + }; + return true; + } + + // onExternalDrop is used when dragging a document out from a CollectionNoteTakingView + // to another tab/view/collection + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + const targInd = this.docsDraggedRowCol?.[0] || 0; + const colInd = this.docsDraggedRowCol?.[1] || 0; + super.onExternalDrop( + e, + {}, + undoBatch( + action(docus => { + this.onPointerMove(true, e.clientX, e.clientY); + docus?.map((doc: Doc) => this.addDocument(doc)); + const newDoc = this.childDocs.lastElement(); + const colHeader = colInd === undefined ? 'unset' : StrCast(this.columnHeaders[colInd].heading); + newDoc[this.notetakingCategoryField] = colHeader; + const docs = this.childDocList; + if (docs && targInd !== -1) { + docs.splice(docs.length - 1, 1); + docs.splice(targInd, 0, newDoc); + } + this.removeDocDragHighlight(); + }) + ) + ); + }; + + headings = () => Array.from(this.Sections); + + refList: any[] = []; + + editableViewProps = () => ({ + GetValue: () => '', + SetValue: this.addGroup, + contents: '+ New Column', + }); + + // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column) + sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { + const type = 'number'; + return ( + <CollectionNoteTakingViewColumn + unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)} + observeHeight={ref => { + if (ref) { + this.refList.push(ref); + this.observer = new _global.ResizeObserver( + action((entries: any) => { + if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))); + if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) { + this.props.setHeight?.(height); + } + } + }) + ); + this.observer.observe(ref); + } + }} + addDocument={this.addDocument} + chromeHidden={this.chromeHidden} + columnHeaders={this.columnHeaders} + Document={this.props.Document} + DataDoc={this.props.DataDoc} + resizeColumns={this.resizeColumns} + renderChildren={this.children} + numGroupColumns={this.numGroupColumns} + gridGap={this.gridGap} + pivotField={this.notetakingCategoryField} + dividerWidth={this.DividerWidth} + maxColWidth={this.maxColWidth} + availableWidth={this.availableWidth} + PanelWidth={this.PanelWidth} + key={heading?.heading ?? 'unset'} + headings={this.headings} + heading={heading?.heading ?? 'unset'} + headingObject={heading} + docList={docList} + yMargin={this.yMargin} + type={type} + createDropTarget={this.createDashEventsTarget} + screenToLocalTransform={this.props.ScreenToLocalTransform} + editableViewProps={this.editableViewProps} + /> + ); + }; + + // addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of + // columnHeaders and resizing the existing columns to make room for our new one. + @undoBatch + @action + addGroup = (value: string) => { + if (this.columnHeaders) { + for (const header of this.columnHeaders) { + if (header.heading == value) { + alert('You cannot use an existing column name. Please try a new column name'); + return value; + } + } + } + const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); + const newColWidth = 1 / (this.numGroupColumns + 1); + value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1); + this.dataDoc.columnHeaders = new List<SchemaHeaderField>(columnHeaders.map(header => header[Copy]())); + return true; + }; + + onContextMenu = (e: React.MouseEvent): void => { + // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout + if (!e.isPropagationStopped()) { + const subItems: ContextMenuProps[] = []; + subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' }); + subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' }); + subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' }); + ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' }); + } + }; + + // setColumnStartXCoords is used to update column widths when using the drag handlers between columns + @action + setColumnStartXCoords = (movementXScreen: number, colIndex: number) => { + const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0]; + const leftHeader = this.columnHeaders[colIndex]; + const rightHeader = this.columnHeaders[colIndex + 1]; + leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); + rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); + }; + + // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view + // has more than one column, those columns will be separated by a CollectionNoteTakingViewDivider that + // allows the user to adjust the column widths. + @computed get renderedSections() { + TraceMobx(); + const entries = Array.from(this.Sections.entries()); + const sections = entries; + const eles: JSX.Element[] = []; + for (let i = 0; i < sections.length; i++) { + const col = this.sectionNoteTaking(sections[i][0], sections[i][1]); + eles.push(col); + if (i < sections.length - 1) { + eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); + } + } + return eles; + } + + @computed get buttonMenu() { + const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + if (menuDoc) { + const width = NumCast(menuDoc._width, 30); + const height = NumCast(menuDoc._height, 30); + return ( + <div className="buttonMenu-docBtn" style={{ width, height }}> + <DocumentView + Document={menuDoc} + DataDoc={menuDoc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => 35} + PanelHeight={() => 35} + renderDepth={this.props.renderDepth} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div> + ); + } + } + + @computed get nativeWidth() { + return Doc.NativeWidth(this.layoutDoc); + } + @computed get nativeHeight() { + return Doc.NativeHeight(this.layoutDoc); + } + + @computed get scaling() { + return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; + } + + @computed get backgroundEvents() { + return SnappingManager.GetIsDragging(); + } + + observer: any; + + render() { + TraceMobx(); + const buttonMenu = this.rootDoc.buttonMenu; + const noviceExplainer = StrCast(this.rootDoc.explainer); + return ( + <> + {buttonMenu || noviceExplainer ? ( + <div className="documentButtonMenu" key="buttons"> + {buttonMenu ? this.buttonMenu : null} + {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null} + </div> + ) : null} + <div + className="collectionNoteTakingView" + ref={this.createRef} + key="notes" + style={{ + overflowY: this.props.isContentActive() ? 'auto' : 'hidden', + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), + pointerEvents: this.backgroundEvents ? 'all' : undefined, + }} + onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} + onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} + onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} + onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} + onDrop={this.onExternalDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}> + {this.renderedSections} + </div> + </> + ); + } +} diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx new file mode 100644 index 000000000..84d1c0205 --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -0,0 +1,310 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Copy, Id } from '../../../fields/FieldSymbols'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { Cast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; +import { returnEmptyString } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager } from '../../util/DragManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { EditableView } from '../EditableView'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import './CollectionNoteTakingView.scss'; + +interface CSVFieldColumnProps { + Document: Doc; + DataDoc: Opt<Doc>; + docList: Doc[]; + heading: string; + pivotField: string; + chromeHidden?: boolean; + columnHeaders: SchemaHeaderField[] | undefined; + headingObject: SchemaHeaderField | undefined; + yMargin: number; + numGroupColumns: number; + gridGap: number; + type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; + headings: () => object[]; + renderChildren: (docs: Doc[]) => JSX.Element[]; + addDocument: (doc: Doc | Doc[]) => boolean; + createDropTarget: (ele: HTMLDivElement) => void; + screenToLocalTransform: () => Transform; + observeHeight: (myref: any) => void; + unobserveHeight: (myref: any) => void; + editableViewProps: () => any; + resizeColumns: (isAdd: boolean, colWidth: number, colIndex: number) => boolean; + PanelWidth: number; + maxColWidth: number; + dividerWidth: number; + availableWidth: number; +} + +/** + * CollectionNoteTakingViewColumn represents an individual column rendered in CollectionNoteTakingView. The + * majority of functions here are for rendering styles. + */ +@observer +export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColumnProps> { + @observable private _background = 'inherit'; + + // columnWidth returns the width of a column in absolute pixels + @computed get columnWidth() { + if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length === 1) return '100%'; + const i = this.props.columnHeaders.indexOf(this.props.headingObject); + return this.props.columnHeaders[i].width * 100 + '%'; + } + + private dropDisposer?: DragManager.DragDropDisposer; + private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); + + public static ColumnMargin = 10; + @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; + @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb'; + _ele: HTMLElement | null = null; + + createColumnDropRef = (ele: HTMLDivElement | null) => { + this.dropDisposer?.(); + if (ele) { + this._ele = ele; + this.props.observeHeight(ele); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); + } + }; + + componentWillUnmount() { + this.props.unobserveHeight(this._ele); + } + + @undoBatch + columnDrop = action((e: Event, de: DragManager.DropEvent) => { + const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; + drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); + }); + + getValue = (value: string): any => { + const parsed = parseInt(value); + if (!isNaN(parsed)) return parsed; + if (value.toLowerCase().indexOf('true') > -1) return true; + if (value.toLowerCase().indexOf('false') > -1) return false; + return value; + }; + + @action + headingChanged = (value: string, shiftDown?: boolean) => { + const castedValue = this.getValue(value); + if (castedValue) { + if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { + return false; + } + this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue)); + if (this.props.headingObject) { + this.props.headingObject.setHeading(castedValue.toString()); + this._heading = this.props.headingObject.heading; + } + return true; + } + return false; + }; + + @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); + @action pointerLeave = () => (this._background = 'inherit'); + textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + + // addNewTextDoc is called when a user starts typing in a column to create a new node + @action + addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { + if (!value && !forceEmptyNote) return false; + const key = this.props.pivotField; + const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true }); + const colValue = this.getValue(this.props.heading); + newDoc[key] = colValue; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; + return this.props.addDocument?.(newDoc) || false; + }; + + // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area. + // If the user deletes the first column, the documents get moved to the second column. Otherwise, + // all docs are added to the column directly to the left. + @undoBatch + @action + deleteColumn = () => { + const acolumnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); + if (acolumnHeaders && this.props.headingObject) { + const index = acolumnHeaders.indexOf(this.props.headingObject); + const columnHeaders = acolumnHeaders; // new List<SchemaHeaderField>(acolumnHeaders.map(header => header[Copy]())); // needed for undo to work properly. otherwise we end up changing field values in the undo stack since they are shared by reference + const newColIndex = index > 0 ? index - 1 : 1; + const newColHeader = this.props.columnHeaders ? this.props.columnHeaders[newColIndex] : undefined; + const newHeading = newColHeader ? newColHeader.heading : 'unset'; + this.props.docList.forEach(d => (d[this.props.pivotField] = newHeading)); + const colWidth = this.props.columnHeaders ? this.props.columnHeaders[index].width : 0; + columnHeaders.splice(index, 1); + //Doc.GetProto(this.props.Document).columnHeaders = columnHeaders; + this.props.resizeColumns(false, colWidth, index); + } + }; + + menuCallback = (x: number, y: number) => { + ContextMenu.Instance.clearItems(); + const layoutItems: ContextMenuProps[] = []; + const docItems: ContextMenuProps[] = []; + const dataDoc = this.props.DataDoc || this.props.Document; + const pivotValue = this.getValue(this.props.heading); + + DocUtils.addDocumentCreatorMenuItems( + doc => { + const key = this.props.pivotField; + doc[key] = this.getValue(this.props.heading); + FormattedTextBox.SelectOnLoad = doc[Id]; + return this.props.addDocument?.(doc); + }, + this.props.addDocument, + x, + y, + true, + this.props.pivotField, + pivotValue + ); + + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string') + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document)); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + return this.props.addDocument?.(created); + } + }, + icon: 'compress-arrows-alt', + }) + ); + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length) + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); + if (created) { + const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document; + if (container.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, container); + return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + } + return this.props.addDocument?.(created) || false; + } + }, + icon: 'compress-arrows-alt', + }) + ); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); + ContextMenu.Instance.setDefaultItem('::', (name: string): void => { + Doc.GetProto(this.props.Document)[name] = ''; + const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true }); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + this.props.addDocument?.(created); + } + }); + ContextMenu.Instance.displayMenu(x, y, undefined, true); + }; + + @computed get innards() { + TraceMobx(); + const key = this.props.pivotField; + const heading = this._heading; + const columnYMargin = this.props.headingObject ? 0 : this.props.yMargin; + const evContents = heading ? heading : '25'; + const headingView = this.props.headingObject ? ( + <div + key={heading} + className="collectionNoteTakingView-sectionHeader" + ref={this._headerRef} + style={{ + marginTop: 2 * this.props.yMargin, + width: 'calc(100% - 5px)', + }}> + <div + className="collectionNoteTakingView-sectionHeader-subCont" + title={evContents === `No Value` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''} + style={{ background: evContents !== `No Value` ? this._color : 'inherit' }}> + <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} /> + </div> + {(this.props.columnHeaders?.length ?? 0) > 1 && ( + <button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}> + <FontAwesomeIcon icon="trash" size="lg" /> + </button> + )} + </div> + ) : null; + const templatecols = this.columnWidth; + const type = this.props.Document.type; + return ( + <> + {headingView} + <div className="collectionNoteTakingView-columnStack"> + <div + key={`${heading}-stack`} + className={`collectionNoteTakingView-Nodes`} + style={{ + padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`, + gridGap: this.props.gridGap, + gridTemplateColumns: templatecols, + }}> + {this.props.renderChildren(this.props.docList)} + </div> + + {!this.props.chromeHidden && type !== DocumentType.PRES ? ( + <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}> + <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton"> + <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> + </div> + <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton"> + <EditableView {...this.props.editableViewProps()} /> + </div> + </div> + ) : null} + </div> + </> + ); + } + + render() { + TraceMobx(); + const heading = this._heading; + return ( + <div + className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')} + key={heading} + style={{ + width: this.columnWidth, + background: this._background, + }} + ref={this.createColumnDropRef} + onPointerEnter={this.pointerEntered} + onPointerLeave={this.pointerLeave}> + {this.innards} + </div> + ); + } +} diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx new file mode 100644 index 000000000..a1309b11f --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx @@ -0,0 +1,65 @@ +import { action, observable } from 'mobx'; +import * as React from 'react'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { UndoManager } from '../../util/UndoManager'; + +interface DividerProps { + index: number; + xMargin: number; + setColumnStartXCoords: (movementX: number, colIndex: number) => void; +} + +/** + * CollectionNoteTakingViewDivider are dividers between CollectionNoteTakingViewColumns, + * which only appear when there is more than 1 column in CollectionNoteTakingView. Dividers + * are two simple vertical lines that allow the user to alter the widths of CollectionNoteTakingViewColumns. + */ +export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { + const batch = UndoManager.StartBatch('resizing'); + setupMoveUpEvents( + this, + e, + (e, down, delta) => { + this.props.setColumnStartXCoords(delta[0], this.props.index); + return false; + }, + action(() => { + this.isResizingActive = false; + this.isHoverActive = false; + batch.end(); + }), + emptyFunction + ); + this.isResizingActive = true; + }; + + render() { + return ( + <div + className="columnResizer" + style={{ + display: 'flex', + alignItems: 'center', + cursor: 'col-resize', + }} + onPointerEnter={action(() => (this.isHoverActive = true))} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}> + <div + className="columnResizer-handler" + onPointerDown={e => this.registerResizing(e)} + style={{ + height: '95%', + width: 12, + borderRight: '4px solid #282828', + borderLeft: '4px solid #282828', + }} + /> + </div> + ); + } +} diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index bb98e1c99..5a107d2ca 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -1,7 +1,8 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; -.timeline-container { +.collectionStackedTimeline-timelineContainer { height: 100%; + position: absolute; overflow-x: auto; overflow-y: hidden; border: none; @@ -9,13 +10,14 @@ border-width: 0 2px 0 2px; &:hover { + cursor: default; .collectionStackedTimeline-hover { display: block; } } } -.timeline-container:hover + .timeline-hoverUI { +.collectionStackedTimeline-timelineContainer:hover + .timeline-hoverUI { display: flex; justify-content: center; } @@ -72,7 +74,8 @@ border-width: 1px; } - .collectionStackedTimeline-current, .collectionStackedTimeline-hover { + .collectionStackedTimeline-current, + .collectionStackedTimeline-hover { width: 1px; height: 100%; position: absolute; @@ -107,14 +110,15 @@ height: 100%; width: 10px; pointer-events: all; - cursor: ew-resize; z-index: 100; } .collectionStackedTimeline-resizer { right: 0; + cursor: e-resize; } .collectionStackedTimeline-left-resizer { left: 0; + cursor: w-resize; } } @@ -141,5 +145,6 @@ transform: translate(0, -100%); font-weight: bold; + pointer-events: none; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index dcf3f7c51..b29abf083 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -2,7 +2,7 @@ import React = require('react'); import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -244,6 +244,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack if (!wasSelecting) { this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width); wasSelecting = true; + this._timelineWrapper && (this._timelineWrapper.style.cursor = 'ew-resize'); } this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); return false; @@ -260,6 +261,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false)); } (!isClick || !wasSelecting) && (this._markerEnd = undefined); + this._timelineWrapper && (this._timelineWrapper.style.cursor = ''); }), (e, doubleTap) => { if (e.button !== 2) { @@ -289,10 +291,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this._hoverTime = this.toTimeline(clientX - rect.x, rect.width); if (this.dataDoc.thumbnails) { const nearest = Math.floor((this._hoverTime / this.props.rawDuration) * VideoBox.numThumbnails); - const thumbnails = Cast(this.dataDoc.thumbnails, listSpec('string'), []); - const imgField = thumbnails && thumbnails.length > 0 ? new ImageField(thumbnails[nearest]) : new ImageField(''); - const src = imgField && imgField.url.href ? imgField.url.href.replace('.png', '_s.png') : ''; - this._thumbnail = src ? src : undefined; + const thumbnails = StrListCast(this.dataDoc.thumbnails); + const imgField = thumbnails?.length > 0 ? new ImageField(thumbnails[nearest]) : undefined; + this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_m.png') : undefined; } } }; @@ -519,7 +520,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack isAnnotationOverlay={true} isDocumentActive={returnFalse} select={emptyFunction} - scaling={returnOne} + NativeDimScaling={returnOne} xMargin={25} yMargin={10} ScreenToLocalTransform={this.dictationScreenToLocalTransform} @@ -561,8 +562,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack return ( <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}> <div - className="timeline-container" - style={{ width: this.props.PanelWidth() }} + className="collectionStackedTimeline-timelineContainer" + style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }} onWheel={e => e.stopPropagation()} onScroll={this.setScroll} onMouseMove={e => this.isContentActive() && this.onHover(e)} @@ -669,7 +670,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack )} </div> </div> - <div className="timeline-hoverUI" style={{ left: `calc(${((this._hoverTime - this.clipStart) / this.clipDuration) * 100}%` }}> + <div className="timeline-hoverUI" style={{ left: ((this._hoverTime - this.clipStart) / this.clipDuration) * this.timelineContentWidth - this._scroll }}> <div className="hoverTime">{formatTime(this._hoverTime - this.clipStart)}</div> {this._thumbnail && <img className="videoBox-thumbnail" src={this._thumbnail} />} </div> diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 73572299a..7385f933b 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .collectionMasonryView { display: inline; @@ -33,7 +33,7 @@ color: $medium-blue; padding: 10px; border-radius: 5px; - border: solid .5px $medium-blue; + border: solid 0.5px $medium-blue; } } @@ -46,9 +46,9 @@ overflow-y: auto; overflow-x: hidden; flex-wrap: wrap; - transition: top .5s; + transition: top 0.5s; - >div { + > div { position: relative; display: block; } @@ -130,9 +130,11 @@ margin-left: -5; } + // Documents in stacking view .collectionStackingView-columnDoc { - display: inline-block; - margin: auto; + display: flex; + // margin: auto; // Removed auto so that it is no longer center aligned - this could be something we change + position: relative; } .collectionStackingView-masonryDoc { @@ -173,7 +175,7 @@ span::before, span::after { - content: ""; + content: ''; width: 50%; border-top: dashed gray 1px; position: relative; @@ -203,6 +205,7 @@ .collectionStackingView-sectionHeader { text-align: center; margin: auto; + margin-bottom: 10px; background: $medium-gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong @@ -213,7 +216,7 @@ .editableView-input:hover, .editableView-container-editing:hover, .editableView-container-editing-oneLine:hover { - cursor: text + cursor: text; } .collectionStackingView-sectionHeader-subCont { @@ -259,7 +262,7 @@ height: 100%; display: none; - [class*="css"] { + [class*='css'] { max-width: 102px; } @@ -301,7 +304,7 @@ height: 100%; display: none; - [class*="css"] { + [class*='css'] { max-width: 102px; } @@ -310,7 +313,6 @@ } .collectionStackingView-optionPicker { - .optionOptions { display: inline; } @@ -370,7 +372,7 @@ .editableView-input:hover, .editableView-container-editing:hover, .editableView-container-editing-oneLine:hover { - cursor: text + cursor: text; } .editableView-input { @@ -385,6 +387,7 @@ .collectionStackingView-addDocumentButton { font-size: 75%; letter-spacing: 2px; + cursor: pointer; .editableView-input { outline-color: black; @@ -422,7 +425,7 @@ top: 4px; border-radius: 50% 50%; background-color: #fff; - content: " "; + content: ' '; cursor: pointer; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); -webkit-transform: scale(1); @@ -452,7 +455,6 @@ } @media only screen and (max-device-width: 480px) { - .collectionStackingView .collectionStackingView-columnDragger, .collectionMasonryView .collectionStackingView-columnDragger { width: 0.1; @@ -460,4 +462,4 @@ opacity: 0; font-size: 0; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6850fb23a..cc006c734 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -34,6 +34,7 @@ const _global = (window /* browser */ || global) /* node */ as any; export type collectionStackingViewProps = { chromeHidden?: boolean; + // view type is stacking viewType?: CollectionViewType; NativeWidth?: () => number; NativeHeight?: () => number; @@ -42,50 +43,69 @@ export type collectionStackingViewProps = { @observer export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() { _masonryGridRef: HTMLDivElement | null = null; + // used in a column dragger, likely due for the masonry grid view. We want to use this _draggerRef = React.createRef<HTMLDivElement>(); + // Not sure what a pivot field is. Seems like we cause reaction in MobX get rid of it once we exit this view _pivotFieldDisposer?: IReactionDisposer; + // Seems like we cause reaction in MobX get rid of our height once we exit this view _autoHeightDisposer?: IReactionDisposer; + // keeping track of documents. Updated on internal and external drops. What's the difference? _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = []; + // Doesn't look like this field is being used anywhere. Obsolete? _columnStart: number = 0; + // map of node headers to their heights. Used in Masonry @observable _heightMap = new Map<string, number>(); + // Assuming that this is the current css cursor style @observable _cursor: CursorProperty = 'grab'; + // gets reset whenever we scroll. Not sure what it is @observable _scroll = 0; // used to force the document decoration to update when scrolling + // does this mean whether the browser is hidden? Or is chrome something else entirely? @computed get chromeHidden() { return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } + // it looks like this gets the column headers that Mehek was showing just now @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null); } + // Still not sure what a pivot is, but it appears that we can actually filter docs somehow? @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } + // filteredChildren is what you want to work with. It's the list of things that you're currently displaying @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); } + // how much margin we give the header @computed get headerMargin() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } @computed get xMargin() { - return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth())); + return NumCast(this.layoutDoc._xMargin, Math.min(3, 0.05 * this.props.PanelWidth())); } @computed get yMargin() { - return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5); - } // 2 * this.gridGap)); } + return this.props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this.props.PanelWidth())); + } + @computed get gridGap() { - return NumCast(this.layoutDoc._gridGap, 10); + return NumCast(this.layoutDoc._gridGap, 5); } + // are we stacking or masonry? @computed get isStackingView() { return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking; } + // this is the number of StackingViewFieldColumns that we have @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } + // reveals a button to add a group in masonry view @computed get showAddAGroup() { return this.pivotField && !this.chromeHidden; } + // columnWidth handles the margin on the left and right side of the documents @computed get columnWidth() { return Math.min(this.props.PanelWidth() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); } + @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } @@ -94,18 +114,26 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection super(props); if (this.columnHeaders === undefined) { + // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out? + // here we're making an empty list of column headers (again, what Mehek showed us) this.layoutDoc._columnHeaders = new List<SchemaHeaderField>(); } } + // TODO: plj - these are the children children = (docs: Doc[]) => { + //TODO: can somebody explain me to what exactly TraceMobX is? TraceMobx(); + // appears that we are going to reset the _docXfs. TODO: what is Xfs? this._docXfs.length = 0; return docs.map((d, i) => { const height = () => this.getDocHeight(d); const width = () => this.getDocWidth(d); + // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); + // just getting the style const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + // So we're choosing whether we're going to render a column or a masonry doc return ( <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}> {this.getDisplayDoc(d, width)} @@ -118,7 +146,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection this._heightMap.set(key, sectionHeight); }; + // is sections that all collections inherit? I think this is how we show the masonry/columns + //TODO: this seems important get Sections() { + // appears that pivot field IS actually for sorting if (!this.pivotField || this.columnHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); if (this.columnHeaders === undefined) { @@ -146,6 +177,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } }); // remove all empty columns if hideHeadings is set + // we will want to have something like this, so that we can hide columns and add them back in if (this.layoutDoc._columnsHideIfEmpty) { Array.from(fields.keys()) .filter(key => !fields.get(key)!.length) @@ -218,6 +250,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); }; + // let's dive in and get the actual document we want to drag/move around focusDocument = (doc: Doc, options?: DocFocusOptions) => { Doc.BrushDoc(doc); @@ -227,7 +260,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0) { - smoothScroll((focusSpeed = doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } } const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing; @@ -268,8 +301,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this.addDocument?.(newDoc); } }; - isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => (this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : undefined); + isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined); + + isChildContentActive = () => + this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined; + // this is what renders the document that you see on the screen + // called in Children: this actually adds a document to our children list getDisplayDoc(doc: Doc, width: () => number) { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); @@ -277,6 +314,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection let dref: Opt<DocumentView>; const stackedDocTransform = () => this.getDocTransform(doc, dref); this._docXfs.push({ stackedDocTransform, width, height }); + //DocumentView is how the node will be rendered return ( <DocumentView ref={r => (dref = r || undefined)} @@ -296,7 +334,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined} - dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} + dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} showTitle={this.props.childShowTitle} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} @@ -357,6 +395,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return Math.min(childHeight, maxHeight, panelHeight); } + // This following three functions must be from the view Mehek showed columnDividerDown = (e: React.PointerEvent) => { runInAction(() => (this._cursor = 'grabbing')); setupMoveUpEvents( @@ -384,10 +423,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection @undoBatch @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + // Fairly confident that this is where the swapping of nodes in the various arrays happens const where = [de.x, de.y]; + // start at -1 until we're sure we want to add it to the column let dropInd = -1; let dropAfter = 0; if (de.complete.docDragData) { + // going to re-add the docs to the _docXFs based on position of where we just dropped this._docXfs.map((cd, i) => { const pos = cd .stackedDocTransform() @@ -402,11 +444,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }); const oldDocs = this.childDocs.length; if (super.onInternalDrop(e, de)) { + // check to see if we actually need anything to the new column of nodes (if droppedDocs != empty) const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= oldDocs); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; // if nothing was added to the end of the list, then presumably the dropped documents were already in the list, but possibly got reordered so we use them. const docs = this.childDocList; - DragManager.docsBeingDragged = []; + // still figuring out where to add the document if (docs && newDocs.length) { const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter; const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0); @@ -433,6 +476,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return true; } + /// an item from outside of Dash is being dropped onto this stacking view (e.g, a document from the file system) @undoBatch @action onExternalDrop = async (e: React.DragEvent): Promise<void> => { @@ -448,19 +492,20 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection targInd = i; } }); - super.onExternalDrop(e, {}, () => { - if (targInd !== -1) { - const newDoc = this.childDocs[this.childDocs.length - 1]; - const docs = this.childDocList; - if (docs) { - docs.splice(docs.length - 1, 1); - docs.splice(targInd, 0, newDoc); + super.onExternalDrop(e, {}, (docs: Doc[]) => { + if (targInd === -1) { + this.addDocument(docs); + } else { + const childDocs = this.childDocList; + if (childDocs) { + childDocs.splice(targInd, 0, ...docs); } } }); }; headings = () => Array.from(this.Sections); refList: any[] = []; + // what a section looks like if we're in stacking view sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined; @@ -512,6 +557,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); }; + // what a section looks like if we're in masonry. Shouldn't actually need to use this. sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { const key = this.pivotField; let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined; @@ -556,6 +602,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); }; + /// add a new group category (column) to the active set of note categories. (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc) @action addGroup = (value: string) => { if (value && this.columnHeaders) { @@ -584,6 +631,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } }; + // @computed get renderedSections() { TraceMobx(); let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index f3a798143..7b268cd49 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,31 +1,32 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; -import { RichTextField } from "../../../fields/RichTextField"; -import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { ImageField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DragManager } from "../../util/DragManager"; -import { SnappingManager } from "../../util/SnappingManager"; -import { Transform } from "../../util/Transform"; -import { undoBatch } from "../../util/UndoManager"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { EditableView } from "../EditableView"; -import "./CollectionStackingView.scss"; -import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; -import { Id } from "../../../fields/FieldSymbols"; -const higflyout = require("@hig/flyout"); +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { RichTextField } from '../../../fields/RichTextField'; +import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { ScriptField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager } from '../../util/DragManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { EditableView } from '../EditableView'; +import './CollectionStackingView.scss'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { Id } from '../../../fields/FieldSymbols'; +const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; +// So this is how we are storing a column interface CSVFieldColumnProps { Document: Doc; DataDoc: Opt<Doc>; @@ -39,8 +40,9 @@ interface CSVFieldColumnProps { columnWidth: number; numGroupColumns: number; gridGap: number; - type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined; + type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; headings: () => object[]; + // I think that stacking view actually has a single column and then supposedly you can add more columns? Unsure renderChildren: (docs: Doc[]) => JSX.Element[]; addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; @@ -51,17 +53,19 @@ interface CSVFieldColumnProps { @observer export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> { - @observable private _background = "inherit"; + @observable private _background = 'inherit'; private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); @observable _paletteOn = false; @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; - @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb'; _ele: HTMLElement | null = null; + // This is likely similar to what we will be doing. Why do we need to make these refs? + // is that the only way to have drop targets? createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) { @@ -69,12 +73,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC this.props.observeHeight(ele); this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); } - } + }; componentWillUnmount() { this.props.unobserveHeight(this._ele); } + //TODO: what is scripting? I found it in SetInPlace def but don't know what that is @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; @@ -83,10 +88,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC getValue = (value: string): any => { const parsed = parseInt(value); if (!isNaN(parsed)) return parsed; - if (value.toLowerCase().indexOf("true") > -1) return true; - if (value.toLowerCase().indexOf("false") > -1) return false; + if (value.toLowerCase().indexOf('true') > -1) return true; + if (value.toLowerCase().indexOf('false') > -1) return false; return value; - } + }; @action headingChanged = (value: string, shiftDown?: boolean) => { @@ -95,7 +100,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { return false; } - this.props.docList.forEach(d => d[this.props.pivotField] = castedValue); + this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue)); if (this.props.headingObject) { this.props.headingObject.setHeading(castedValue.toString()); this._heading = this.props.headingObject.heading; @@ -103,17 +108,17 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC return true; } return false; - } + }; @action changeColumnColor = (color: string) => { this.props.headingObject?.setColor(color); this._color = color; - } + }; - @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = "#b4b4b4"); - @action pointerLeave = () => this._background = "inherit"; - textCallback = (char: string) => this.addNewTextDoc("-typed text-", false, true); + @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); + @action pointerLeave = () => (this._background = 'inherit'); + textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { @@ -121,71 +126,78 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const key = this.props.pivotField; const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); - const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); + const maxHeading = this.props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; FormattedTextBox.SelectOnLoad = newDoc[Id]; - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? "" : " "; + FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; return this.props.addDocument?.(newDoc) || false; - } + }; @action deleteColumn = () => { - this.props.docList.forEach(d => d[this.props.pivotField] = undefined); + this.props.docList.forEach(d => (d[this.props.pivotField] = undefined)); if (this.props.columnHeaders && this.props.headingObject) { const index = this.props.columnHeaders.indexOf(this.props.headingObject); this.props.columnHeaders.splice(index, 1); } - } + }; @action collapseSection = () => { this.props.headingObject?.setCollapsed(!this.props.headingObject.collapsed); this.toggleVisibility(); - } + }; headerDown = (e: React.PointerEvent<HTMLDivElement>) => setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); + //TODO: I think this is where I'm supposed to edit stuff startDrag = (e: PointerEvent, down: number[], delta: number[]) => { + // is MakeAlias a way to make a copy of a doc without rendering it? const alias = Doc.MakeAlias(this.props.Document); alias._width = this.props.columnWidth / (this.props.columnHeaders?.length || 1); alias._pivotField = undefined; let value = this.getValue(this._heading); - value = typeof value === "string" ? `"${value}"` : value; + value = typeof value === 'string' ? `"${value}"` : value; alias.viewSpecScript = ScriptField.MakeFunction(`doc.${this.props.pivotField} === ${value}`, { doc: Doc.name }); if (alias.viewSpecScript) { DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); return true; } return false; - } + }; renderColorPicker = () => { - const gray = "#f1efeb"; + const gray = '#f1efeb'; const selected = this.props.headingObject ? this.props.headingObject.color : gray; - const colors = ["pink2", "purple4", "bluegreen1", "yellow4", "gray", "red2", "bluegreen7", "bluegreen5", "orange1"]; - return <div className="collectionStackingView-colorPicker"> - <div className="colorOptions"> - {colors.map(col => { - const palette = PastelSchemaPalette.get(col); - return <div className={"colorPicker" + (selected === palette ? " active" : "")} - style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />; - })} + const colors = ['pink2', 'purple4', 'bluegreen1', 'yellow4', 'gray', 'red2', 'bluegreen7', 'bluegreen5', 'orange1']; + return ( + <div className="collectionStackingView-colorPicker"> + <div className="colorOptions"> + {colors.map(col => { + const palette = PastelSchemaPalette.get(col); + return <div className={'colorPicker' + (selected === palette ? ' active' : '')} style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />; + })} + </div> </div> - </div>; - } + ); + }; renderMenu = () => { - return <div className="collectionStackingView-optionPicker"> - <div className="optionOptions"> - <div className={"optionPicker" + (true ? " active" : "")} onClick={action(() => { })}>Add options here</div> + return ( + <div className="collectionStackingView-optionPicker"> + <div className="optionOptions"> + <div className={'optionPicker' + (true ? ' active' : '')} onClick={action(() => {})}> + Add options here + </div> + </div> </div> - </div >; - } + ); + }; @observable private collapsed: boolean = false; - private toggleVisibility = action(() => this.collapsed = !this.collapsed); + private toggleVisibility = action(() => (this.collapsed = !this.collapsed)); menuCallback = (x: number, y: number) => { ContextMenu.Instance.clearItems(); @@ -193,42 +205,58 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const docItems: ContextMenuProps[] = []; const dataDoc = this.props.DataDoc || this.props.Document; - DocUtils.addDocumentCreatorMenuItems((doc) => { - FormattedTextBox.SelectOnLoad = doc[Id]; - return this.props.addDocument?.(doc); - }, this.props.addDocument, x, y, true); + DocUtils.addDocumentCreatorMenuItems( + doc => { + FormattedTextBox.SelectOnLoad = doc[Id]; + return this.props.addDocument?.(doc); + }, + this.props.addDocument, + x, + y, + true + ); - Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => - docItems.push({ - description: ":" + fieldKey, event: () => { - const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document)); - if (created) { - if (this.props.Document.isTemplateDoc) { - Doc.MakeMetadataFieldTemplate(created, this.props.Document); + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string') + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document)); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + return this.props.addDocument?.(created); } - return this.props.addDocument?.(created); - } - }, icon: "compress-arrows-alt" - })); - Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => DocListCast(dataDoc[fieldKey]).length).map(fieldKey => - docItems.push({ - description: ":" + fieldKey, event: () => { - const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); - if (created) { - const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document; - if (container.isTemplateDoc) { - Doc.MakeMetadataFieldTemplate(created, container); - return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + }, + icon: 'compress-arrows-alt', + }) + ); + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length) + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); + if (created) { + const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document; + if (container.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, container); + return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + } + return this.props.addDocument?.(created) || false; } - return this.props.addDocument?.(created) || false; - } - }, icon: "compress-arrows-alt" - })); - !Doc.noviceMode && ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); - !Doc.noviceMode && ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); - ContextMenu.Instance.setDefaultItem("::", (name: string): void => { - Doc.GetProto(this.props.Document)[name] = ""; - const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); + }, + icon: 'compress-arrows-alt', + }) + ); + !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' }); + !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); + ContextMenu.Instance.setDefaultItem('::', (name: string): void => { + Doc.GetProto(this.props.Document)[name] = ''; + const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true }); if (created) { if (this.props.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this.props.Document); @@ -238,9 +266,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC }); const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); ContextMenu.Instance.displayMenu(x, y, undefined, true); - } - - + }; @computed get innards() { TraceMobx(); @@ -249,39 +275,39 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const columnYMargin = this.props.headingObject ? 0 : this.props.yMargin; const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); - const evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; - const headingView = this.props.headingObject ? - <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} + const evContents = heading ? heading : this.props?.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`; + const headingView = this.props.headingObject ? ( + <div + key={heading} + className="collectionStackingView-sectionHeader" + ref={this._headerRef} style={{ marginTop: this.props.yMargin, - width: (this.props.columnWidth) / - ((uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1)) || 1) + width: this.props.columnWidth / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1), }}> - <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> + <div className={'collectionStackingView-collapseBar' + (this.props.headingObject.collapsed === true ? ' active' : '')} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} - <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} - title={evContents === `NO ${key.toUpperCase()} VALUE` ? - `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} - style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit" }}> - <EditableView - GetValue={() => evContents} - SetValue={this.headingChanged} - contents={evContents} - oneLine={true} - toggle={this.toggleVisibility} /> - {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div + className="collectionStackingView-sectionHeader-subCont" + onPointerDown={this.headerDown} + title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''} + style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : 'inherit' }}> + <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} toggle={this.toggleVisibility} /> + {evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( <div className="collectionStackingView-sectionColor"> - <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}> <FontAwesomeIcon icon="palette" size="lg" /> </button> - {this._paletteOn ? this.renderColorPicker() : (null)} + {this._paletteOn ? this.renderColorPicker() : null} </div> + )} + { + <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> + <FontAwesomeIcon icon="trash" size="lg" /> + </button> } - {<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> - <FontAwesomeIcon icon="trash" size="lg" /> - </button>} - {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + {evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( <div className="collectionStackingView-sectionOptions"> <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> <button className="collectionStackingView-sectionOptionButton"> @@ -289,65 +315,76 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </button> </Flyout> </div> - } + )} </div> - </div> : (null); + </div> + ) : null; const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `; const type = this.props.Document.type; - return <> - {this.props.Document._columnsHideIfEmpty ? (null) : headingView} - { - this.collapsed ? (null) : + return ( + <> + {this.props.Document._columnsHideIfEmpty ? null : headingView} + {this.collapsed ? null : ( <div> - <div key={`${heading}-stack`} className={`collectionStackingView-masonrySingle`} + <div + key={`${heading}-stack`} + className={`collectionStackingView-masonrySingle`} style={{ padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`, - margin: "auto", - width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + margin: 'auto', + width: 'max-content', //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', - position: "relative", + position: 'relative', gridGap: this.props.gridGap, gridTemplateColumns: templatecols, - gridAutoRows: "0px" + gridAutoRows: '0px', }}> {this.props.renderChildren(this.props.docList)} </div> - {!this.props.chromeHidden ? - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}> + {!this.props.chromeHidden && type !== DocumentType.PRES ? ( + // TODO: this is the "new" button: see what you can work with here + // change cursor to pointer for this, and update dragging cursor + //TODO: there is a bug that occurs when adding a freeform document and trying to move it around + //TODO: would be great if there was additional space beyond the frame, so that you can actually see your + // bottom note + //TODO: ok, so we are using a single column, and this is it! + <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10, marginLeft: 25 }}> <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} - contents={"+ NEW"} + placeholder={"Type ':' for commands"} + contents={<FontAwesomeIcon icon={'plus'} />} toggle={this.toggleVisibility} - menuCallback={this.menuCallback} /> + menuCallback={this.menuCallback} + /> </div> - : null - } - - + ) : null} </div> - } - </>; + )} + </> + ); } - render() { TraceMobx(); const headings = this.props.headings(); const heading = this._heading; const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); return ( - <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading} + <div + className={'collectionStackingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')} + key={heading} style={{ width: `${100 / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1)}%`, height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, - background: this._background + background: this._background, }} - ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> + ref={this.createColumnDropRef} + onPointerEnter={this.pointerEntered} + onPointerLeave={this.pointerLeave}> {this.innards} - </div > + </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5479929bd..4227955ef 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -88,11 +88,11 @@ export function CollectionSubView<X>(moreProps?: X) { } collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters); collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []); + // child filters apply to the descendants of the documents in this collection childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; + // unrecursive filters apply to the documents in the collection, but no their children. See Utils.noRecursionHack unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()]; - IsFiltered = () => - this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined; searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs); @computed.struct get childDocs() { TraceMobx(); @@ -122,13 +122,11 @@ export function CollectionSubView<X>(moreProps?: X) { return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one } - // console.log(Doc.ActiveDashboard._docFilters); - // if (!this.props.Document._docFilters && this.props.Document.currentFilter) { - // (this.props.Document.currentFilter as Doc).filterBoolean = (this.props.ContainingCollectionDoc?.currentFilter as Doc)?.filterBoolean; - // } const docsforFilter: Doc[] = []; childDocs.forEach(d => { - // if (DocUtils.Excluded(d, docFilters)) return; + // dragging facets + const dragged = this.props.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); + if (dragged && DragManager.docsBeingDragged.includes(d)) return false; let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, viewSpecScript, this.props.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0; @@ -448,6 +446,7 @@ export function CollectionSubView<X>(moreProps?: X) { } this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end); } + slowLoadDocuments = async ( files: File[] | string, options: DocumentOptions, @@ -458,11 +457,24 @@ export function CollectionSubView<X>(moreProps?: X) { clientY: number, addDocument: (doc: Doc | Doc[]) => boolean ) => { - const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 }); + // create placeholder docs + // inside placeholder docs have some func that + + let pileUpDoc = undefined; if (typeof files === 'string') { - generatedDocuments.push(...(await DocUtils.uploadYoutubeVideo(files, options))); + const loading = Docs.Create.LoadingDocument(files, options); + generatedDocuments.push(loading); + loading.isLoading = true; + DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { - generatedDocuments.push(...(await DocUtils.uploadFilesToDocs(files, options))); + generatedDocuments.push( + ...files.map(file => { + const loading = Docs.Create.LoadingDocument(file, options); + loading.isLoading = true; + DocUtils.uploadFileToDoc(file, {}, loading); + return loading; + }) + ); } if (generatedDocuments.length) { // Creating a dash document @@ -478,7 +490,8 @@ export function CollectionSubView<X>(moreProps?: X) { if (completed) completed(set); else { if (isFreeformView && generatedDocuments.length > 1) { - addDocument(DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!); + pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!; + addDocument(pileUpDoc); } else { generatedDocuments.forEach(addDocument); } @@ -490,7 +503,6 @@ export function CollectionSubView<X>(moreProps?: X) { alert('Document upload failed - possibly an unsupported file type.'); } } - disposer(); }; } @@ -503,5 +515,4 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes' import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; -import { OverlayView } from '../OverlayView'; import { CollectionView, CollectionViewProps } from './CollectionView'; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 93523a6cf..c0561e42c 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,6 +1,5 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; - .collectionTreeView-container { transform-origin: top left; height: 100%; @@ -28,7 +27,7 @@ list-style: none; padding-left: $TREE_BULLET_WIDTH; margin-bottom: 1px; // otherwise vertical scrollbars may pop up for no apparent reason.... - > .contentFittingDocumentView { + > .contentFittingDocumentView { width: unset; height: unset; } @@ -39,7 +38,7 @@ .no-indent { padding-left: 0; - width: max-content; + //width: max-content; } .no-indent-outline { @@ -85,7 +84,7 @@ width: 100%; height: max-content; .contentFittingDocumentView { - display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason) + display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason) } } @@ -114,4 +113,4 @@ padding-left: 3px; padding-right: 3px; padding-bottom: 2px; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 809a73a77..dce792d19 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -235,7 +235,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight PanelWidth={this.documentTitleWidth} PanelHeight={this.documentTitleHeight} - scaling={returnOne} + NativeDimScaling={returnOne} onKey={this.onKey} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} @@ -351,7 +351,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return Doc.NativeHeight(this.Document, undefined, true); } - @computed get contentScaling() { + /// scale factor for tree view so that it will fit within it's panel bounds + @computed get nativeDimScaling() { const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; @@ -365,7 +366,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); - panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1); + panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.NativeDimScaling?.() || 1); addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false; remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false; @@ -389,9 +390,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree <div className="collectionTreeView-container" style={{ - transform: this.outlineMode ? `scale(${this.contentScaling})` : '', + transform: this.outlineMode ? `scale(${this.nativeDimScaling})` : '', paddingLeft: `${this.marginX()}px`, - width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : '', + width: this.outlineMode ? `calc(${100 / this.nativeDimScaling}%)` : '', }} onContextMenu={this.onContextMenu}> {!this.buttonMenu && !this.noviceExplainer ? null : ( diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 2ab5f6247..1ee77d4ce 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -26,6 +26,7 @@ import { CollectionGridView } from './collectionGrid/CollectionGridView'; import { CollectionLinearView } from './collectionLinear'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; +import { CollectionNoteTakingView } from './CollectionNoteTakingView'; import { CollectionPileView } from './CollectionPileView'; import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView'; import { CollectionStackingView } from './CollectionStackingView'; @@ -125,6 +126,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab case CollectionViewType.Carousel: return <CollectionCarouselView key="collview" {...props} />; case CollectionViewType.Carousel3D: return <CollectionCarousel3DView key="collview" {...props} />; case CollectionViewType.Stacking: return <CollectionStackingView key="collview" {...props} />; + case CollectionViewType.NoteTaking: return <CollectionNoteTakingView key="collview" {...props} />; case CollectionViewType.Masonry: return <CollectionStackingView key="collview" {...props} />; case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />; case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />; @@ -141,6 +143,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab subItems.push({ description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' }); subItems.push({ description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' }); subItems.push({ description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._autoHeight = true), icon: 'ellipsis-v' }); + subItems.push({ description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._autoHeight = true), icon: 'ellipsis-v' }); subItems.push({ description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' }); subItems.push({ description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' }); subItems.push({ description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' }); @@ -225,7 +228,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab }, }) ); - DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null).data).forEach(childClick => + DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null)?.data).forEach(childClick => onClicks.push({ description: `Set child ${childClick.title}`, icon: 'edit', diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 7614154c0..30fa33b52 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -212,15 +212,12 @@ export class TabDocView extends React.Component<TabDocViewProps> { @action public static PinDoc(docs: Doc | Doc[], pinProps?: PinProps) { const docList = docs instanceof Doc ? [docs] : docs; - const batch = UndoManager.StartBatch('pinning doc'); let curPres = Doc.ActivePresentation console.log(curPres) - // let curPres = Doc.ActiveDashboard!['presentation'] if (!curPres) { curPres = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true); Doc.ActivePresentation = curPres - // Doc.ActiveDashboard!['presentation'] = curPres } curPres && @@ -246,46 +243,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.treeViewGrowsHorizontally = true; // the document expands horizontally when displayed as a tree view header pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header const presArray: Doc[] = PresBox.Instance?.sortArray(); - const size: number = PresBox.Instance?._selectedArray.size; + const size: number = PresBox.Instance?.selectedArray.size; const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); - // If pinWithView option set then update scale and x / y props of slide - if (pinProps?.pinWithView) { - const viewProps = pinProps.pinWithView; - pinDoc.presPinView = true; - pinDoc.presPinViewX = viewProps.bounds.left + viewProps.bounds.width / 2; - pinDoc.presPinViewY = viewProps.bounds.top + viewProps.bounds.height / 2; - pinDoc.presPinViewScale = viewProps.scale; - pinDoc.contentBounds = new List<number>([viewProps.bounds.left, viewProps.bounds.top, viewProps.bounds.left + viewProps.bounds.width, viewProps.bounds.top + viewProps.bounds.height]); - } - if (pinProps?.pinDocView) { - const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(pinDoc.type as any) || pinDoc._viewType === CollectionViewType.Stacking; - const pannable: boolean = (pinDoc.type === DocumentType.COL && doc._viewType === CollectionViewType.Freeform) || doc.type === DocumentType.IMG; - if (scrollable) { - const scroll = doc._scrollTop; - pinDoc.presPinView = true; - pinDoc.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(doc.type as any)) { - pinDoc.presPinView = true; - pinDoc.presStartTime = doc._currentTimecode; - pinDoc.presEndTime = NumCast(doc._currentTimecode) + 0.1; - } else if (pannable) { - pinDoc.presPinView = true; - pinDoc.presPinViewX = pinDoc._panX; - pinDoc.presPinViewY = pinDoc._panY; - pinDoc.presPinViewScale = pinDoc._viewScale; - const pw = NumCast(pinProps.panelWidth); - const ph = NumCast(pinProps.panelHeight); - const ps = NumCast(pinDoc._viewScale); - if (pw && ph && ps) { - pinDoc.contentBounds = new List<number>([NumCast(pinDoc.panX) - pw / 2 / ps, NumCast(pinDoc.panY) - ph / 2 / ps, NumCast(pinDoc.panX) + pw / 2 / ps, NumCast(pinDoc.panY) + ph / 2 / ps]); - } - } else if (doc.type === DocumentType.COMPARISON) { - const width = doc._clipWidth; - pinDoc.presPinClipWidth = width; - pinDoc.presPinView = true; - } - } + + PresBox.pinDocView(pinDoc, pinProps, doc); pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)'); Doc.AddDocToList(curPres, 'data', pinDoc, presSelected); if (!pinProps?.audioRange && duration !== undefined) { @@ -295,31 +257,33 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.presEndTime = NumCast(doc.clipEnd, duration); } //save position - if (pinProps?.setPosition || pinDoc.isInkMask) { - pinDoc.setPosition = true; - pinDoc.y = doc.y; - pinDoc.x = doc.x; + if (pinProps?.activeFrame !== undefined) { + pinDoc.presActiveFrame = pinProps?.activeFrame; + pinDoc.title = doc.title + ' (move)'; + pinDoc.presMovement = PresMovement.Pan; + } + if (pinDoc.isInkMask) { pinDoc.presHideAfter = true; pinDoc.presHideBefore = true; - pinDoc.title = doc.title + ' (move)'; pinDoc.presMovement = PresMovement.None; } if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - PresBox.Instance?._selectedArray.clear(); - pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array + PresBox.Instance?.clearSelectedArray(); + pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array }); - if ( - !Array.from(CollectionDockingView.Instance.tabMap) - .map(d => d.DashDoc) - .includes(curPres) - ) { - const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); - if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); - CollectionDockingView.AddSplit(curPres, 'right'); - setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things - } - setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs - } + if ( + CollectionDockingView.Instance && + !Array.from(CollectionDockingView.Instance.tabMap) + .map(d => d.DashDoc) + .includes(curPres) + ) { + const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); + if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); + CollectionDockingView.AddSplit(curPres, 'right'); + setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things + } + setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs + } componentDidMount() { new _global.ResizeObserver( @@ -405,7 +369,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { focusFunc = (doc: Doc, options?: DocFocusOptions) => { const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap; if (shrinkwrap && this._document) { - const focusSpeed = 1000; + const focusSpeed = NumCast(this._document.focusSpeed, 500); shrinkwrap(); this._document._viewTransition = `transform ${focusSpeed}ms`; setTimeout( @@ -427,7 +391,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { ScreenToLocalTransform = () => { this._forceInvalidateScreenToLocal; const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement); - return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY); + return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY) ?? Transform.Identity(); }; PanelWidth = () => this._panelWidth; PanelHeight = () => this._panelHeight; @@ -456,9 +420,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} styleProvider={DefaultStyleProvider} - docFilters={CollectionDockingView.Instance.childDocFilters} - docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} - searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} addDocument={undefined} removeDocument={this.remDocTab} addDocTab={this.addDocTab} @@ -631,9 +595,9 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { styleProvider={TabMinimapView.miniStyleProvider} addDocTab={this.props.addDocTab} pinToPres={TabDocView.PinDoc} - docFilters={CollectionDockingView.Instance.childDocFilters} - docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} - searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} fitContentsToBox={returnTrue} /> <div className="miniOverlay" onPointerDown={this.miniDown}> diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index f587dbbf6..ce87e6f89 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .treeView-label { max-height: 1.5em; @@ -21,7 +21,7 @@ } .treeView-bulletIcons { - // width: $TREE_BULLET_WIDTH; + // width: $TREE_BULLET_WIDTH; width: 100%; height: 100%; @@ -101,6 +101,9 @@ .treeView-border { display: flex; overflow: hidden; + > ul { + width: 100%; + } } .treeView-border { @@ -118,7 +121,6 @@ } .formattedTextBox-cont { - .formattedTextbox-sidebar, .formattedTextbox-sidebar-inking { overflow: visible !important; @@ -144,12 +146,12 @@ pointer-events: all; cursor: pointer; - >svg { + > svg { margin-left: 0.25rem; margin-right: 0.25rem; } - >svg { + > svg { //display: none; opacity: 0; pointer-events: none; @@ -176,8 +178,7 @@ } .treeView-rightButtons { - - >svg, + > svg, .styleProvider-treeView-icon { display: inherit; opacity: unset; @@ -196,4 +197,4 @@ .treeView-header-inside { border: black 1px solid; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 5a2103e98..aa1330762 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -403,7 +403,7 @@ export class TreeView extends React.Component<TreeViewProps> { const aspect = Doc.NativeAspect(layoutDoc); if (layoutDoc._fitWidth) return Math.min(this.props.panelWidth() - treeBulletWidth(), layoutDoc[WidthSym]()); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - treeBulletWidth())); - return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.scaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); + return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); }; docHeight = () => { const layoutDoc = this.layoutDoc; @@ -514,7 +514,7 @@ export class TreeView extends React.Component<TreeViewProps> { rtfWidth = () => { const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; - return Math.min(layout[WidthSym](), this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.scaling?.() || 1); + return Math.min(layout[WidthSym](), this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1); }; rtfHeight = () => { const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; @@ -921,7 +921,6 @@ export class TreeView extends React.Component<TreeViewProps> { searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={this.props.treeView.props.Document} - ContentScaling={returnOne} /> ); @@ -992,7 +991,6 @@ export class TreeView extends React.Component<TreeViewProps> { hideResizeHandles={this.props.treeView.outlineMode} onClick={this.onChildClick} focus={this.refocus} - ContentScaling={returnOne} onKey={this.onKeyDown} hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index a0ebe4cdc..89cc22d07 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -34,6 +34,7 @@ export interface PoolData { zIndex?: number; width?: number; height?: number; + backgroundColor?: string; color?: string; opacity?: number; transition?: string; @@ -45,6 +46,7 @@ export interface PoolData { export interface ViewDefResult { ele: JSX.Element; bounds?: ViewDefBounds; + inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden } function toLabel(target: FieldResult<Field>) { if (typeof target === 'number' || Number(target)) { @@ -393,8 +395,10 @@ function normalizeResults( 0 ); aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x); - const wscale = panelDim[0] / (aggBounds.r - aggBounds.x); - let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? panelDim[1] / (aggBounds.b - aggBounds.y) : wscale; + const width = aggBounds.r - aggBounds.x === 0 ? 1 : aggBounds.r - aggBounds.x; + const height = aggBounds.b - aggBounds.y === 0 ? 1 : aggBounds.b - aggBounds.y; + const wscale = panelDim[0] / width; + let scale = wscale * height > panelDim[1] ? panelDim[1] / height : wscale; if (Number.isNaN(scale)) scale = 1; Array.from(docMap.entries()) diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index d979ef961..bf9de6760 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -195,7 +195,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; const aleft = this.visibleX(adiv); const bleft = this.visibleX(bdiv); - const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top; + const aclipped = aleft !== a.left || atop !== a.top; + const bclipped = bleft !== b.left || btop !== b.top; + if (aclipped && bclipped) return undefined; + const clipped = aclipped || bclipped; const pt1 = [aleft + a.width / 2, atop + a.height / 2]; const pt2 = [bleft + b.width / 2, btop + b.width / 2]; const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]]; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 8720c9097..b8344dc0c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -5,11 +5,14 @@ import { DocumentManager } from '../../../util/DocumentManager'; import './CollectionFreeFormLinksView.scss'; import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView'; import React = require('react'); +import { LightboxView } from '../../LightboxView'; @observer export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> { @computed get uniqueConnections() { - return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)).map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />); + return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)) + .filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath))) + .map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 79e063f7f..d80fcdfc3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,4 +1,4 @@ -@import "../../global/globalCssVariables"; +@import '../../global/globalCssVariables'; .collectionfreeformview-none { position: inherit; @@ -20,8 +20,24 @@ pointer-events: none; } +.collectionfreeformview-mask-empty, +.collectionfreeformview-mask { + z-index: 5000; + width: $INK_MASK_SIZE; + height: $INK_MASK_SIZE; + transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF); + pointer-events: none; + position: absolute; + background-color: transparent; + transition: background-color 1s ease 0s; +} +.collectionfreeformview-mask { + mix-blend-mode: multiply; + background-color: rgba(0, 0, 0, 0.7); +} + .collectionfreeformview-viewdef { - >.collectionFreeFormDocumentView-container { + > .collectionFreeFormDocumentView-container { pointer-events: none; .contentFittingDocumentDocumentView-previewDoc { @@ -210,13 +226,13 @@ } } - .collectionfreeformview>.jsx-parser { + .collectionfreeformview > .jsx-parser { position: inherit; height: 100%; width: 100%; } - >.jsx-parser { + > .jsx-parser { z-index: 0; } @@ -268,6 +284,6 @@ .pullpane-indicator { z-index: 99999; - background-color: rgba($color: #000000, $alpha: .4); + background-color: rgba($color: #000000, $alpha: 0.4); position: absolute; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4174661d8..039407e77 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -23,7 +23,6 @@ import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { HistoryUtil } from '../../../util/History'; import { InteractionUtils } from '../../../util/InteractionUtils'; -import { RecordingApi } from '../../../util/RecordingApi'; import { ReplayMovements } from '../../../util/ReplayMovements'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -35,7 +34,7 @@ import { COLLECTION_BORDER_WIDTH } from '../../../views/global/globalCssVariable import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { GestureOverlay } from '../../GestureOverlay'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView'; @@ -45,7 +44,6 @@ import { PresBox } from '../../nodes/trails/PresBox'; import { VideoBox } from '../../nodes/VideoBox'; import { CreateImage } from '../../nodes/WebBoxRenderer'; import { StyleProp } from '../../StyleProvider'; -import { CollectionDockingView } from '../CollectionDockingView'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; import { TabDocView } from '../TabDocView'; @@ -54,12 +52,11 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; import React = require('react'); -import e = require('connect-flash'); export type collectionFreeformViewProps = { annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; - childPointerEvents?: boolean; + childPointerEvents?: string; scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; @@ -110,19 +107,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _hLines: number[] | undefined; @observable _vLines: number[] | undefined; @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. - @observable _pullCoords: number[] = [0, 0]; - @observable _pullDirection: string = ''; @observable _showAnimTimeline = false; @observable _clusterSets: Doc[][] = []; @observable _deleteList: DocumentView[] = []; @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeRef = React.createRef<HTMLDivElement>(); @observable _marqueeViewRef = React.createRef<MarqueeView>(); - @observable _keyframeEditing = false; @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. @computed get views() { - return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); + const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1).map(ele => ele.ele); + const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask === -1).map(ele => ele.ele); + if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>); + return renderableEles; } @computed get fitToContentVals() { return { @@ -131,7 +128,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; } @computed get fitContentsToBox() { - return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay; + return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox || this.Document.isGroup) && !this.isAnnotationOverlay; } @computed get contentBounds() { const cb = Cast(this.rootDoc.contentBounds, listSpec('number')); @@ -151,11 +148,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } @computed get cachedCenteringShiftX(): number { - const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { - const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { @@ -185,8 +182,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame)); } }; - @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set); - getKeyFrameEditing = () => this._keyframeEditing; onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick); onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); @@ -236,11 +231,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const newBoxes = newBox instanceof Doc ? [newBox] : newBox; for (const newBox of newBoxes) { if (newBox.activeFrame !== undefined) { - const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field}-indexed`]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field]); + const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field.key]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}-indexed`]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field.key]); delete newBox.activeFrame; - CollectionFreeFormDocumentView.animFields.forEach((field, i) => field !== 'opacity' && (newBox[field] = vals[i])); + CollectionFreeFormDocumentView.animFields.forEach((field, i) => field.key !== 'opacity' && (newBox[field.key] = vals[i])); } } if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { @@ -272,16 +267,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1)); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); - const dropPos = this.Document._currentFrame !== undefined ? [dvals.x || 0, dvals.y || 0] : [NumCast(refDoc.x), NumCast(refDoc.y)]; + const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false); - const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - vals.x = x + (vals.x || 0) - dropPos[0]; - vals.y = y + (vals.y || 0) - dropPos[1]; - vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined; + const pvals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); // get filled in values (uses defaults when not value is specified) for position + const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000), false); // get non-default values for everything else + vals.x = x + NumCast(pvals.x) - dropPos[0]; + vals.y = y + NumCast(pvals.y) - dropPos[1]; CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals); } else { d.x = x + NumCast(d.x) - dropPos[0]; @@ -463,20 +458,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getClusterColor = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => { let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1 - if (property !== StyleProp.BackgroundColor) return styleProp; - const cluster = NumCast(doc?.cluster); - if (this.Document._useClusters) { - if (this._clusterSets.length <= cluster) { - setTimeout(() => doc && this.updateCluster(doc)); - } else { - // choose a cluster color from a palette - const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)']; - styleProp = colors[cluster % colors.length]; - const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); - // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set?.map(s => (styleProp = StrCast(s.backgroundColor))); - } - } //else if (doc && NumCast(doc.group, -1) !== -1) styleProp = "gray"; + switch (property) { + case StyleProp.BackgroundColor: + const cluster = NumCast(doc?.cluster); + if (this.Document._useClusters) { + if (this._clusterSets.length <= cluster) { + setTimeout(() => doc && this.updateCluster(doc)); + } else { + // choose a cluster color from a palette + const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)']; + styleProp = colors[cluster % colors.length]; + const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); + // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document + set?.map(s => (styleProp = StrCast(s.backgroundColor))); + } + } + break; + } return styleProp; }; @@ -497,7 +495,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const currentCol = DocListCast(this.rootDoc.currentInkDoc); const rootDocList = DocListCast(this.rootDoc.data); currentCol.push(rootDocList[rootDocList.length - 1]); - console.log(currentCol); this._batch?.end(); } @@ -522,17 +519,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views case InkTool.Eraser: - document.addEventListener('pointermove', this.onEraserMove); - document.addEventListener('pointerup', this.onEraserUp); this._batch = UndoManager.StartBatch('collectionErase'); - e.stopPropagation(); - e.preventDefault(); + setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction); break; case InkTool.None: if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); - document.addEventListener('pointermove', this.onPointerMove); - document.addEventListener('pointerup', this.onPointerUp); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false); } break; } @@ -571,13 +564,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, { - title: 'ink stroke', - x: B.x - ActiveInkWidth() / 2, - y: B.y - ActiveInkWidth() / 2, - _width: B.width + ActiveInkWidth(), - _height: B.height + ActiveInkWidth(), - }); + const inkDoc = Docs.Create.InkDocument( + ActiveInkColor(), + Doc.ActiveTool, + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, + ActiveInkBezierApprox(), + ActiveFillColor(), + ActiveArrowStart(), + ActiveArrowEnd(), + ActiveDash(), + points, + ActiveIsInkMask(), + { + title: 'ink stroke', + x: B.x - ActiveInkWidth() / 2, + y: B.y - ActiveInkWidth() / 2, + _width: B.width + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, + _height: B.height + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, + } + ); if (Doc.ActiveTool === InkTool.Write) { this.unprocessedDocs.push(inkDoc); CollectionFreeFormView.collectionsWithUnprocessedInk.add(this); @@ -685,23 +690,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action onEraserUp = (e: PointerEvent): void => { - if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - document.removeEventListener('pointermove', this.onEraserMove); - document.removeEventListener('pointerup', this.onEraserUp); - this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); - this._deleteList = []; - this._batch?.end(); - } - }; - - @action - onPointerUp = (e: PointerEvent): void => { - if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); - this.removeMoveListeners(); - this.removeEndListeners(); - } + this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); + this._deleteList = []; + this._batch?.end(); }; onClick = (e: React.MouseEvent) => { @@ -739,46 +730,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety. */ @action - onEraserMove = (e: PointerEvent) => { + onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { const currPoint = { X: e.clientX, Y: e.clientY }; - this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => { + this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => { if (!this._deleteList.includes(intersect.inkView)) { this._deleteList.push(intersect.inkView); SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || '1'); SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black'); // create a new curve by appending all curves of the current segment together in order to render a single new stroke. - !e.shiftKey && + if (!e.shiftKey) { this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment => GestureOverlay.Instance.dispatchGesture( GestureUtils.Gestures.Stroke, segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]) ) ); + } // Lower ink opacity to give the user a visual indicator of deletion. intersect.inkView.layoutDoc.opacity = 0.5; + intersect.inkView.layoutDoc.dontIntersect = true; } }); - this._lastX = currPoint.X; - this._lastY = currPoint.Y; - - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); + return false; }; @action - onPointerMove = (e: PointerEvent): void => { - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; + onPointerMove = (e: PointerEvent): boolean => { + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false; if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { Doc.ActiveTool = InkTool.None; if (this.props.isContentActive(true)) e.stopPropagation(); } else if (!e.cancelBubble) { if (this.tryDragCluster(e, this._hitCluster)) { - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); + return true; } else this.pan(e); - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); } + return false; }; /** @@ -788,6 +775,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => { const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) }; const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) }; + return this.childDocs .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)) .filter(inkView => inkView?.ComponentView instanceof InkingStroke) @@ -875,7 +863,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const tVals: number[] = []; // Iterating through all ink strokes in the current freeform collection. this.childDocs - .filter(doc => doc.type === DocumentType.INK) + .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect) .forEach(doc => { const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke; const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; @@ -906,7 +894,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); return; } // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected' @@ -954,13 +941,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY); - if (!this._pullDirection) { - // if we are not bezel movement - this.pan({ clientX: centerX, clientY: centerY }); - } else { - this._pullCoords = [centerX, centerY]; - } - this._lastX = centerX; this._lastY = centerY; } @@ -985,24 +965,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; this._lastX = centerX; this._lastY = centerY; - const screenBox = this._mainCont?.getBoundingClientRect(); - - // determine if we are using a bezel movement - if (screenBox) { - if (screenBox.right - centerX < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'right'; - } else if (centerX - screenBox.left < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'left'; - } else if (screenBox.bottom - centerY < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'bottom'; - } else if (centerY - screenBox.top < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'top'; - } - } this.removeMoveListeners(); this.addMoveListeners(); @@ -1014,19 +976,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; cleanUpInteractions = () => { - switch (this._pullDirection) { - case 'left': - case 'right': - case 'top': - case 'bottom': - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: 'New Collection' }), this._pullDirection); - } - - this._pullDirection = ''; - this._pullCoords = [0, 0]; - - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); this.removeMoveListeners(); this.removeEndListeners(); }; @@ -1055,7 +1004,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.layoutDoc._Transform || (this.layoutDoc._fitWidth && this.layoutDoc.nativeHeight) || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; + if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); @@ -1068,17 +1017,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { - // set the current respective FFview to the tab being panned. - Doc.UserDoc()?.presentationMode === 'recording' && RecordingApi.Instance.setRecordingFFView(this); - // TODO: make this based off the specific recording FFView - Doc.UserDoc()?.presentationMode === 'none' && RecordingApi.Instance.setPlayFFView(this); - - // TODO: zzz + michael to figure out this merge in case of strange behaviour - // if (Doc.UserDoc()?.presentationMode === 'watching') { - // RecordingApi.Instance.pauseVideoAndMovements(); - // Doc.UserDoc().presentationMode = 'none'; - // // RecordingApi.Instance.pauseMovements() - // } // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code. if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction(); @@ -1202,12 +1140,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined); if (!cantTransform) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection - newState.initializers![this.Document[Id]] = { panX: panX, panY: panY }; + newState.initializers![this.Document[Id]] = { panX, panY }; HistoryUtil.pushState(newState); } // focus on the document in the collection const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); - const focusSpeed = options?.instant ? 0 : didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 500) : 0; + const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0; // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (didMove) { scale && (this.Document[this.scaleFieldKey] = scale); @@ -1226,8 +1164,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document._panY = restoreState.panY; this.Document[this.scaleFieldKey] = restoreState.scale; } - runInAction(() => (this._viewTransition = 0)); } + runInAction(() => (this._viewTransition = 0)); return resetView; }; const xf = !cantTransform @@ -1252,17 +1190,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (scale) { const maxZoom = 5; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context - const newScale = Math.min(maxZoom, (1 / (this.contentScaling || 1)) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height))); + const newScale = Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height))); return { panX: this.props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2, panY: this.props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2, scale: newScale, }; } - const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1); - const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1); - const cx = NumCast(this.layoutDoc._panX); - const cy = NumCast(this.layoutDoc._panY); + + const panelWidth = this.props.isAnnotationOverlay ? this.nativeWidth : this.props.PanelWidth(); + const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight(); + const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1); + const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1); + const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0); + const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0); const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; if (screen.right - screen.left < bounds.right - bounds.left || screen.bot - screen.top < bounds.bot - bounds.top) { return { @@ -1272,8 +1213,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; } return { - panX: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), - panY: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), + panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), + panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), }; }; @@ -1302,7 +1243,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; pointerEvents = () => { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ? 'all' : this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.(); + const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); return pointerEvents; }; getChildDocView(entry: PoolData) { @@ -1374,22 +1315,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }); getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData { - const layoutDoc = Doc.Layout(params.pair.layout); - const { z, color, zIndex } = params.pair.layout; - const { x, y, opacity } = - this.Document._currentFrame === undefined - ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) } - : CollectionFreeFormDocumentView.getValues(params.pair.layout, NumCast(this.Document._currentFrame)); + const curFrame = Cast(this.Document._currentFrame, 'number'); + const childDoc = params.pair.layout; + const childDocLayout = Doc.Layout(childDoc); + const { z, zIndex } = childDoc; + const { backgroundColor, color } = curFrame === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame); + const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame); return { x: NumCast(x), y: NumCast(y), z: Cast(z, 'number'), - color: StrCast(color), + color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color), + backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor), + opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity), zIndex: Cast(zIndex, 'number'), - transition: StrCast(layoutDoc.dataTransition), - opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number', null), - width: Cast(layoutDoc._width, 'number'), - height: Cast(layoutDoc._height, 'number'), + width: Cast(childDocLayout._width, 'number'), + height: Cast(childDocLayout._height, 'number'), + transition: StrCast(childDocLayout.dataTransition), pair: params.pair, replica: '', }; @@ -1504,7 +1446,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (const entry of array) { const lastPos = this._cachedPool.get(entry[0]); // last computed pos const newPos = entry[1]; - if (!lastPos || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) { + if ( + !lastPos || + newPos.color !== lastPos.color || + newPos.backgroundColor !== lastPos.backgroundColor || + newPos.opacity !== lastPos.opacity || + newPos.x !== lastPos.x || + newPos.y !== lastPos.y || + newPos.z !== lastPos.z || + newPos.zIndex !== lastPos.zIndex || + newPos.transition !== lastPos.transition + ) { this._layoutPoolData.set(entry[0], newPos); } if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { @@ -1521,6 +1473,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection elements.push({ ele: this.getChildDocView(entry[1]), bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica), + inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1, }) ); @@ -1573,11 +1526,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection setTimeout( action(() => { this._firstRender = false; - this._disposers.layoutComputation = reaction( - () => this.doLayoutComputation, - elements => (this._layoutElements = elements || []), - { fireImmediately: true, name: 'doLayout' } - ); this._marqueeRef.current?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); @@ -1594,10 +1542,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; const pbounds = { - x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], - y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], - r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], - b: (cbounds.b - p[1]) * this.zoomScaling() + c[1], + x: cbounds.x - p[0] + c[0], + y: cbounds.y - p[1] + c[1], + r: cbounds.r - p[0] + c[0], + b: cbounds.b - p[1] + c[1], }; this.layoutDoc._width = pbounds.r - pbounds.x; this.layoutDoc._height = pbounds.b - pbounds.y; @@ -1609,6 +1557,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }, { fireImmediately: true } ); + + this._disposers.layoutComputation = reaction( + () => this.doLayoutComputation, + elements => (this._layoutElements = elements || []), + { fireImmediately: true, name: 'doLayout' } + ); }) ); } @@ -1632,9 +1586,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; - const parEle = newCan.parentElement as HTMLElement; - parEle.removeChild(newCan); - parEle.appendChild(img); + if (newCan) { + const parEle = newCan.parentElement as HTMLElement; + parEle.removeChild(newCan); + parEle.appendChild(img); + } } } } @@ -1888,7 +1844,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @computed get placeholder() { return ( <div className="collectionfreeformview-placeholder" style={{ background: StrCast(this.Document.backgroundColor) }}> - <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span> + <span className="collectionfreeformview-placeholderSpan">{this.props.Document.annotationOn ? '' : this.props.Document.title?.toString()}</span> </div> ); } @@ -1902,6 +1858,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined} nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge} addDocTab={this.addDocTab} + slowLoadDocuments={this.slowLoadDocuments} trySelectCluster={this.trySelectCluster} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} @@ -1910,7 +1867,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}> + <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}> {this.layoutDoc._backgroundGridShow ? ( <div> <CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!? @@ -1944,13 +1901,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } - @computed get contentScaling() { + @computed get nativeDimScaling() { if (this._firstRender || (this.props.isAnnotationOverlay && !this.props.annotationLayerHostsContent)) return 0; const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; - return wscale < hscale ? wscale : hscale; + return wscale < hscale || this.layoutDoc.fitWidth ? wscale : hscale; } private groupDropDisposer?: DragManager.DragDropDisposer; @@ -1984,22 +1941,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection : SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement()) ? 'all' : (this.props.pointerEvents?.() as any), - transform: `scale(${this.contentScaling || 1})`, - width: `${100 / (this.contentScaling || 1)}%`, - height: this.isAnnotationOverlay && this.Document.scrollHeight ? NumCast(this.Document.scrollHeight) : `${100 / (this.contentScaling || 1)}%`, // : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() + transform: `scale(${this.nativeDimScaling || 1})`, + width: `${100 / (this.nativeDimScaling || 1)}%`, + height: this.isAnnotationOverlay && this.Document.scrollHeight ? NumCast(this.Document.scrollHeight) : `${100 / (this.nativeDimScaling || 1)}%`, // : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> {this._firstRender ? this.placeholder : this.marqueeView} {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} - <div - className={'pullpane-indicator'} - style={{ - display: this._pullDirection ? 'block' : 'none', - top: clientRect ? (this._pullDirection === 'bottom' ? this._pullCoords[1] - clientRect.y : 0) : 'auto', - left: clientRect ? (this._pullDirection === 'right' ? this._pullCoords[0] - clientRect.x : 0) : 'auto', - width: clientRect ? (this._pullDirection === 'left' ? this._pullCoords[0] - clientRect.left : this._pullDirection === 'right' ? clientRect.right - this._pullCoords[0] : clientRect.width) : 0, - height: clientRect ? (this._pullDirection === 'top' ? this._pullCoords[1] - clientRect.top : this._pullDirection === 'bottom' ? clientRect.bottom - this._pullCoords[1] : clientRect.height) : 0, - }}></div> { // uncomment to show snap lines <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}> @@ -2269,3 +2217,6 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); }); +ScriptingGlobals.add(function pinWithView(readOnly: boolean) { + !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocView: true, panelWidth: view.props.PanelWidth(), panelHeight: view.props.PanelHeight() })); +}); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 41e4d6b6a..e0f5cbe5b 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -1,16 +1,14 @@ - .marqueeView { position: inherit; - top:0; - left:0; - width:100%; - height:100%; + top: 0; + left: 0; + width: 100%; + height: 100%; overflow: hidden; border-radius: inherit; user-select: none; } - .marqueeView:focus-within { overflow: hidden; } @@ -22,13 +20,13 @@ border-color: black; pointer-events: none; .marquee-legend { - bottom:-18px; - left:0; + bottom: -18px; + left: 0; position: absolute; font-size: 9; - white-space:nowrap; + white-space: nowrap; } .marquee-legend::after { - content: "Press <space> for lasso" + content: 'Press <space> for lasso'; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 4513ffb39..58a00bbac 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -40,6 +40,16 @@ interface MarqueeViewProps { nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; + slowLoadDocuments: ( + files: File[] | string, + options: DocumentOptions, + generatedDocuments: Doc[], + text: string, + completed: ((doc: Doc[]) => void) | undefined, + clientX: number, + clientY: number, + addDocument: (doc: Doc | Doc[]) => boolean + ) => Promise<void>; } export interface MarqueeViewBounds { @@ -277,8 +287,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const hideMarquee = () => { this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); - document.removeEventListener('pointerdown', hideMarquee); - document.removeEventListener('wheel', hideMarquee); + document.removeEventListener('pointerdown', hideMarquee, true); + document.removeEventListener('wheel', hideMarquee, true); }; if (PresBox.startMarquee) { this.pinWithView(); @@ -292,8 +302,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView; - document.addEventListener('pointerdown', hideMarquee); - document.addEventListener('wheel', hideMarquee); + document.addEventListener('pointerdown', hideMarquee, true); + document.addEventListener('wheel', hideMarquee, true); } else { this.hideMarquee(); } @@ -330,7 +340,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) { - PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments); } this.clearSelection(); } @@ -356,14 +366,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }; @action - showMarquee = () => { - this._visible = true; - }; - + showMarquee = () => (this._visible = true); @action - hideMarquee = () => { - this._visible = false; - }; + hideMarquee = () => (this._visible = false); @undoBatch @action diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 4e4c33446..9468c5f06 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -1,6 +1,6 @@ import { action, computed, Lambda, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import * as React from "react"; +import * as React from 'react'; import { Doc, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -15,8 +15,8 @@ import { ContextMenuProps } from '../../ContextMenuItem'; import { DocumentView } from '../../nodes/DocumentView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { CollectionSubView } from '../CollectionSubView'; -import "./CollectionGridView.scss"; -import Grid, { Layout } from "./Grid"; +import './CollectionGridView.scss'; +import Grid, { Layout } from './Grid'; @observer export class CollectionGridView extends CollectionSubView() { @@ -29,50 +29,76 @@ export class CollectionGridView extends CollectionSubView() { onChildClickHandler = () => ScriptCast(this.Document.onChildClick); - @computed get numCols() { return NumCast(this.props.Document.gridNumCols, 10); } - @computed get rowHeight() { return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; } - // sets the default width and height of the grid nodes - @computed get defaultW() { return NumCast(this.props.Document.gridDefaultW, 2); } - @computed get defaultH() { return NumCast(this.props.Document.gridDefaultH, 2); } + @computed get numCols() { + return NumCast(this.props.Document.gridNumCols, 10); + } + @computed get rowHeight() { + return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; + } + // sets the default width and height of the grid nodes + @computed get defaultW() { + return NumCast(this.props.Document.gridDefaultW, 2); + } + @computed get defaultH() { + return NumCast(this.props.Document.gridDefaultH, 2); + } - @computed get colWidthPlusGap() { return (this.props.PanelWidth() - this.margin) / this.numCols; } - @computed get rowHeightPlusGap() { return this.rowHeight + this.margin; } + @computed get colWidthPlusGap() { + return (this.props.PanelWidth() - this.margin) / this.numCols; + } + @computed get rowHeightPlusGap() { + return this.rowHeight + this.margin; + } - @computed get margin() { return NumCast(this.props.Document.margin, 10); } // sets the margin between grid nodes + @computed get margin() { + return NumCast(this.props.Document.margin, 10); + } // sets the margin between grid nodes - @computed get flexGrid() { return BoolCast(this.props.Document.gridFlex, true); } // is grid static/flexible i.e. whether nodes be moved around and resized - @computed get compaction() { return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, "vertical")); } // is grid static/flexible i.e. whether nodes be moved around and resized + @computed get flexGrid() { + return BoolCast(this.props.Document.gridFlex, true); + } // is grid static/flexible i.e. whether nodes be moved around and resized + @computed get compaction() { + return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, 'vertical')); + } // is grid static/flexible i.e. whether nodes be moved around and resized /** * Sets up the listeners for the list of documents and the reset button. */ componentDidMount() { - this._changeListenerDisposer = reaction(() => this.childLayoutPairs, (pairs) => { - const newLayouts: Layout[] = []; - const oldLayouts = this.savedLayoutList; - pairs.forEach((pair, i) => { - const existing = oldLayouts.find(l => l.i === pair.layout[Id]); - if (existing) newLayouts.push(existing); - else { - if (Object.keys(this.dropLocation).length) { // external drop - this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.dropLocation as { x: number, y: number }, !this.flexGrid)); - this.dropLocation = {}; - } - else { // internal drop - this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + this._changeListenerDisposer = reaction( + () => this.childLayoutPairs, + pairs => { + const newLayouts: Layout[] = []; + const oldLayouts = this.savedLayoutList; + pairs.forEach((pair, i) => { + const existing = oldLayouts.find(l => l.i === pair.layout[Id]); + if (existing) newLayouts.push(existing); + else { + if (Object.keys(this.dropLocation).length) { + // external drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.dropLocation as { x: number; y: number }, !this.flexGrid)); + this.dropLocation = {}; + } else { + // internal drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + } } - } - }); - pairs?.length && this.setLayoutList(newLayouts); - }, { fireImmediately: true }); + }); + pairs?.length && this.setLayoutList(newLayouts); + }, + { fireImmediately: true } + ); // updates the layouts if the reset button has been clicked - this._resetListenerDisposer = reaction(() => this.props.Document.gridResetLayout, (reset) => { - if (reset && this.flexGrid) { - this.setLayout(this.childLayoutPairs.map((pair, index) => this.makeLayoutItem(pair.layout, this.unflexedPosition(index)))); + this._resetListenerDisposer = reaction( + () => this.props.Document.gridResetLayout, + reset => { + if (reset && this.flexGrid) { + this.setLayout(this.childLayoutPairs.map((pair, index) => this.makeLayoutItem(pair.layout, this.unflexedPosition(index)))); + } + this.props.Document.gridResetLayout = false; } - this.props.Document.gridResetLayout = false; - }); + ); } /** @@ -85,15 +111,15 @@ export class CollectionGridView extends CollectionSubView() { /** * @returns the default location of the grid node (i.e. when the grid is static) - * @param index + * @param index */ - unflexedPosition(index: number): Omit<Layout, "i"> { + unflexedPosition(index: number): Omit<Layout, 'i'> { return { x: (index % (Math.floor(this.numCols / this.defaultW) || 1)) * this.defaultW, y: Math.floor(index / (Math.floor(this.numCols / this.defaultH) || 1)) * this.defaultH, w: this.defaultW, h: this.defaultH, - static: true + static: true, }; } @@ -110,9 +136,9 @@ export class CollectionGridView extends CollectionSubView() { /** * Creates a layout object for a grid item */ - makeLayoutItem = (doc: Doc, pos: { x: number, y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => { - return ({ i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static }); - } + makeLayoutItem = (doc: Doc, pos: { x: number; y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => { + return { i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static }; + }; /** * Adds a layout to the list of layouts. @@ -122,16 +148,16 @@ export class CollectionGridView extends CollectionSubView() { f !== -1 && layouts.splice(f, 1); layouts.push(layout); return layouts; - } + }; /** - * @returns the transform that will correctly place the document decorations box. + * @returns the transform that will correctly place the document decorations box. */ private lookupIndividualTransform = (layout: Layout) => { const xypos = this.flexGrid ? layout : this.unflexedPosition(this.renderedLayoutList.findIndex(l => l.i === layout.i)); const pos = { x: xypos.x * this.colWidthPlusGap + this.margin, y: xypos.y * this.rowHeightPlusGap + this.margin - this._scroll }; return this.props.ScreenToLocalTransform().translate(-pos.x, -pos.y); - } + }; /** * @returns the layout list converted from JSON @@ -147,26 +173,32 @@ export class CollectionGridView extends CollectionSubView() { this.props.Document.gridLayoutString = JSON.stringify(layouts); } + isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + isChildContentActive = () => (this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : undefined); /** - * - * @param layout + * + * @param layout * @param dxf the x- and y-translations of the decorations box as a transform i.e. this.lookupIndividualTransform - * @param width - * @param height + * @param width + * @param height * @returns the `ContentFittingDocumentView` of the node */ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { - return <DocumentView - {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} - PanelWidth={width} - PanelHeight={height} - ScreenToLocalTransform={dxf} - onClick={this.onChildClickHandler} - renderDepth={this.props.renderDepth + 1} - dontCenter={this.props.Document.centerY ? "" : "y"} - />; + return ( + <DocumentView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + Document={layout} + DataDoc={layout.resolvedDataDoc as Doc} + isContentActive={this.isChildContentActive} + PanelWidth={width} + PanelHeight={height} + ScreenToLocalTransform={dxf} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + onClick={this.onChildClickHandler} + renderDepth={this.props.renderDepth + 1} + dontCenter={this.props.Document.centerY ? '' : 'y'} + /> + ); } /** @@ -176,7 +208,7 @@ export class CollectionGridView extends CollectionSubView() { @action setLayout = (layoutArray: Layout[]) => { // for every child in the collection, check to see if there's a corresponding grid layout object and - // updated layout object. If both exist, which they should, update the grid layout object from the updated object + // updated layout object. If both exist, which they should, update the grid layout object from the updated object if (this.flexGrid) { const savedLayouts = this.savedLayoutList; this.childLayoutPairs.forEach(({ layout: doc }) => { @@ -194,7 +226,7 @@ export class CollectionGridView extends CollectionSubView() { undoBatch(() => this.setLayoutList(savedLayouts))(); } } - } + }; /** * @returns a list of `ContentFittingDocumentView`s inside wrapper divs. @@ -209,11 +241,12 @@ export class CollectionGridView extends CollectionSubView() { const dxf = () => this.lookupIndividualTransform(l); const width = () => (this.flexGrid ? l.w : this.defaultW) * this.colWidthPlusGap - this.margin; const height = () => (this.flexGrid ? l.h : this.defaultH) * this.rowHeightPlusGap - this.margin; - child && collector.push( - <div key={child.layout[Id]} className={"document-wrapper" + (this.flexGrid && this.props.isSelected() ? "" : " static")} > - {this.getDisplayDoc(child.layout, dxf, width, height)} - </div > - ); + child && + collector.push( + <div key={child.layout[Id]} className={'document-wrapper' + (this.flexGrid && this.props.isSelected() ? '' : ' static')}> + {this.getDisplayDoc(child.layout, dxf, width, height)} + </div> + ); }); } return collector; @@ -223,14 +256,19 @@ export class CollectionGridView extends CollectionSubView() { * @returns a list of `Layout` objects with attributes depending on whether the grid is flexible or static */ @computed get renderedLayoutList(): Layout[] { - return this.flexGrid ? - this.savedLayoutList.map(({ i, x, y, w, h }) => ({ - i, y, h, - x: x + w > this.numCols ? 0 : x, // handles wrapping around of nodes when numCols decreases - w: Math.min(w, this.numCols), // reduces width if greater than numCols - static: BoolCast(this.childLayoutPairs.find(({ layout }) => layout[Id] === i)?.layout._lockedPosition, false) // checks if the lock position item has been selected in the context menu - })) : - this.savedLayoutList.map((layout, index) => { Object.assign(layout, this.unflexedPosition(index)); return layout; }); + return this.flexGrid + ? this.savedLayoutList.map(({ i, x, y, w, h }) => ({ + i, + y, + h, + x: x + w > this.numCols ? 0 : x, // handles wrapping around of nodes when numCols decreases + w: Math.min(w, this.numCols), // reduces width if greater than numCols + static: BoolCast(this.childLayoutPairs.find(({ layout }) => layout[Id] === i)?.layout._lockedPosition, false), // checks if the lock position item has been selected in the context menu + })) + : this.savedLayoutList.map((layout, index) => { + Object.assign(layout, this.unflexedPosition(index)); + return layout; + }); } /** @@ -246,7 +284,7 @@ export class CollectionGridView extends CollectionSubView() { return true; } return false; - } + }; /** * Handles external drop of images/PDFs etc from outside Dash. @@ -255,7 +293,7 @@ export class CollectionGridView extends CollectionSubView() { onExternalDrop = async (e: React.DragEvent): Promise<void> => { this.dropLocation = this.screenToCell(e.clientX, e.clientY); super.onExternalDrop(e, {}); - } + }; /** * Handles the change in the value of the rowHeight slider. @@ -263,65 +301,83 @@ export class CollectionGridView extends CollectionSubView() { @action onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => { this._rowHeight = event.currentTarget.valueAsNumber; - } + }; /** * Handles the user clicking on the slider. */ @action onSliderDown = (e: React.PointerEvent) => { this._rowHeight = this.rowHeight; // uses _rowHeight during dragging and sets doc's rowHeight when finished so that operation is undoable - setupMoveUpEvents(this, e, returnFalse, action(() => { - undoBatch(() => this.props.Document.gridRowHeight = this._rowHeight)(); - this._rowHeight = undefined; - }), emptyFunction, false, false); + setupMoveUpEvents( + this, + e, + returnFalse, + action(() => { + undoBatch(() => (this.props.Document.gridRowHeight = this._rowHeight))(); + this._rowHeight = undefined; + }), + emptyFunction, + false, + false + ); e.stopPropagation(); - } + }; /** * Adds the display option to change the css display attribute of the `ContentFittingDocumentView`s */ onContextMenu = () => { const displayOptionsMenu: ContextMenuProps[] = []; - displayOptionsMenu.push({ description: "Toggle Content Display Style", event: () => this.props.Document.display = this.props.Document.display ? undefined : "contents", icon: "copy" }); - displayOptionsMenu.push({ description: "Toggle Vertical Centering", event: () => this.props.Document.centerY = !this.props.Document.centerY, icon: "copy" }); - ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" }); - } + displayOptionsMenu.push({ description: 'Toggle Content Display Style', event: () => (this.props.Document.display = this.props.Document.display ? undefined : 'contents'), icon: 'copy' }); + displayOptionsMenu.push({ description: 'Toggle Vertical Centering', event: () => (this.props.Document.centerY = !this.props.Document.centerY), icon: 'copy' }); + ContextMenu.Instance.addItem({ description: 'Display', subitems: displayOptionsMenu, icon: 'tv' }); + }; /** * Handles text document creation on double click. */ onPointerDown = (e: React.PointerEvent) => { if (this.props.isContentActive(true)) { - setupMoveUpEvents(this, e, returnFalse, returnFalse, + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, (e: PointerEvent, doubleTap?: boolean) => { if (doubleTap && !e.button) { - undoBatch(action(() => { - const text = Docs.Create.TextDocument("", { _width: 150, _height: 50 }); - FormattedTextBox.SelectOnLoad = text[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed - Doc.AddDocToList(this.props.Document, this.props.fieldKey, text); - this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(e.clientX, e.clientY)))); - }))(); + undoBatch( + action(() => { + const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 }); + FormattedTextBox.SelectOnLoad = text[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + Doc.AddDocToList(this.props.Document, this.props.fieldKey, text); + this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(e.clientX, e.clientY)))); + }) + )(); } }, - false); + false + ); if (this.props.isSelected(true)) e.stopPropagation(); } - } + }; render() { return ( - <div className="collectionGridView-contents" ref={this.createDashEventsTarget} - style={{ pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined }} + <div + className="collectionGridView-contents" + ref={this.createDashEventsTarget} + style={{ pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined }} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} - onDrop={this.onExternalDrop} - > - <div className="collectionGridView-gridContainer" ref={this._containerRef} - style={{ backgroundColor: StrCast(this.layoutDoc._backgroundColor, "white") }} + onDrop={this.onExternalDrop}> + <div + className="collectionGridView-gridContainer" + ref={this._containerRef} + style={{ backgroundColor: StrCast(this.layoutDoc._backgroundColor, 'white') }} onWheel={e => e.stopPropagation()} onScroll={action(e => { if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll; else this._scroll = e.currentTarget.scrollTop; - })} > + })}> <Grid width={this.props.PanelWidth()} nodeList={this.contents.length ? this.contents : null} @@ -332,15 +388,21 @@ export class CollectionGridView extends CollectionSubView() { setLayout={this.setLayout} transformScale={this.props.ScreenToLocalTransform().Scale} compactType={this.compaction} // determines whether nodes should remain in position, be bound to the top, or to the left - preventCollision={BoolCast(this.props.Document.gridPreventCollision)}// determines whether nodes should move out of the way (i.e. collide) when other nodes are dragged over them + preventCollision={BoolCast(this.props.Document.gridPreventCollision)} // determines whether nodes should move out of the way (i.e. collide) when other nodes are dragged over them margin={this.margin} /> - <input className="rowHeightSlider" type="range" + <input + className="rowHeightSlider" + type="range" style={{ width: this.props.PanelHeight() - 30 }} - min={1} value={this.rowHeight} max={this.props.PanelHeight() - 30} - onPointerDown={this.onSliderDown} onChange={this.onSliderChange} /> + min={1} + value={this.rowHeight} + max={this.props.PanelHeight() - 30} + onPointerDown={this.onSliderDown} + onChange={this.onSliderChange} + /> </div> - </div > + </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 777ef464f..465dbfe6d 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,6 +1,6 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; -import * as React from "react"; +import * as React from 'react'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -11,11 +11,10 @@ import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; import { CollectionSubView } from '../CollectionSubView'; -import "./CollectionMulticolumnView.scss"; +import './CollectionMulticolumnView.scss'; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; - interface WidthSpecifier { magnitude: number; unit: string; @@ -27,8 +26,8 @@ interface LayoutData { } export const DimUnit = { - Pixel: "px", - Ratio: "*" + Pixel: 'px', + Ratio: '*', }; const resolvedUnits = Object.values(DimUnit); @@ -36,14 +35,13 @@ const resizerWidth = 8; @observer export class CollectionMulticolumnView extends CollectionSubView() { - /** * @returns the list of layout documents whose width unit is * *, denoting that it will be displayed with a ratio, not fixed pixel, value */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, '*') === DimUnit.Ratio); } @computed @@ -65,10 +63,10 @@ export class CollectionMulticolumnView extends CollectionSubView() { let starSum = 0; const widthSpecifiers: WidthSpecifier[] = []; this.childLayoutPairs.map(pair => { - const unit = StrCast(pair.layout._dimUnit, "*"); + const unit = StrCast(pair.layout._dimUnit, '*'); const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { - (unit === DimUnit.Ratio) && (starSum += magnitude); + unit === DimUnit.Ratio && (starSum += magnitude); widthSpecifiers.push({ magnitude, unit }); } /** @@ -100,14 +98,13 @@ export class CollectionMulticolumnView extends CollectionSubView() { * This returns the total quantity, in pixels, that this * view needs to reserve for child documents that have * (with higher priority) requested a fixed pixel width. - * + * * If the underlying resolvedLayoutInformation returns null * because we're waiting on promises to resolve, this value will be undefined as well. */ @computed private get totalFixedAllocation(): number | undefined { - return this.resolvedLayoutInformation?.widthSpecifiers.reduce( - (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); + return this.resolvedLayoutInformation?.widthSpecifiers.reduce((sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); } /** @@ -115,7 +112,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { * view needs to reserve for child documents that have * (with lower priority) requested a certain relative proportion of the * remaining pixel width not allocated for fixed widths. - * + * * If the underlying totalFixedAllocation returns undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -135,7 +132,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { * this accessor returns 1000 / (2 + 2 + 1), or 200px. * Elsewhere, this is then multiplied by each relative-width * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). - * + * * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -165,17 +162,17 @@ export class CollectionMulticolumnView extends CollectionSubView() { return 0; // we're still waiting on promises to resolve } let width = NumCast(layout._dimMagnitude, this.minimumDim); - if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { + if (StrCast(layout._dimUnit, '*') === DimUnit.Ratio) { width *= columnUnitLength; } return width; - } + }; /** * @returns the transform that will correctly place * the document decorations box, shifted to the right by * the sum of all the resolved column widths of the - * documents before the target. + * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { const columnUnitLength = this.columnUnitLength; @@ -185,12 +182,12 @@ export class CollectionMulticolumnView extends CollectionSubView() { let offset = 0; for (const { layout: candidate } of this.childLayoutPairs) { if (candidate === layout) { - return this.props.ScreenToLocalTransform().translate(-offset / (this.props.scaling?.() || 1), 0); + return this.props.ScreenToLocalTransform().translate(-offset / (this.props.NativeDimScaling?.() || 1), 0); } offset += this.lookupPixels(candidate) + resizerWidth; } return Transform.Identity(); // type coersion, this case should never be hit - } + }; @undoBatch @action @@ -198,16 +195,17 @@ export class CollectionMulticolumnView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - })); + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + curInd = this.childDocs.indexOf(d); + }) + ); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.x < de.x && brect.x + brect.width > de.x) { if (curInd !== -1 && curInd === Math.floor(index / 2)) { dropInd = curInd; - } - else if (child.className === "multiColumnResizer") { + } else if (child.className === 'multiColumnResizer') { dropInd = Math.floor(index / 2); } else { dropInd = Math.ceil(index / 2 + (de.x - brect.x > brect.width / 2 ? 0 : -1)); @@ -215,76 +213,80 @@ export class CollectionMulticolumnView extends CollectionSubView() { } }); if (super.onInternalDrop(e, de)) { - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - d._dimUnit = "*"; - d._dimMagnitude = 1; - if (dropInd !== curInd || dropInd === -1) { - if (this.childDocs.includes(d)) { - if (dropInd > this.childDocs.indexOf(d)) dropInd--; + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + d._dimUnit = '*'; + d._dimMagnitude = 1; + if (dropInd !== curInd || dropInd === -1) { + if (this.childDocs.includes(d)) { + if (dropInd > this.childDocs.indexOf(d)) dropInd--; + } + Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); + Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); } - Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); - Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); - } - })); + }) + ); } } return false; - } - + }; onChildClickHandler = () => ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { - if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); return true; } return this.props.addDocTab(doc, where); - } + }; isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; + isChildContentActive = () => + ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { - return <DocumentView - Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} - styleProvider={this.props.styleProvider} - docViewPath={this.props.docViewPath} - LayoutTemplate={this.props.childLayoutTemplate} - LayoutTemplateString={this.props.childLayoutString} - renderDepth={this.props.renderDepth + 1} - PanelWidth={width} - PanelHeight={height} - rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} - onClick={this.onChildClickHandler} - onDoubleClick={this.onChildDoubleClickHandler} - suppressSetHeight={true} - ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} - isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} - hideResizeHandles={this.props.childHideResizeHandles?.()} - hideDecorationTitle={this.props.childHideDecorationTitle?.()} - fitContentsToBox={this.props.fitContentsToBox} - focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - ContainingCollectionDoc={this.props.CollectionView?.props.Document} - ContainingCollectionView={this.props.CollectionView} - dontRegisterView={this.props.dontRegisterView} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - />; - } + return ( + <DocumentView + Document={layout} + DataDoc={layout.resolvedDataDoc as Doc} + styleProvider={this.props.styleProvider} + docViewPath={this.props.docViewPath} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={this.props.childLayoutString} + renderDepth={this.props.renderDepth + 1} + PanelWidth={width} + PanelHeight={height} + rootSelected={this.rootSelected} + dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + suppressSetHeight={true} + ScreenToLocalTransform={dxf} + isContentActive={this.isChildContentActive} + isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} + hideResizeHandles={this.props.childHideResizeHandles?.()} + hideDecorationTitle={this.props.childHideDecorationTitle?.()} + fitContentsToBox={this.props.fitContentsToBox} + focus={this.props.focus} + docFilters={this.childDocFilters} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + dontRegisterView={this.props.dontRegisterView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + /> + ); + }; /** * @returns the resolved list of rendered child documents, displayed - * at their resolved pixel widths, each separated by a resizer. + * at their resolved pixel widths, each separated by a resizer. */ @computed private get contents(): JSX.Element[] | null { @@ -293,22 +295,20 @@ export class CollectionMulticolumnView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; - const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)).scale((this.props.scaling?.() || 1)); + const dxf = () => + this.lookupIndividualTransform(layout) + .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .scale(this.props.NativeDimScaling?.() || 1); const width = () => this.lookupPixels(layout); const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( - <div className={"document-wrapper"} - key={"wrapper" + i} - style={{ width: width() }} > + <div className={'document-wrapper'} key={'wrapper' + i} style={{ width: width() }}> {this.getDisplayDoc(layout, dxf, width, height)} - <WidthLabel - layout={layout} - collectionDoc={Document} - /> + <WidthLabel layout={layout} collectionDoc={Document} /> </div>, <ResizeBar width={resizerWidth} - key={"resizer" + i} + key={'resizer' + i} styleProvider={this.props.styleProvider} isContentActive={this.props.isContentActive} select={this.props.select} @@ -324,16 +324,19 @@ export class CollectionMulticolumnView extends CollectionSubView() { render(): JSX.Element { return ( - <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget} + <div + className={'collectionMulticolumnView_contents'} + ref={this.createDashEventsTarget} style={{ width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`, height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`, - marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), - marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) - }} > + marginLeft: NumCast(this.props.Document._xMargin), + marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), + marginBottom: NumCast(this.props.Document._yMargin), + }}> {this.contents} </div> ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 08385bcb5..f8de4e5de 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -1,6 +1,6 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; -import * as React from "react"; +import * as React from 'react'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -11,7 +11,7 @@ import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; import { CollectionSubView } from '../CollectionSubView'; -import "./CollectionMultirowView.scss"; +import './CollectionMultirowView.scss'; import HeightLabel from './MultirowHeightLabel'; import ResizeBar from './MultirowResizer'; @@ -26,8 +26,8 @@ interface LayoutData { } export const DimUnit = { - Pixel: "px", - Ratio: "*" + Pixel: 'px', + Ratio: '*', }; const resolvedUnits = Object.values(DimUnit); @@ -35,14 +35,13 @@ const resizerHeight = 8; @observer export class CollectionMultirowView extends CollectionSubView() { - /** * @returns the list of layout documents whose width unit is * *, denoting that it will be displayed with a ratio, not fixed pixel, value */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, '*') === DimUnit.Ratio); } @computed @@ -64,10 +63,10 @@ export class CollectionMultirowView extends CollectionSubView() { let starSum = 0; const heightSpecifiers: HeightSpecifier[] = []; this.childLayoutPairs.map(pair => { - const unit = StrCast(pair.layout._dimUnit, "*"); + const unit = StrCast(pair.layout._dimUnit, '*'); const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { - (unit === DimUnit.Ratio) && (starSum += magnitude); + unit === DimUnit.Ratio && (starSum += magnitude); heightSpecifiers.push({ magnitude, unit }); } /** @@ -99,14 +98,13 @@ export class CollectionMultirowView extends CollectionSubView() { * This returns the total quantity, in pixels, that this * view needs to reserve for child documents that have * (with higher priority) requested a fixed pixel width. - * + * * If the underlying resolvedLayoutInformation returns null * because we're waiting on promises to resolve, this value will be undefined as well. */ @computed private get totalFixedAllocation(): number | undefined { - return this.resolvedLayoutInformation?.heightSpecifiers.reduce( - (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); + return this.resolvedLayoutInformation?.heightSpecifiers.reduce((sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); } /** @@ -114,7 +112,7 @@ export class CollectionMultirowView extends CollectionSubView() { * view needs to reserve for child documents that have * (with lower priority) requested a certain relative proportion of the * remaining pixel width not allocated for fixed widths. - * + * * If the underlying totalFixedAllocation returns undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -134,7 +132,7 @@ export class CollectionMultirowView extends CollectionSubView() { * this accessor returns 1000 / (2 + 2 + 1), or 200px. * Elsewhere, this is then multiplied by each relative-width * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). - * + * * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -164,17 +162,17 @@ export class CollectionMultirowView extends CollectionSubView() { return 0; // we're still waiting on promises to resolve } let height = NumCast(layout._dimMagnitude, this.minimumDim); - if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { + if (StrCast(layout._dimUnit, '*') === DimUnit.Ratio) { height *= rowUnitLength; } return height; - } + }; /** * @returns the transform that will correctly place * the document decorations box, shifted to the right by * the sum of all the resolved row widths of the - * documents before the target. + * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { const rowUnitLength = this.rowUnitLength; @@ -184,13 +182,12 @@ export class CollectionMultirowView extends CollectionSubView() { let offset = 0; for (const { layout: candidate } of this.childLayoutPairs) { if (candidate === layout) { - return this.props.ScreenToLocalTransform().translate(0, -offset / (this.props.scaling?.() || 1)); + return this.props.ScreenToLocalTransform().translate(0, -offset / (this.props.NativeDimScaling?.() || 1)); } offset += this.lookupPixels(candidate) + resizerHeight; } return Transform.Identity(); // type coersion, this case should never be hit - } - + }; @undoBatch @action @@ -198,16 +195,17 @@ export class CollectionMultirowView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - })); + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + curInd = this.childDocs.indexOf(d); + }) + ); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.y < de.y && brect.y + brect.height > de.y) { if (curInd !== -1 && curInd === Math.floor(index / 2)) { dropInd = curInd; - } - else if (child.className === "multiColumnResizer") { + } else if (child.className === 'multiColumnResizer') { dropInd = Math.floor(index / 2); } else { dropInd = Math.ceil(index / 2 + (de.y - brect.y > brect.height / 2 ? 0 : -1)); @@ -215,75 +213,79 @@ export class CollectionMultirowView extends CollectionSubView() { } }); if (super.onInternalDrop(e, de)) { - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - d._dimUnit = "*"; - d._dimMagnitude = 1; - if (dropInd !== curInd || dropInd === -1) { - if (this.childDocs.includes(d)) { - if (dropInd > this.childDocs.indexOf(d)) dropInd--; + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + d._dimUnit = '*'; + d._dimMagnitude = 1; + if (dropInd !== curInd || dropInd === -1) { + if (this.childDocs.includes(d)) { + if (dropInd > this.childDocs.indexOf(d)) dropInd--; + } + Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); + Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); } - Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); - Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); - } - })); + }) + ); } } return false; - } - + }; onChildClickHandler = () => ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { - if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); return true; } return this.props.addDocTab(doc, where); - } + }; isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; + isChildContentActive = () => + ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { - return <DocumentView - Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} - styleProvider={this.props.styleProvider} - docViewPath={this.props.docViewPath} - LayoutTemplate={this.props.childLayoutTemplate} - LayoutTemplateString={this.props.childLayoutString} - renderDepth={this.props.renderDepth + 1} - PanelWidth={width} - PanelHeight={height} - rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} - onClick={this.onChildClickHandler} - onDoubleClick={this.onChildDoubleClickHandler} - ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} - isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} - hideResizeHandles={this.props.childHideResizeHandles?.()} - hideDecorationTitle={this.props.childHideDecorationTitle?.()} - fitContentsToBox={this.props.fitContentsToBox} - focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - ContainingCollectionDoc={this.props.CollectionView?.props.Document} - ContainingCollectionView={this.props.CollectionView} - dontRegisterView={this.props.dontRegisterView} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - />; - } + return ( + <DocumentView + Document={layout} + DataDoc={layout.resolvedDataDoc as Doc} + styleProvider={this.props.styleProvider} + docViewPath={this.props.docViewPath} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={this.props.childLayoutString} + renderDepth={this.props.renderDepth + 1} + PanelWidth={width} + PanelHeight={height} + rootSelected={this.rootSelected} + dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + ScreenToLocalTransform={dxf} + isContentActive={this.isChildContentActive} + isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} + hideResizeHandles={this.props.childHideResizeHandles?.()} + hideDecorationTitle={this.props.childHideDecorationTitle?.()} + fitContentsToBox={this.props.fitContentsToBox} + focus={this.props.focus} + docFilters={this.childDocFilters} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + dontRegisterView={this.props.dontRegisterView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + /> + ); + }; /** * @returns the resolved list of rendered child documents, displayed - * at their resolved pixel widths, each separated by a resizer. + * at their resolved pixel widths, each separated by a resizer. */ @computed private get contents(): JSX.Element[] | null { @@ -292,13 +294,14 @@ export class CollectionMultirowView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; - const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)).scale((this.props.scaling?.() || 1)); + const dxf = () => + this.lookupIndividualTransform(layout) + .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .scale(this.props.NativeDimScaling?.() || 1); const height = () => this.lookupPixels(layout); const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( - <div className={"document-wrapper"} - style={{ height: height() }} - key={"wrapper" + i} > + <div className={'document-wrapper'} style={{ height: height() }} key={'wrapper' + i}> {this.getDisplayDoc(layout, dxf, width, height)} <HeightLabel layout={layout} collectionDoc={Document} /> </div>, @@ -306,7 +309,7 @@ export class CollectionMultirowView extends CollectionSubView() { height={resizerHeight} styleProvider={this.props.styleProvider} isContentActive={this.props.isContentActive} - key={"resizer" + i} + key={'resizer' + i} columnUnitLength={this.getRowUnitLength} toTop={layout} toBottom={childLayoutPairs[i + 1]?.layout} @@ -319,16 +322,19 @@ export class CollectionMultirowView extends CollectionSubView() { render(): JSX.Element { return ( - <div className={"collectionMultirowView_contents"} + <div + className={'collectionMultirowView_contents'} style={{ width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`, height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`, - marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), - marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) - }} ref={this.createDashEventsTarget}> + marginLeft: NumCast(this.props.Document._xMargin), + marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), + marginBottom: NumCast(this.props.Document._yMargin), + }} + ref={this.createDashEventsTarget}> {this.contents} </div> ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index a14634bdc..c5968d9a7 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -1,4 +1,4 @@ -@import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); // colors $white: #ffffff; $off-white: #fdfdfd; @@ -16,8 +16,10 @@ $yellow: #f5d747; $close-red: #e48282; -$drop-shadow: "#32323215"; +$drop-shadow: '#32323215'; +$INK_MASK_SIZE: 50000; +$INK_MASK_SIZE_HALF: -25000px; //padding $minimum-padding: 4px; @@ -28,8 +30,7 @@ $large-padding: 32px; $icon-size: 28px; // fonts -$sans-serif: "Roboto", -sans-serif; +$sans-serif: 'Roboto', sans-serif; $large-header: 16px; $body-text: 12px; $small-text: 9px; @@ -54,7 +55,6 @@ $standard-border-radius: 3px; // shadow $standard-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); - $dashboardselector-height: 32px; $mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc $docDecorations-zindex: 998; // then doc decorations appear over everything else @@ -79,4 +79,5 @@ $TREE_BULLET_WIDTH: 20px; DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM; LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH; TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; -}
\ No newline at end of file + INK_MASK_SIZE: $INK_MASK_SIZE; +} diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts index 59c2b3585..76259113c 100644 --- a/src/client/views/global/globalCssVariables.scss.d.ts +++ b/src/client/views/global/globalCssVariables.scss.d.ts @@ -1,6 +1,5 @@ - interface IGlobalScss { - contextMenuZindex: string; // context menu shows up over everything + contextMenuZindex: string; // context menu shows up over everything SCHEMA_DIVIDER_WIDTH: string; COLLECTION_BORDER_WIDTH: string; MINIMIZED_ICON_SIZE: string; @@ -11,7 +10,8 @@ interface IGlobalScss { DFLT_IMAGE_NATIVE_DIM: string; LEFT_MENU_WIDTH: string; TREE_BULLET_WIDTH: string; + INK_MASK_SIZE: number; } declare const globalCssVariables: IGlobalScss; -export = globalCssVariables;
\ No newline at end of file +export = globalCssVariables; diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss index 1d6496d3c..b0ee4e46d 100644 --- a/src/client/views/linking/LinkEditor.scss +++ b/src/client/views/linking/LinkEditor.scss @@ -1,10 +1,11 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .linkEditor { width: 100%; height: auto; font-size: 13px; // TODO user-select: none; + max-width: 280px; } .linkEditor-button-back { @@ -20,19 +21,20 @@ } .linkEditor-info { - //border-bottom: 0.5px solid $light-gray; - //padding-bottom: 1px; - padding: 12px; + padding-top: 12px; padding-left: 5px; + padding-bottom: 3px; //margin-bottom: 6px; display: flex; justify-content: space-between; color: black; .linkEditor-linkedTo { - width: calc(100% - 26px); - padding-left: 5px; - padding-right: 5px; + width: calc(100% - 46px); + overflow: hidden; + position: relative; + text-overflow: ellipsis; + white-space: pre; .linkEditor-downArrow { &:hover { @@ -77,11 +79,14 @@ width: 20px; } } +.linkEditor-deleteBtn { + padding-left: 3px; +} .linkEditor-description { padding-left: 26px; - padding-right: 6.5px; padding-bottom: 3.5px; + display: flex; .linkEditor-description-label { text-decoration-color: black; @@ -96,7 +101,6 @@ //border: 1px solid grey; //border-radius: 4px; padding-left: 2px; - padding-right: 2px; //margin-right: 4px; color: black; text-decoration-color: grey; @@ -104,17 +108,13 @@ .linkEditor-description-add-button { display: inline; - /* float: right; */ border-radius: 7px; font-size: 9px; background: black; - /* padding: 3px; */ - padding-top: 4px; - padding-left: 7px; - padding-bottom: 4px; - padding-right: 8px; height: 80%; color: white; + padding: 3px; + margin-left: 3px; &:hover { cursor: pointer; @@ -146,6 +146,7 @@ padding-left: 26px; padding-right: 6.5px; padding-bottom: 15px; + display: flex; &:hover { cursor: pointer; @@ -153,12 +154,11 @@ .linkEditor-followingDropdown-label { color: black; + padding-right: 3px; } .linkEditor-followingDropdown-dropdown { - .linkEditor-followingDropdown-header { - border: 1px solid grey; border-radius: 4px; //background-color: rgb(236, 236, 236); @@ -195,27 +195,25 @@ background-color: rgb(187, 220, 231); } } - } } - - } - - - - - .linkEditor-button, .linkEditor-addbutton { - width: 18px; - height: 18px; - padding: 0; - // font-size: 12px; - border-radius: 10px; - - + width: 15%; + border-radius: 7px; + font-size: 9px; + background: black; + padding: 3px; + height: 80%; + color: white; + text-align: center; + margin: auto; + margin-left: 3px; + > svg { + margin: auto; + } &:disabled { background-color: gray; } @@ -270,7 +268,6 @@ } } - .linkEditor-dropdown { width: 100%; position: relative; @@ -334,4 +331,4 @@ .linkEditor-button { margin-left: 3px; } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index ba301962b..1697062f4 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -3,32 +3,27 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, NumListCast, StrListCast, Field } from '../../../fields/Doc'; -import { DateCast, StrCast, Cast } from '../../../fields/Types'; +import { DateCast, StrCast, Cast, BoolCast } from '../../../fields/Types'; import { LinkManager } from '../../util/LinkManager'; import { undoBatch } from '../../util/UndoManager'; import './LinkEditor.scss'; import { LinkRelationshipSearch } from './LinkRelationshipSearch'; import React = require('react'); +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; interface LinkEditorProps { sourceDoc: Doc; linkDoc: Doc; - showLinks: () => void; + showLinks?: () => void; hideback?: boolean; } @observer export class LinkEditor extends React.Component<LinkEditorProps> { @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); - @observable zoomFollow = StrCast(this.props.sourceDoc.followLinkZoom); + @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom); + @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio); @observable openDropdown: boolean = false; - @observable showInfo: boolean = false; - @computed get infoIcon() { - if (this.showInfo) { - return 'chevron-up'; - } - return 'chevron-down'; - } @observable private buttonColor: string = ''; @observable private relationshipButtonColor: string = ''; @observable private relationshipSearchVisibility: string = 'none'; @@ -37,12 +32,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> { //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; @undoBatch - deleteLink = (): void => { - LinkManager.Instance.deleteLink(this.props.linkDoc); - this.props.showLinks(); - }; - - @undoBatch setRelationshipValue = action((value: string) => { if (LinkManager.currentLink) { const prevRelationship = LinkManager.currentLink.linkRelationship as string; @@ -159,8 +148,12 @@ export class LinkEditor extends React.Component<LinkEditorProps> { this.relationship = e.target.value; }; @action - handleZoomFollowChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.props.sourceDoc.followLinkZoom = e.target.checked; + handleZoomFollowChange = () => { + this.props.sourceDoc.followLinkZoom = !this.props.sourceDoc.followLinkZoom; + }; + @action + handleAudioFollowChange = () => { + this.props.sourceDoc.followLinkAudio = !this.props.sourceDoc.followLinkAudio; }; @action handleRelationshipSearchChange = (result: string) => { @@ -173,7 +166,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS return ( <div className="linkEditor-description"> - <div className="linkEditor-description-label">Link Relationship:</div> + <div className="linkEditor-description-label">Relationship:</div> <div className="linkEditor-description-input"> <div className="linkEditor-description-editing"> <input @@ -203,7 +196,23 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-zoomFollow-label">Zoom To Link Target:</div> <div className="linkEditor-zoomFollow-input"> <div className="linkEditor-zoomFollow-editing"> - <input style={{ width: '100%' }} type="checkbox" value={this.zoomFollow} onChange={this.handleZoomFollowChange} /> + <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleZoomFollowChange)} defaultChecked={this.zoomFollow} /> + </div> + </div> + </div> + ); + } + + @computed + get editAudioFollow() { + //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS + console.log('AudioFollow:' + this.audioFollow); + return ( + <div className="linkEditor-zoomFollow"> + <div className="linkEditor-zoomFollow-label">Play Target Audio:</div> + <div className="linkEditor-zoomFollow-input"> + <div className="linkEditor-zoomFollow-editing"> + <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleAudioFollowChange)} defaultChecked={this.audioFollow} /> </div> </div> </div> @@ -214,7 +223,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { get editDescription() { return ( <div className="linkEditor-description"> - <div className="linkEditor-description-label">Link Description:</div> + <div className="linkEditor-description-label">Description:</div> <div className="linkEditor-description-input"> <div className="linkEditor-description-editing"> <input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input> @@ -284,68 +293,100 @@ export class LinkEditor extends React.Component<LinkEditorProps> { ); } - @action - changeInfo = () => { - this.showInfo = !this.showInfo; + autoMove = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove)))); + }; + + showAnchor = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden)))); + }; + + showLink = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay)))); + }; + + deleteLink = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc)))); }; render() { const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); return !destination ? null : ( - <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()}> + <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="linkEditor-info"> - <Tooltip - title={ - <> - <div className="dash-tooltip">Return to link menu</div> - </> - } - placement="top"> - <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onClick={this.props.showLinks}> - <FontAwesomeIcon icon="arrow-left" size="sm" />{' '} - </button> - </Tooltip> + {!this.props.showLinks ? null : ( + <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top"> + <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}> + <FontAwesomeIcon icon="arrow-left" size="sm" />{' '} + </button> + </Tooltip> + )} <p className="linkEditor-linkedTo"> Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b> </p> - <Tooltip - title={ - <> - <div className="dash-tooltip">Show more link information</div> - </> - } - placement="top"> - <div className="linkEditor-downArrow"> - <FontAwesomeIcon className="button" icon={this.infoIcon} size="lg" onPointerDown={this.changeInfo} /> + <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> + <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> </div> </Tooltip> </div> - {this.showInfo ? ( - <div className="linkEditor-moreInfo"> - <div> - {this.props.linkDoc.author ? ( - <div> - {' '} - <b>Author:</b> {StrCast(this.props.linkDoc.author)} - </div> - ) : null} - </div> - <div> - {this.props.linkDoc.creationDate ? ( - <div> - {' '} - <b>Creation Date:</b> - {DateCast(this.props.linkDoc.creationDate).toString()} - </div> - ) : null} - </div> - </div> - ) : null} + <div className="linkEditor-moreInfo"> + {this.props.linkDoc.author ? ( + <> + {' '} + <b>Author:</b> {StrCast(this.props.linkDoc.author)} + </> + ) : null} + {this.props.linkDoc.creationDate ? ( + <> + {' '} + <b>Creation Date:</b> + {DateCast(this.props.linkDoc.creationDate).toString()} + </> + ) : null} + </div> {this.editDescription} {this.editRelationship} {this.editZoomFollow} + {this.editAudioFollow} + <div className="linkEditor-description"> + Show Anchor: + <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}> + <div + className="linkEditor-button" + style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue */ }} + onPointerDown={this.showAnchor} + onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" /> + </div> + </Tooltip> + </div> + <div className="linkEditor-description"> + Show Link Line: + <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}> + <div + className="linkEditor-button" + style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }} + onPointerDown={this.showLink} + onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" /> + </div> + </Tooltip> + </div> + <div className="linkEditor-description"> + Freeze Anchor: + <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>}> + <div + className="linkEditor-button" + style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }} + onPointerDown={this.autoMove} + onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" /> + </div> + </Tooltip> + </div> {this.followingDropdown} </div> ); diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 17d28e886..0096a58bd 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,17 +1,17 @@ -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; -import { LinkManager } from "../../util/LinkManager"; -import { DocumentView } from "../nodes/DocumentView"; -import { LinkDocPreview } from "../nodes/LinkDocPreview"; -import { LinkEditor } from "./LinkEditor"; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../fields/Doc'; +import { LinkManager } from '../../util/LinkManager'; +import { DocumentView } from '../nodes/DocumentView'; +import { LinkDocPreview } from '../nodes/LinkDocPreview'; +import { LinkEditor } from './LinkEditor'; import './LinkMenu.scss'; -import { LinkMenuGroup } from "./LinkMenuGroup"; -import React = require("react"); +import { LinkMenuGroup } from './LinkMenuGroup'; +import React = require('react'); interface Props { docView: DocumentView; - position?: { x?: number, y?: number }; + position?: { x?: number; y?: number }; itemHandler?: (doc: Doc) => void; clearLinkEditor: () => void; } @@ -34,13 +34,16 @@ export class LinkMenu extends React.Component<Props> { this._editingLink = undefined; }); - componentDidMount() { document.addEventListener("pointerdown", this.onPointerDown); } - componentWillUnmount() { document.removeEventListener("pointerdown", this.onPointerDown); } + componentDidMount() { + document.addEventListener('pointerdown', this.onPointerDown, true); + } + componentWillUnmount() { + document.removeEventListener('pointerdown', this.onPointerDown, true); + } onPointerDown = action((e: PointerEvent) => { LinkDocPreview.Clear(); - if (!this._linkMenuRef.current?.contains(e.target as any) && - !this._editorRef.current?.contains(e.target as any)) { + if (!this._linkMenuRef.current?.contains(e.target as any) && !this._editorRef.current?.contains(e.target as any)) { this.clear(); } }); @@ -51,7 +54,7 @@ export class LinkMenu extends React.Component<Props> { * @returns list of link JSX elements if there at least one linked element */ renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { - const linkItems = Array.from(groups.entries()).map(group => + const linkItems = Array.from(groups.entries()).map(group => ( <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} @@ -60,23 +63,25 @@ export class LinkMenu extends React.Component<Props> { group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} - showEditor={action(linkDoc => this._editingLink = linkDoc)} />); + showEditor={action(linkDoc => (this._editingLink = linkDoc))} + /> + )); return linkItems.length ? linkItems : this.props.position ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; - } + }; render() { const sourceDoc = this.props.docView.props.Document; - return <div className="linkMenu" ref={this._linkMenuRef} - style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }} - > - {this._editingLink ? - <div className="linkMenu-listEditor"> - <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} /> - </div> : - <div className="linkMenu-list" > - {this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceDoc))} - </div>} - </div>; + return ( + <div className="linkMenu" ref={this._linkMenuRef} style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }}> + {this._editingLink ? ( + <div className="linkMenu-listEditor"> + <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} /> + </div> + ) : ( + <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceDoc))}</div> + )} + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index ed856a4ab..3f9db2612 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -6,14 +6,12 @@ import { observer } from 'mobx-react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { Cast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; -import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; -import { undoBatch } from '../../util/UndoManager'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; @@ -125,33 +123,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { ); }; - deleteLink = (e: React.PointerEvent): void => { - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoBatch( - action(() => { - this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); - LinkManager.Instance.deleteLink(this.props.linkDoc); - }) - ) - ); - }; - - autoMove = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove)))); - }; - - showLink = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay)))); - }; - - showAnchor = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden)))); - }; - render() { const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp; @@ -183,7 +154,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { linkSrc: this.props.sourceDoc, linkDoc: this.props.linkDoc, showHeader: false, - location: [e.clientX, e.clientY + 20], + location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY], }) } onPointerDown={this.onLinkButtonDown}> @@ -206,69 +177,11 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { </div> <div className="linkMenu-item-buttons" ref={this._buttonRef}> - <Tooltip - title={ - <> - <div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div> - </> - }> - <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? '' : '#4476f7' /* $medium-blue */ }} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" /> - </div> - </Tooltip> - - <Tooltip - title={ - <> - <div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div> - </> - }> - <div - className="button" - ref={this._editRef} - style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }} - onPointerDown={this.showLink} - onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" /> - </div> - </Tooltip> - - <Tooltip - title={ - <> - <div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div> - </> - }> - <div - className="button" - ref={this._editRef} - style={{ background: this.props.linkDoc.hidden ? 'gray' : !this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }} - onPointerDown={this.autoMove} - onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" /> - </div> - </Tooltip> - - <Tooltip - title={ - <> - <div className="dash-tooltip">Edit Link</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> </div> </Tooltip> - <Tooltip - title={ - <> - <div className="dash-tooltip">Delete Link</div> - </> - }> - <div className="button" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> - </div> - </Tooltip> </div> </div> </div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 284584a3d..a48906372 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,26 +1,27 @@ -import { action, computed, observable, trace } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { TraceMobx } from "../../../fields/util"; -import { DashColor, numberRange, OmitKeys } from "../../../Utils"; -import { DocumentManager } from "../../util/DocumentManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Transform } from "../../util/Transform"; -import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { DocComponent } from "../DocComponent"; -import { InkingStroke } from "../InkingStroke"; -import { StyleProp } from "../StyleProvider"; -import "./CollectionFreeFormDocumentView.scss"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; -import React = require("react"); +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, Opt } from '../../../fields/Doc'; +import { InkField } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { numberRange } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; +import { SelectionManager } from '../../util/SelectionManager'; +import { Transform } from '../../util/Transform'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { DocComponent } from '../DocComponent'; +import { StyleProp } from '../StyleProvider'; +import './CollectionFreeFormDocumentView.scss'; +import { DocumentView, DocumentViewProps } from './DocumentView'; +import React = require('react'); export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined; - sizeProvider?: (doc: Doc, replica: string) => { width: number, height: number } | undefined; + dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; + sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined; renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; highlight?: boolean; @@ -32,65 +33,131 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() { - public static animFields = ["_height", "_width", "x", "y", "_scrollTop", "opacity"]; // fields that are configured to be animatable using animation frames + public static animFields: { key: string; val?: number }[] = [ + { key: '_height' }, + { key: '_width' }, + { key: 'x' }, + { key: 'y' }, + { key: '_jitterRotation', val: 0 }, + { key: '_scrollTop' }, + { key: 'opacity', val: 1 }, + { key: 'viewScale', val: 1 }, + { key: 'panX' }, + { key: 'panY' }, + ]; // fields that are configured to be animatable using animation frames + public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames + public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames @observable _animPos: number[] | undefined = undefined; @observable _contentView: DocumentView | undefined | null; - get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive - get maskCentering() { return this.props.Document.isInkMask ? InkingStroke.MaskDim / 2 : 0; } - get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`; } - get X() { return this.dataProvider ? this.dataProvider.x : NumCast(this.Document.x); } - get Y() { return this.dataProvider ? this.dataProvider.y : NumCast(this.Document.y); } - get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : NumCast(this.Document.zIndex); } - get Opacity() { return this.dataProvider ? this.dataProvider.opacity : undefined; } - get Highlight() { return this.dataProvider?.highlight; } - @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt<string>); } - @computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); } - @computed get sizeProvider() { return this.props.sizeProvider?.(this.props.Document, this.props.replica); } + get displayName() { + // this makes mobx trace() statements more descriptive + return 'CollectionFreeFormDocumentView(' + this.rootDoc.title + ')'; + } + + get transform() { + return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`; + } + get X() { + return this.dataProvider?.x ?? NumCast(this.Document.x); + } + get Y() { + return this.dataProvider?.y ?? NumCast(this.Document.y); + } + get ZInd() { + return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex); + } + get Opacity() { + return this.dataProvider?.opacity; + } + get BackgroundColor() { + return this.dataProvider?.backgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); + } + get Color() { + return this.dataProvider?.color ?? Cast(this.Document._color, 'string', null); + } + get Highlight() { + return this.dataProvider?.highlight; + } + @computed get dataProvider() { + return this.props.dataProvider?.(this.props.Document, this.props.replica); + } + @computed get sizeProvider() { + return this.props.sizeProvider?.(this.props.Document, this.props.replica); + } styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { - if (property === StyleProp.Opacity && doc === this.layoutDoc) return this.Opacity; // only change the opacity for this specific document, not its children + if (doc === this.layoutDoc) + // prettier-ignore + switch (property) { + case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children + case StyleProp.BackgroundColor: return this.BackgroundColor; + case StyleProp.Color: return this.Color; + } return this.props.styleProvider?.(doc, props, property); - } + }; - public static getValues(doc: Doc, time: number) { + public static getValues(doc: Doc, time: number, fillIn: boolean = true) { return CollectionFreeFormDocumentView.animFields.reduce((p, val) => { - p[val] = Cast(`${val}-indexed`, listSpec("number"), [NumCast(doc[val])]).reduce((p, v, i) => (i <= Math.round(time) && v !== undefined) || p === undefined ? v : p, undefined as any as number); + p[val.key] = Cast(doc[`${val.key}-indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); return p; }, {} as { [val: string]: Opt<number> }); } + public static getStringValues(doc: Doc, time: number) { + return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => { + p[val] = Cast(doc[`${val}-indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); + return p; + }, {} as { [val: string]: Opt<string> }); + } + public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { - const findexed = Cast(d[`${val}-indexed`], listSpec("number"), []).slice(); + const findexed = Cast(d[`${val}-indexed`], listSpec('number'), []).slice(); findexed[timecode] = vals[val] as any as number; d[`${val}-indexed`] = new List<number>(findexed); }); - d.appearFrame && (d["text-color"] = - d.appearFrame === timecode + 1 ? "red" : - d.appearFrame < timecode + 1 ? "grey" : "black"); } public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) { const timecode = Math.round(time); - docs.forEach(action(doc => { - doc._viewTransition = doc.dataTransition = "all 1s"; - doc["text-color"] = - !doc.appearFrame || !targetDoc ? "black" : - doc.appearFrame === timecode + 1 ? StrCast(targetDoc["pres-text-color"]) : - doc.appearFrame < timecode + 1 ? StrCast(targetDoc["pres-text-viewed-color"]) : - "black"; - CollectionFreeFormDocumentView.animFields.forEach(val => { - const findexed = Cast(doc[`${val}-indexed`], listSpec("number"), null); - findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); - }); - })); - setTimeout(() => docs.forEach(doc => { doc._viewTransition = undefined; doc.dataTransition = "inherit"; }), 1010); + docs.forEach( + action(doc => { + doc._viewTransition = doc.dataTransition = 'all 1s'; + CollectionFreeFormDocumentView.animFields.forEach(val => { + const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); + }); + CollectionFreeFormDocumentView.animStringFields.forEach(val => { + const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); + }); + CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { + const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any); + }); + }) + ); + setTimeout( + () => + docs.forEach(doc => { + doc._viewTransition = undefined; + doc.dataTransition = 'inherit'; + }), + 1010 + ); } - public static gotoKeyframe(docs: Doc[]) { - docs.forEach(doc => doc._viewTransition = doc.dataTransition = "all 1s"); - setTimeout(() => docs.forEach(doc => { doc._viewTransition = undefined; doc.dataTransition = "inherit"; }), 1010); + public static gotoKeyframe(docs: Doc[], duration = 1000) { + docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`)); + setTimeout( + () => + docs.forEach(doc => { + doc._viewTransition = undefined; + doc.dataTransition = 'inherit'; + }), + 1010 + ); } public static setupZoom(doc: Doc, targDoc: Doc) { @@ -102,21 +169,26 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF height.push(NumCast(targDoc._height)); top.push(NumCast(targDoc._height) / -2); left.push(NumCast(targDoc._width) / -2); - doc["viewfinder-width-indexed"] = width; - doc["viewfinder-height-indexed"] = height; - doc["viewfinder-top-indexed"] = top; - doc["viewfinder-left-indexed"] = left; + doc['viewfinder-width-indexed'] = width; + doc['viewfinder-height-indexed'] = height; + doc['viewfinder-top-indexed'] = top; + doc['viewfinder-left-indexed'] = left; } public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) { docs.forEach(doc => { if (doc.appearFrame === undefined) doc.appearFrame = currTimecode; - if (!doc["opacity-indexed"]) { // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in - doc["opacity-indexed"] = new List<number>(numberRange(currTimecode + 1).map(t => !doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)); + if (!doc['opacity-indexed']) { + // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in + doc['opacity-indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); } - CollectionFreeFormDocumentView.animFields.forEach(val => doc[val] = ComputedField.MakeInterpolated(val, "activeFrame", doc, currTimecode)); - doc.activeFrame = ComputedField.MakeFunction("self.context?._currentFrame||0"); - doc.dataTransition = "inherit"; + CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val))); + CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); + CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode))); + const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so + doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) + targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0'); + targetDoc.dataTransition = 'inherit'; }); } @@ -131,7 +203,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF topDoc.x = spt[0]; topDoc.y = spt[1]; this.props.removeDocument?.(topDoc); - this.props.addDocTab(topDoc, "inParent"); + this.props.addDocTab(topDoc, 'inParent'); } else { const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0); const fpt = screenXf.transformPoint(spt[0], spt[1]); @@ -141,14 +213,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, container)!, false), 0); } - } + }; nudge = (x: number, y: number) => { this.props.Document.x = NumCast(this.props.Document.x) + x; this.props.Document.y = NumCast(this.props.Document.y) + y; - } - panelWidth = () => (this.sizeProvider?.width || this.props.PanelWidth?.()); - panelHeight = () => (this.sizeProvider?.height || this.props.PanelHeight?.()); + }; + panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.(); + panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.(); screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y); focusDoc = (doc: Doc) => this.props.focus(doc); returnThis = () => this; @@ -162,25 +234,25 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, }; - const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && background && (!background.startsWith("linear") && DashColor(background).alpha() !== 1) ? "multiply" : undefined); - return <div className={"collectionFreeFormDocumentView-container"} - style={{ - outline: this.Highlight ? "orange solid 2px" : "", - width: this.panelWidth(), - height: this.panelHeight(), - transform: this.transform, - transformOrigin: '50% 50%', - transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)), - zIndex: this.ZInd, - mixBlendMode: mixBlendMode, - display: this.ZInd === -99 ? "none" : undefined - }} > - {this.props.renderCutoffProvider(this.props.Document) ? - <div style={{ position: "absolute", width: this.panelWidth(), height: this.panelHeight(), background: "lightGreen" }} /> - : - <DocumentView {...divProps} ref={action((r: DocumentView | null) => this._contentView = r)} /> - } - </div>; + return ( + <div + className={'collectionFreeFormDocumentView-container'} + style={{ + outline: this.Highlight ? 'orange solid 2px' : '', + width: this.panelWidth(), + height: this.panelHeight(), + transform: this.transform, + transformOrigin: '50% 50%', + transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)), + zIndex: this.ZInd, + display: this.ZInd === -99 ? 'none' : undefined, + }}> + {this.props.renderCutoffProvider(this.props.Document) ? ( + <div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} /> + ) : ( + <DocumentView {...divProps} ref={action((r: DocumentView | null) => (this._contentView = r))} /> + )} + </div> + ); } } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index f1d8123da..6d6609317 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -25,7 +25,7 @@ import './DocumentView.scss'; import { EquationBox } from './EquationBox'; import { FieldView, FieldViewProps } from './FieldView'; import { FilterBox } from './FilterBox'; -import { FormattedTextBox, FormattedTextBoxProps } from './formattedText/FormattedTextBox'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { FunctionPlotBox } from './FunctionPlotBox'; import { ImageBox } from './ImageBox'; import { KeyValueBox } from './KeyValueBox'; @@ -43,6 +43,7 @@ import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; import React = require('react'); import XRegExp = require('xregexp'); +import { LoadingBox } from './LoadingBox'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -114,14 +115,13 @@ export class HTMLtag extends React.Component<HTMLtagProps> { @observer export class DocumentContentsView extends React.Component< - DocumentViewProps & - FormattedTextBoxProps & { - isSelected: (outsideReaction: boolean) => boolean; - select: (ctrl: boolean) => void; - scaling?: () => number; - setHeight?: (height: number) => void; - layoutKey: string; - } + DocumentViewProps & { + isSelected: (outsideReaction: boolean) => boolean; + select: (ctrl: boolean) => void; + NativeDimScaling?: () => number; + setHeight?: (height: number) => void; + layoutKey: string; + } > { @computed get layout(): string { TraceMobx(); @@ -161,7 +161,6 @@ export class DocumentContentsView extends React.Component< 'LayoutTemplateString', 'LayoutTemplate', 'dontCenter', - 'ContentScaling', 'contextMenuItems', 'onClick', 'onDoubleClick', @@ -195,7 +194,7 @@ export class DocumentContentsView extends React.Component< // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'> const replacer2 = (match: any, p1: string, offset: any, string: any) => { - return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} scaling='${this.props.scaling?.() || 1}' htmltag='${p1}'`; + return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} scaling='${this.props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; }; layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2); @@ -268,6 +267,7 @@ export class DocumentContentsView extends React.Component< DataVizBox, HTMLtag, ComparisonBox, + LoadingBox, }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 78d35ab99..a37de7f69 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -1,24 +1,24 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from "@material-ui/core"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../fields/Doc"; -import { StrCast } from "../../../fields/Types"; -import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; -import { DocUtils } from "../../documents/Documents"; -import { DragManager } from "../../util/DragManager"; -import { Hypothesis } from "../../util/HypothesisUtils"; -import { LinkManager } from "../../util/LinkManager"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { Colors } from "../global/globalEnums"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, Opt } from '../../../fields/Doc'; +import { StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { DocUtils } from '../../documents/Documents'; +import { DragManager } from '../../util/DragManager'; +import { Hypothesis } from '../../util/HypothesisUtils'; +import { LinkManager } from '../../util/LinkManager'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { Colors } from '../global/globalEnums'; import './DocumentLinksButton.scss'; -import { DocumentView } from "./DocumentView"; -import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; -import { TaskCompletionBox } from "./TaskCompletedBox"; -import React = require("react"); +import { DocumentView } from './DocumentView'; +import { LinkDescriptionPopup } from './LinkDescriptionPopup'; +import { TaskCompletionBox } from './TaskCompletedBox'; +import React = require('react'); -const higflyout = require("@hig/flyout"); +const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -28,7 +28,7 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) - ContentScaling?: () => number; + scaling?: () => number; // how uch doc is scaled so that link buttons can invert it } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -42,53 +42,71 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @observable public static invisibleWebDoc: Opt<Doc>; public static invisibleWebRef = React.createRef<HTMLDivElement>(); - @action @undoBatch + @action + @undoBatch onLinkButtonMoved = (e: PointerEvent) => { if (this.props.InMenu && this.props.StartLink) { if (this._linkButton.current !== null) { - const linkDrag = UndoManager.StartBatch("Drag Link"); - this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View, this.props.View.ComponentView?.getAnchor, e.pageX, e.pageY, { - dragComplete: dropEv => { - if (this.props.View && dropEv.linkDocument) {// dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop - !dropEv.linkDocument.linkRelationship && (Doc.GetProto(dropEv.linkDocument).linkRelationship = "hyperlink"); - } - linkDrag?.end(); - }, - hideSource: false - }); + const linkDrag = UndoManager.StartBatch('Drag Link'); + this.props.View && + DragManager.StartLinkDrag(this._linkButton.current, this.props.View, this.props.View.ComponentView?.getAnchor, e.pageX, e.pageY, { + dragComplete: dropEv => { + if (this.props.View && dropEv.linkDocument) { + // dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop + !dropEv.linkDocument.linkRelationship && (Doc.GetProto(dropEv.linkDocument).linkRelationship = 'hyperlink'); + } + linkDrag?.end(); + }, + hideSource: false, + }); return true; } return false; } return false; - } + }; onLinkMenuOpen = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - if (doubleTap) { - DocumentView.showBackLinks(this.props.View.rootDoc); - } - }), undefined, undefined, - action(() => DocumentLinksButton.LinkEditorDocView = this.props.View)); - } + setupMoveUpEvents( + this, + e, + this.onLinkButtonMoved, + emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + DocumentView.showBackLinks(this.props.View.rootDoc); + } + }), + undefined, + undefined, + action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View)) + ); + }; @undoBatch onLinkButtonDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - if (doubleTap && this.props.InMenu && this.props.StartLink) { - //action(() => Doc.BrushDoc(this.props.View.Document)); - if (DocumentLinksButton.StartLink === this.props.View.props.Document) { - DocumentLinksButton.StartLink = undefined; - DocumentLinksButton.StartLinkView = undefined; - } else { - DocumentLinksButton.StartLink = this.props.View.props.Document; - DocumentLinksButton.StartLinkView = this.props.View; + setupMoveUpEvents( + this, + e, + this.onLinkButtonMoved, + emptyFunction, + action((e, doubleTap) => { + if (doubleTap && this.props.InMenu && this.props.StartLink) { + //action(() => Doc.BrushDoc(this.props.View.Document)); + if (DocumentLinksButton.StartLink === this.props.View.props.Document) { + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + } else { + DocumentLinksButton.StartLink = this.props.View.props.Document; + DocumentLinksButton.StartLinkView = this.props.View; + } } - } - })); - } + }) + ); + }; - @action @undoBatch + @action + @undoBatch onLinkClick = (e: React.MouseEvent): void => { if (this.props.InMenu && this.props.StartLink) { DocumentLinksButton.AnnotationId = undefined; @@ -96,108 +114,125 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; - } else { //if this LinkButton's Document is undefined + } else { + //if this LinkButton's Document is undefined DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } //action(() => Doc.BrushDoc(this.props.View.Document)); } - } - + }; completeLink = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action((e, doubleTap) => { - if (doubleTap && !this.props.StartLink) { - if (DocumentLinksButton.StartLink === this.props.View.props.Document) { - DocumentLinksButton.StartLink = undefined; - DocumentLinksButton.StartLinkView = undefined; - DocumentLinksButton.AnnotationId = undefined; - } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { - const sourceDoc = DocumentLinksButton.StartLink; - const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document; - const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, "links"); //why is long drag here when this is used for completing links by clicking? + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoBatch( + action((e, doubleTap) => { + if (doubleTap && !this.props.StartLink) { + if (DocumentLinksButton.StartLink === this.props.View.props.Document) { + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + DocumentLinksButton.AnnotationId = undefined; + } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { + const sourceDoc = DocumentLinksButton.StartLink; + const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document; + const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, 'links'); //why is long drag here when this is used for completing links by clicking? - LinkManager.currentLink = linkDoc; + LinkManager.currentLink = linkDoc; - runInAction(() => { - if (linkDoc) { - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = e.screenX; - TaskCompletionBox.popupY = e.screenY - 133; - TaskCompletionBox.taskCompleted = true; + runInAction(() => { + if (linkDoc) { + TaskCompletionBox.textDisplayed = 'Link Created'; + TaskCompletionBox.popupX = e.screenX; + TaskCompletionBox.popupY = e.screenY - 133; + TaskCompletionBox.taskCompleted = true; - LinkDescriptionPopup.popupX = e.screenX; - LinkDescriptionPopup.popupY = e.screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + LinkDescriptionPopup.popupX = e.screenX; + LinkDescriptionPopup.popupY = e.screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; - const rect = document.body.getBoundingClientRect(); - if (LinkDescriptionPopup.popupX + 200 > rect.width) { - LinkDescriptionPopup.popupX -= 190; - TaskCompletionBox.popupX -= 40; - } - if (LinkDescriptionPopup.popupY + 100 > rect.height) { - LinkDescriptionPopup.popupY -= 40; - TaskCompletionBox.popupY -= 40; - } + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } - setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); + setTimeout( + action(() => (TaskCompletionBox.taskCompleted = false)), + 2500 + ); + } + }); } - }); - } - } - }))); - } + } + }) + ) + ); + }; - public static finishLinkClick = undoBatch(action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView,) => { - if (startLink === endLink) { - DocumentLinksButton.StartLink = undefined; - DocumentLinksButton.StartLinkView = undefined; - DocumentLinksButton.AnnotationId = undefined; - DocumentLinksButton.AnnotationUri = undefined; - //!this.props.StartLink - } else if (startLink !== endLink) { - endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink; - startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink; - const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, - DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : undefined, undefined, undefined, true); + public static finishLinkClick = undoBatch( + action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView) => { + if (startLink === endLink) { + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + DocumentLinksButton.AnnotationId = undefined; + DocumentLinksButton.AnnotationUri = undefined; + //!this.props.StartLink + } else if (startLink !== endLink) { + endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink; + startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink; + const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true); - LinkManager.currentLink = linkDoc; + LinkManager.currentLink = linkDoc; - if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation - Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; - Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; - Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; - const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink); - Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId, - (startIsAnnotation ? startLink : endLink)); // edit annotation to add a Dash hyperlink to the linked doc - } + if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { + // if linking from a Hypothes.is annotation + Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; + Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; + Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; + const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink); + Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId, startIsAnnotation ? startLink : endLink); // edit annotation to add a Dash hyperlink to the linked doc + } - if (linkDoc) { - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = screenX; - TaskCompletionBox.popupY = screenY - 133; - TaskCompletionBox.taskCompleted = true; + if (linkDoc) { + TaskCompletionBox.textDisplayed = 'Link Created'; + TaskCompletionBox.popupX = screenX; + TaskCompletionBox.popupY = screenY - 133; + TaskCompletionBox.taskCompleted = true; - if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { - LinkDescriptionPopup.popupX = screenX; - LinkDescriptionPopup.popupY = screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; - } + if (LinkDescriptionPopup.showDescriptions === 'ON' || !LinkDescriptionPopup.showDescriptions) { + LinkDescriptionPopup.popupX = screenX; + LinkDescriptionPopup.popupY = screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; + } - const rect = document.body.getBoundingClientRect(); - if (LinkDescriptionPopup.popupX + 200 > rect.width) { - LinkDescriptionPopup.popupX -= 190; - TaskCompletionBox.popupX -= 40; - } - if (LinkDescriptionPopup.popupY + 100 > rect.height) { - LinkDescriptionPopup.popupY -= 40; - TaskCompletionBox.popupY -= 40; - } + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } - setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); + setTimeout( + action(() => { + TaskCompletionBox.taskCompleted = false; + }), + 2500 + ); + } } - } - })); + }) + ); @action clearLinks() { DocumentLinksButton.StartLink = undefined; @@ -208,9 +243,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp const results = [] as Doc[]; const filters = this.props.View.props.docFilters(); Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { - if (DocUtils.FilterDocs([link], filters, []).length || - DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || - DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { + if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { results.push(link); } }); @@ -219,48 +252,45 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp /** * gets the JSX of the link button (btn used to start/complete links) OR the link-view button (btn on bottom left of each linked node) - * + * * todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button */ @computed get linkButtonInner() { - const btnDim = "30px"; - const link = <img style={{ width: "22px", height: "16px" }} src={`/assets/${"link.png"}`} />; - const isActive = (DocumentLinksButton.StartLink === this.props.View.props.Document) && this.props.StartLink; - return (!this.props.InMenu ? - <div className="documentLinksButton-cont" - style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }} - > - <div className={"documentLinksButton"} - onPointerDown={this.onLinkMenuOpen} onClick={this.onLinkClick} + const btnDim = '30px'; + const link = <img style={{ width: '22px', height: '16px' }} src={`/assets/${'link.png'}`} />; + const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink; + return !this.props.InMenu ? ( + <div className="documentLinksButton-cont" style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }}> + <div + className={'documentLinksButton'} + onPointerDown={this.onLinkMenuOpen} + onClick={this.onLinkClick} style={{ backgroundColor: Colors.LIGHT_BLUE, color: Colors.BLACK, - fontSize: "20px", + fontSize: '20px', width: btnDim, height: btnDim, }}> {Array.from(this.filteredLinks).length} </div> </div> - : - <div className="documentLinksButton-menu" > - {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? //if the origin node is not this node - <div className={"documentLinksButton-endLink"} ref={this._linkButton} + ) : ( + <div className="documentLinksButton-menu"> + {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node + <div + className={'documentLinksButton-endLink'} + ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink} onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}> <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> - : (null) - } - { - this.props.InMenu && this.props.StartLink ? //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again - <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} - onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> - <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> - </div> - : - (null) - } + ) : null} + {this.props.InMenu && this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again + <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> + <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> + </div> + ) : null} </div> ); } @@ -268,25 +298,23 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp render() { TraceMobx(); - const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link"; - const buttonTitle = "Tap to view links; double tap to open link collection"; + const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; + const buttonTitle = 'Tap to view links; double tap to open link collection'; const title = this.props.InMenu ? menuTitle : buttonTitle; //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu - return (!Array.from(this.filteredLinks).length && !this.props.AlwaysOn) ? (null) : - <div className="documentLinksButton-wrapper" + return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : ( + <div + className="documentLinksButton-wrapper" style={{ - transform: this.props.InMenu ? undefined : - `scale(${(this.props.ContentScaling?.() || 1) * this.props.View.screenToLocalTransform().Scale})` - }} > - { - (this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || - (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? - <Tooltip title={<div className="dash-tooltip">{title}</div>}> - {this.linkButtonInner} - </Tooltip> - : this.linkButtonInner - } - </div>; + transform: `scale(${this.props.scaling?.() || 1})`, + }}> + {(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? ( + <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip> + ) : ( + this.linkButtonInner + )} + </div> + ); } } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 6a1bfa406..6cadeec41 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,7 +1,8 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .documentView-effectsWrapper { border-radius: inherit; + transition: inherit; } // documentViews have a docView-hack tag which is replaced by this tag when capturing bitmaps (when the dom is converted to an html string) @@ -24,8 +25,7 @@ width: 100%; height: 100%; border-radius: inherit; - transition: outline .3s linear; - cursor: grab; + transition: outline 0.3s linear; // background: $white; //overflow: hidden; transform-origin: left top; @@ -62,14 +62,16 @@ .documentView-audioBackground { display: inline-block; - width: 10%; + width: 25px; height: 25; position: absolute; - top: 10px; - left: 10px; + top: 0; + left: 50%; border-radius: 25px; background: white; opacity: 0.3; + pointer-events: all; + cursor: default; svg { width: 90% !important; @@ -88,7 +90,7 @@ width: 100%; overflow: hidden; - >.documentView-node { + > .documentView-node { position: absolute; } } @@ -97,6 +99,7 @@ border-radius: inherit; width: 100%; height: 100%; + transition: inherit; .sharingIndicator { height: 30px; @@ -158,7 +161,7 @@ top: 0; width: 100%; height: 14; - background: rgba(0, 0, 0, .4); + background: rgba(0, 0, 0, 0.4); text-align: center; text-overflow: ellipsis; white-space: pre; @@ -187,19 +190,18 @@ transition: opacity 0.5s; } } - } .documentView-node:hover, .documentView-node-topmost:hover { - >.documentView-styleWrapper { - >.documentView-titleWrapper-hover { + > .documentView-styleWrapper { + > .documentView-titleWrapper-hover { display: inline-block; } } - >.documentView-styleWrapper { - >.documentView-captionWrapper { + > .documentView-styleWrapper { + > .documentView-captionWrapper { opacity: 1; } } @@ -210,10 +212,12 @@ display: flex; width: 100%; height: 100%; + transition: inherit; .contentFittingDocumentView-previewDoc { position: relative; display: inline; + transition: inherit; } .contentFittingDocumentView-input { @@ -225,6 +229,6 @@ .documentView-node:first-child { position: relative; - background: "#B59B66"; //$white; + background: '#B59B66'; //$white; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index dea718a0d..e628c2e44 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,8 +1,9 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -20,6 +21,7 @@ import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; +import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { InteractionUtils } from '../../util/InteractionUtils'; @@ -37,7 +39,6 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; -import { InkingStroke } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; @@ -156,6 +157,7 @@ export interface DocumentViewSharedProps { scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document createNewFilterDoc?: () => void; updateFilterDoc?: (doc: Doc) => void; + dontHideOnDrag?: boolean; } // these props are specific to DocuentViews @@ -175,9 +177,9 @@ export interface DocumentViewProps extends DocumentViewSharedProps { LayoutTemplateString?: string; dontCenter?: 'x' | 'y' | 'xy'; dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling - ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NativeWidth?: () => number; NativeHeight?: () => number; + NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps LayoutTemplate?: () => Opt<Doc>; contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; onClick?: () => ScriptField; @@ -203,7 +205,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. _animateScaleTime = 300; // milliseconds; @observable _animateScalingTo = 0; - @observable _mediaState = 0; @observable _pendingDoubleClick = false; private _disposers: { [name: string]: IReactionDisposer } = {}; private _downX: number = 0; @@ -235,11 +236,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>; } - @computed get ContentScale() { - return this.props.ContentScaling?.() || 1; + @computed get NativeDimScaling() { + return this.props.NativeDimScaling?.() || 1; } @computed get thumb() { - return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url.href.replace('.png', '_m.png'); + return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); } @computed get hidden() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden); @@ -428,7 +429,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps let nheight = Doc.NativeHeight(layoutDoc); const width = layoutDoc._width || 0; const height = layoutDoc._height || (nheight / nwidth) * width; - const scale = this.props.ScreenToLocalTransform().Scale * this.ContentScale; + const scale = this.props.ScreenToLocalTransform().Scale * this.NativeDimScaling; const actualdW = Math.max(width + dW * scale, 20); const actualdH = Math.max(height + dH * scale, 20); doc.x = (doc.x || 0) + dX * (actualdW - width); @@ -491,20 +492,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { if (this._mainCont.current) { const dragData = new DragManager.DocumentDragData([this.props.Document]); - const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0); + const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0); dragData.offset = this.props .ScreenToLocalTransform() - .scale(this.ContentScale) + .scale(this.NativeDimScaling) .transformDirection(x - left, y - top); - dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); - dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); + // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document + // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); dragData.dropAction = dropAction; dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; + //dragData.dimSource : + // dragEffects field, set dim + // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping + // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging: + // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); - DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) }, () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. + DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () => + setTimeout(action(() => ffview && (ffview.ChildDrag = undefined))) + ); // this needs to happen after the drop event is processed. ffview?.setupDragLines(false); } } @@ -716,7 +724,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.Document.followLinkLocation = location; } else if (this.Document._isLinkButton && this.onClickHandler) { this.Document._isLinkButton = false; - this.Document['onClick-rawScript'] = this.dataDoc['onClick-rawScript'] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; + this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; } }; @undoBatch @@ -750,7 +758,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); - @undoBatch setToggleDetail = () => (this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace('layout_', '')}")`, { documentView: 'any' })); + @undoBatch setToggleDetail = () => + (this.Document.onClick = ScriptField.MakeScript( + `toggleDetail(documentView, "${StrCast(this.Document.layoutKey) + .replace('layout_', '') + .replace(/^layout$/, 'detail')}")`, + { documentView: 'any' } + )); @undoBatch @action @@ -863,7 +877,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { - !Doc.noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? 'Show' : 'Hide'} Audio Button`, event: action(() => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio)), icon: 'microphone' }); const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; @@ -956,12 +969,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps collectionFilters = () => StrListCast(this.props.Document._docFilters); collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters); @computed get showFilterIcon() { - return this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined; + return this.collectionFilters().length || this.collectionRangeDocFilters().length + ? 'hasFilter' + : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || this.props.docRangeFilters().length + ? 'inheritsFilter' + : undefined; } rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; panelHeight = () => this.props.PanelHeight() - this.headerMargin; screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); - contentScaling = () => this.ContentScale; onClickFunc = () => this.onClickHandler; setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: () => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); @@ -989,18 +1005,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ? true : false; }; + linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale; @computed get contents() { TraceMobx(); - const audioView = !this.layoutDoc._showAudio ? null : ( - <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter}> - <FontAwesomeIcon - className="documentView-audioFont" - style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }} - icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'microphone' : 'file-audio'} - size="sm" - /> - </div> - ); + const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length; + const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null); + const audioView = + (!this.props.isSelected() && !this._isHovering && this.dataDoc.audioAnnoState !== 2) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this.dataDoc.audioAnnoState) ? null : ( + <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}> + <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}> + <FontAwesomeIcon + className="documentView-audioFont" + style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][NumCast(this.dataDoc.audioAnnoState)] }} + icon={!audioAnnosCount ? 'microphone' : 'file-audio'} + size="sm" + /> + </div> + </Tooltip> + ); return ( <div @@ -1041,7 +1063,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps thumbShown={this.thumbShown} isHovering={this.isHovering} setContentView={this.setContentView} - scaling={this.contentScaling} + NativeDimScaling={this.props.NativeDimScaling} PanelHeight={this.panelHeight} setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined} isContentActive={this.isContentActive} @@ -1055,8 +1077,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? null : ( <DocumentLinksButton View={this.props.DocumentView()} - ContentScaling={this.props.ContentScaling} - Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -30]} + scaling={this.linkButtonInverseScaling} + Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -28]} /> )} {audioView} @@ -1134,58 +1156,72 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } @action - onPointerEnter = () => { + playAnnotation = () => { const self = this; - const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']); - if (audioAnnos && audioAnnos.length && this._mediaState === 0) { - const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; - anno.data instanceof AudioField && - new Howl({ - src: [anno.data.url.href], - format: ['mp3'], - autoplay: true, - loop: false, - volume: 0.5, - onend: function () { - runInAction(() => (self._mediaState = 0)); - }, - }); - this._mediaState = 1; + const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); + const anno = audioAnnos.lastElement(); + if (anno instanceof AudioField && this.dataDoc.audioAnnoState === 0) { + new Howl({ + src: [anno.url.href], + format: ['mp3'], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => { + self.dataDoc.audioAnnoState = 0; + }); + }, + }); + this.dataDoc.audioAnnoState = 1; } }; - recordAudioAnnotation = () => { + + static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) { let gumStream: any; let recorder: any; - const self = this; navigator.mediaDevices .getUserMedia({ audio: true, }) .then(function (stream) { + let audioTextAnnos = Cast(dataDoc[field + '-audioAnnotations-text'], listSpec('string'), null); + if (audioTextAnnos) audioTextAnnos.push(''); + else audioTextAnnos = dataDoc[field + '-audioAnnotations-text'] = new List<string>(['']); + DictationManager.Controls.listen({ + interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), + continuous: { indefinite: false }, + }).then(results => { + if (results && [DictationManager.Controls.Infringed].includes(results)) { + DictationManager.Controls.stop(); + } + onEnd?.(); + }); + gumStream = stream; recorder = new MediaRecorder(stream); recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer(e.data); if (!(result instanceof Error)) { - const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: 'audio test', _width: 200, _height: 32 }); - audioDoc.treeViewExpandedView = 'layout'; - const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(Doc)); + const audioField = new AudioField(result.accessPaths.agnostic.client); + const audioAnnos = Cast(dataDoc[field + '-audioAnnotations'], listSpec(AudioField), null); if (audioAnnos === undefined) { - self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioDoc]); + dataDoc[field + '-audioAnnotations'] = new List([audioField]); } else { - audioAnnos.push(audioDoc); + audioAnnos.push(audioField); } } }; - runInAction(() => (self._mediaState = 2)); + runInAction(() => (dataDoc.audioAnnoState = 2)); recorder.start(); setTimeout(() => { recorder.stop(); - runInAction(() => (self._mediaState = 0)); + DictationManager.Controls.stop(false); + runInAction(() => (dataDoc.audioAnnoState = 0)); gumStream.getAudioTracks()[0].stop(); }, 5000); }); - }; + } captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); @computed get innards() { @@ -1229,6 +1265,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps style={{ position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, + width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button color: lightOrDark(background), background, pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, @@ -1283,9 +1320,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps isHovering = () => this._isHovering; @observable _isHovering = false; @observable _: string = ''; + _hoverTimeout: any = undefined; @computed get renderDoc() { TraceMobx(); - const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url.href.replace('.png', '_m.png'); + const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); const isButton = this.props.Document.type === DocumentType.FONTICON; if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null; return ( @@ -1293,11 +1331,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <div className={`documentView-node${this.topMost ? '-topmost' : ''}`} id={this.props.Document[Id]} - onPointerEnter={action(() => (this._isHovering = true))} - onPointerLeave={action(() => (this._isHovering = false))} + onPointerEnter={action(() => { + clearTimeout(this._hoverTimeout); + this._isHovering = true; + })} + onPointerLeave={action(() => { + clearTimeout(this._hoverTimeout); + this._hoverTimeout = setTimeout( + action(() => (this._isHovering = false)), + 500 + ); + })} style={{ background: isButton || thumb ? undefined : this.backgroundColor, opacity: this.opacity, + cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', color: StrCast(this.layoutDoc.color, 'inherit'), fontFamily: StrCast(this.Document._fontFamily, 'inherit'), fontSize: Cast(this.Document._fontSize, 'string', null), @@ -1492,7 +1540,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; } @computed get Yshift() { - return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && !this.layoutDoc.nativeHeightUnfrozen ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; + return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && (!this.layoutDoc.nativeHeightUnfrozen || (!this.fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight())) + ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) + : 0; } @computed get centeringX() { return this.props.dontCenter?.includes('x') ? 0 : this.Xshift; @@ -1501,7 +1551,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.dontCenter?.includes('y') ? 0 : this.Yshift; } - toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); + toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options); getBounds = () => { if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { @@ -1535,11 +1585,15 @@ export class DocumentView extends React.Component<DocumentViewProps> { Doc.setNativeView(this.props.Document); custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); }; - switchViews = action((custom: boolean, view: string, finished?: () => void) => { + switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc setTimeout( action(() => { - this.setCustomView(custom, view); + if (useExistingLayout && custom && this.rootDoc['layout_' + view]) { + this.rootDoc.layoutKey = 'layout_' + view; + } else { + this.setCustomView(custom, view); + } this.docView && (this.docView._animateScalingTo = 1); // expand it setTimeout( action(() => { @@ -1562,7 +1616,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { NativeHeight = () => this.effectiveNativeHeight; PanelWidth = () => this.panelWidth; PanelHeight = () => this.panelHeight; - ContentScale = () => this.nativeScaling; + NativeDimScaling = () => this.nativeScaling; selfView = () => this; screenToLocalTransform = () => this.props @@ -1590,8 +1644,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { render() { TraceMobx(); - const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); - const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); + const xshift = () => (Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); + const yshift = () => (Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES; const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; return ( @@ -1602,7 +1656,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { ref={this.ContentRef} style={{ transition: this.props.dataTransition, - position: this.props.Document.isInkMask ? 'absolute' : undefined, transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, height: @@ -1618,9 +1671,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { PanelHeight={this.PanelHeight} NativeWidth={this.NativeWidth} NativeHeight={this.NativeHeight} + NativeDimScaling={this.NativeDimScaling} isSelected={this.isSelected} select={this.select} - ContentScaling={this.ContentScale} ScreenToLocalTransform={this.screenToLocalTransform} focus={this.props.focus || emptyFunction} ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} @@ -1643,7 +1696,7 @@ ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout'); - else dv.switchViews(true, detailLayoutKeySuffix); + else dv.switchViews(true, detailLayoutKeySuffix, undefined, true); }); ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) { diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index c170f9867..0bd30bce9 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -12,11 +12,12 @@ import { LightboxView } from '../LightboxView'; import './EquationBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; - @observer export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(EquationBox, fieldKey); } - public static SelectOnLoad: string = ""; + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(EquationBox, fieldKey); + } + public static SelectOnLoad: string = ''; _ref: React.RefObject<EquationEditor> = React.createRef(); componentDidMount() { if (EquationBox.SelectOnLoad === this.rootDoc[Id] && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { @@ -25,75 +26,92 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { this._ref.current!.mathField.focus(); this._ref.current!.mathField.select(); } - reaction(() => StrCast(this.dataDoc.text), + reaction( + () => StrCast(this.dataDoc.text), text => { if (text && text !== this._ref.current!.mathField.latex()) { this._ref.current!.mathField.latex(text); } - }); - reaction(() => this.props.isSelected(), + } + ); + reaction( + () => this.props.isSelected(), selected => { if (this._ref.current) { - if (selected) this._ref.current.element.current.children[0].addEventListener("keydown", this.keyPressed, true); - else this._ref.current.element.current.children[0].removeEventListener("keydown", this.keyPressed); + if (selected) this._ref.current.element.current.children[0].addEventListener('keydown', this.keyPressed, true); + else this._ref.current.element.current.children[0].removeEventListener('keydown', this.keyPressed); } - }, { fireImmediately: true }); + }, + { fireImmediately: true } + ); } plot: any; @action keyPressed = (e: KeyboardEvent) => { - const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace("px", "")); - const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace("px", "")); - if (e.key === "Enter") { + const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', '')); + const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', '')); + if (e.key === 'Enter') { const nextEq = Docs.Create.EquationDocument({ - title: "# math", text: StrCast(this.dataDoc.text), _width, _height: 25, - x: NumCast(this.layoutDoc.x), y: NumCast(this.layoutDoc.y) + _height + 10 + title: '# math', + text: StrCast(this.dataDoc.text), + _width, + _height: 25, + x: NumCast(this.layoutDoc.x), + y: NumCast(this.layoutDoc.y) + _height + 10, }); EquationBox.SelectOnLoad = nextEq[Id]; this.props.addDocument?.(nextEq); e.stopPropagation(); - } - if (e.key === "Tab") { + if (e.key === 'Tab') { const graph = Docs.Create.FunctionPlotDocument([this.rootDoc], { x: NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym](), y: NumCast(this.layoutDoc.y), - _width: 400, _height: 300, backgroundColor: "white" + _width: 400, + _height: 300, + backgroundColor: 'white', }); this.props.addDocument?.(graph); e.stopPropagation(); } - if (e.key === "Backspace" && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc); - } + if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc); + }; onChange = (str: string) => { this.dataDoc.text = str; const style = this._ref.current && getComputedStyle(this._ref.current.element.current); if (style) { - const _height = Number(style.height.replace("px", "")); - const _width = Number(style.width.replace("px", "")); + const _height = Number(style.height.replace('px', '')); + const _width = Number(style.width.replace('px', '')); this.layoutDoc._width = Math.max(35, _width); this.layoutDoc._height = Math.max(25, _height); } - } + }; render() { TraceMobx(); - const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); - return (<div className="equationBox-cont" - onPointerDown={e => !e.ctrlKey && e.stopPropagation()} - style={{ - transform: `scale(${scale})`, - width: `${100 / scale}%`, - height: `${100 / scale}%`, - pointerEvents: !this.props.isSelected() ? "none" : undefined, - }} - onKeyDown={e => e.stopPropagation()} - > - <EquationEditor ref={this._ref} - value={this.dataDoc.text || "x"} - spaceBehavesLikeTab={true} - onChange={this.onChange} - autoCommands="pi theta sqrt sum prod alpha beta gamma rho" - autoOperatorNames="sin cos tan" /> - </div>); + const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + return ( + <div + ref={r => { + r instanceof HTMLDivElement && + new ResizeObserver( + action((entries: any) => { + if (entries[0].contentBoxSize[0].inlineSize) { + this.rootDoc._width = entries[0].contentBoxSize[0].inlineSize; + } + }) + ).observe(r); + }} + className="equationBox-cont" + onPointerDown={e => !e.ctrlKey && e.stopPropagation()} + style={{ + transform: `scale(${scale})`, + width: 'fit-content', // `${100 / scale}%`, + height: `${100 / scale}%`, + pointerEvents: !this.props.isSelected() ? 'none' : undefined, + }} + onKeyDown={e => e.stopPropagation()}> + <EquationEditor ref={this._ref} value={this.dataDoc.text || 'x'} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 67cf18d8b..77aaa4441 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,18 +1,17 @@ -import React = require("react"); -import { computed } from "mobx"; -import { observer } from "mobx-react"; -import { DateField } from "../../../fields/DateField"; -import { Doc, Field, FieldResult, Opt } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { WebField } from "../../../fields/URLField"; -import { DocumentView, DocumentViewSharedProps } from "./DocumentView"; -import { ScriptField } from "../../../fields/ScriptField"; -import { RecordingBox } from "./RecordingBox"; +import React = require('react'); +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import { DateField } from '../../../fields/DateField'; +import { Doc, Field, FieldResult, Opt } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { ScriptField } from '../../../fields/ScriptField'; +import { WebField } from '../../../fields/URLField'; +import { DocumentViewSharedProps } from './DocumentView'; // // these properties get assigned through the render() method of the DocumentView when it creates this node. // However, that only happens because the properties are "defined" in the markup for the field view. -// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc. +// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc. // export interface FieldViewProps extends DocumentViewSharedProps { // FieldView specific props that are not part of DocumentView props @@ -23,26 +22,31 @@ export interface FieldViewProps extends DocumentViewSharedProps { isContentActive: (outsideReaction?: boolean) => boolean | undefined; isDocumentActive?: () => boolean; isSelected: (outsideReaction?: boolean) => boolean; - scaling?: () => number; setHeight?: (height: number) => void; - onBrowseClick?: () => (ScriptField | undefined); - onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => (boolean | undefined); + NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps + onBrowseClick?: () => ScriptField | undefined; + onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; + pointerEvents?: () => Opt<string>; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) - pointerEvents?: () => Opt<string>; + // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields + // Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields + backgroundColor?: string; + color?: string; fontSize?: number; height?: number; width?: number; - background?: string; - color?: string; xPadding?: number; yPadding?: number; + noSidebar?: boolean; + dontScale?: boolean; + dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field) } @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />" } @computed @@ -75,23 +79,22 @@ export class FieldView extends React.Component<FieldViewProps> { //} else if (field instanceof DateField) { return <p>{field.date.toLocaleString()}</p>; - } - else if (field instanceof Doc) { - return <p><b>{field.title?.toString()}</b></p>; - } - else if (field instanceof List) { - return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : ""} </div>; + } else if (field instanceof Doc) { + return ( + <p> + <b>{field.title?.toString()}</b> + </p> + ); + } else if (field instanceof List) { + return <div> {field.length ? field.map(f => Field.toString(f)).join(', ') : ''} </div>; } // bcz: this belongs here, but it doesn't render well so taking it out for now else if (field instanceof WebField) { return <p>{Field.toString(field.url.href)}</p>; - } - else if (!(field instanceof Promise)) { + } else if (!(field instanceof Promise)) { return <p>{Field.toString(field)}</p>; - } - else { - return <p> {"Waiting for server..."} </p>; + } else { + return <p> {'Waiting for server...'} </p>; } } - -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index ff04a293c..dc3fc0396 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -116,7 +116,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _loaded = false; componentDidMount() { reaction( - () => DocListCastAsync(this.layoutDoc.data), + () => DocListCastAsync(this.layoutDoc[this.fieldKey]), async activeTabsAsync => { const activeTabs = await activeTabsAsync; activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction)); diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 3ab0a3ff2..15d0f88f6 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -1,4 +1,4 @@ -import functionPlot from "function-plot"; +import functionPlot from 'function-plot'; import { action, computed, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -12,7 +12,6 @@ import { Docs } from '../../documents/Documents'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; - const EquationSchema = createSchema({}); type EquationDocument = makeInterface<[typeof EquationSchema, typeof documentSchema]>; @@ -20,74 +19,83 @@ const EquationDocument = makeInterface(EquationSchema, documentSchema); @observer export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FunctionPlotBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(FunctionPlotBox, fieldKey); + } public static GraphCount = 0; _plot: any; - _plotId = ""; + _plotId = ''; _plotEle: any; constructor(props: any) { super(props); - this._plotId = "graph" + FunctionPlotBox.GraphCount++; + this._plotId = 'graph' + FunctionPlotBox.GraphCount++; } componentDidMount() { this.props.setContentView?.(this); - reaction(() => [DocListCast(this.dataDoc.data).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange], - () => this.createGraph()); + reaction( + () => [DocListCast(this.dataDoc[this.fieldKey]).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange], + () => this.createGraph() + ); } getAnchor = () => { const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc }); anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); return anchor; - } + }; @action scrollFocus = (doc: Doc, smooth: boolean) => { - this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec("number"), Cast(this.dataDoc.xRange, listSpec("number"), [-10, 10])))); - this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec("number"), Cast(this.dataDoc.xRange, listSpec("number"), [-1, 9])))); + this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10])))); + this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9])))); return 0; - } + }; createGraph = (ele?: HTMLDivElement) => { this._plotEle = ele || this._plotEle; const width = this.props.PanelWidth(); const height = this.props.PanelHeight(); - const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, "x^2").replace(/\\frac\{(.*)\}\{(.*)\}/, "($1/$2)"); + const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'); try { this._plot = functionPlot({ - target: "#" + this._plotEle.id, + target: '#' + this._plotEle.id, width, height, - xAxis: { domain: Cast(this.dataDoc.xRange, listSpec("number"), [-10, 10]) }, - yAxis: { domain: Cast(this.dataDoc.xRange, listSpec("number"), [-1, 9]) }, + xAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]) }, + yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) }, grid: true, data: [ { fn, // derivative: { fn: "2 * x", updateOnMouseMove: true } - } - ] + }, + ], }); } catch (e) { console.log(e); } - } + }; @computed get theGraph() { - return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: "absolute", width: "100%", height: "100%" }} - onPointerDown={e => e.stopPropagation()} />; + return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />; } render() { TraceMobx(); - return (<div - style={{ - pointerEvents: !this.isContentActive() ? "all" : undefined, - width: this.props.PanelWidth(), - height: this.props.PanelHeight() - }} - > - {this.theGraph} - <div style={{ - display: this.props.isSelected() ? "none" : undefined, position: "absolute", width: "100%", height: "100%", - pointerEvents: "all" - }} /> - </div>); + return ( + <div + style={{ + pointerEvents: !this.isContentActive() ? 'all' : undefined, + width: this.props.PanelWidth(), + height: this.props.PanelHeight(), + }}> + {this.theGraph} + <div + style={{ + display: this.props.isSelected() ? 'none' : undefined, + position: 'absolute', + width: '100%', + height: '100%', + pointerEvents: 'all', + }} + /> + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index ffa839fcb..9590bcb15 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -148,8 +148,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp cropping.title = 'crop: ' + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); cropping.y = NumCast(this.rootDoc.y); - cropping._width = anchw * (this.props.scaling?.() || 1); - cropping._height = anchh * (this.props.scaling?.() || 1); + cropping._width = anchw * (this.props.NativeDimScaling?.() || 1); + cropping._height = anchh * (this.props.NativeDimScaling?.() || 1); cropping.isLinkButton = undefined; const croppingProto = Doc.GetProto(cropping); croppingProto.annotationOn = undefined; @@ -384,7 +384,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp render() { TraceMobx(); const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this.props.scaling?.() || 1)}px` : borderRad; + const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this.props.NativeDimScaling?.() || 1)}px` : borderRad; return ( <div className="imageBox" @@ -407,7 +407,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp PanelHeight={this.props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} select={emptyFunction} - scaling={returnOne} + NativeDimScaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} @@ -420,7 +420,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp rootDoc={this.rootDoc} scrollTop={0} down={this._marqueeing} - scaling={this.props.scaling} + scaling={this.props.NativeDimScaling} docView={this.props.docViewPath().slice(-1)[0]} addDocument={this.addDocument} finishMarquee={this.finishMarquee} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 4b1fbaf7d..d9f46509e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,34 +1,37 @@ - -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Field, FieldResult } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { RichTextField } from "../../../fields/RichTextField"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, FieldValue, NumCast } from "../../../fields/Types"; -import { ImageField } from "../../../fields/URLField"; -import { Docs } from "../../documents/Documents"; -import { SetupDrag } from "../../util/DragManager"; -import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting"; -import { undoBatch } from "../../util/UndoManager"; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, Field, FieldResult } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { Cast, DocCast, FieldValue, NumCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { Docs } from '../../documents/Documents'; +import { SetupDrag } from '../../util/DragManager'; +import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting'; +import { undoBatch } from '../../util/UndoManager'; import { FieldView, FieldViewProps } from './FieldView'; -import "./KeyValueBox.scss"; -import { KeyValuePair } from "./KeyValuePair"; -import React = require("react"); -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import e = require("express"); +import './KeyValueBox.scss'; +import { KeyValuePair } from './KeyValuePair'; +import React = require('react'); +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import e = require('express'); +import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { ImageBox } from './ImageBox'; export type KVPScript = { script: CompiledScript; - type: "computed" | "script" | false; + type: 'computed' | 'script' | false; onDelegate: boolean; }; @observer export class KeyValueBox extends React.Component<FieldViewProps> { - public static LayoutString(fieldStr: string) { return FieldView.LayoutString(KeyValueBox, fieldStr); } + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(KeyValueBox, fieldStr); + } private _mainCont = React.createRef<HTMLDivElement>(); private _keyHeader = React.createRef<HTMLTableHeaderCellElement>(); @@ -37,8 +40,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @observable private rows: KeyValuePair[] = []; - @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } - get fieldDocToLayout() { return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; } + @computed get splitPercentage() { + return NumCast(this.props.Document.schemaSplitPercentage, 50); + } + get fieldDocToLayout() { + return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; + } @action onEnterKey = (e: React.KeyboardEvent): void => { @@ -46,19 +53,19 @@ export class KeyValueBox extends React.Component<FieldViewProps> { e.stopPropagation(); if (this._keyInput.current?.value && this._valInput.current?.value && this.fieldDocToLayout) { if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput.current.value, this._valInput.current.value)) { - this._keyInput.current.value = ""; - this._valInput.current.value = ""; + this._keyInput.current.value = ''; + this._valInput.current.value = ''; document.body.focus(); } } } - } + }; public static CompileKVPScript(value: string): KVPScript | undefined { - const eq = value.startsWith("="); + const eq = value.startsWith('='); value = eq ? value.substr(1) : value; - const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false; + const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith(';=') ? 'script' : false; value = dubEq ? value.substr(2) : value; - const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: "any", _readOnly_: "boolean" }, editable: false }; + const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: false }; if (dubEq) options.typecheck = false; const script = CompileScript(value, options); return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; @@ -67,11 +74,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = forceOnDelegate || onDelegate || key.startsWith("_") ? doc : doc.proto || doc; + const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : doc.proto || doc; let field: Field; - if (type === "computed") { + if (type === 'computed') { field = new ComputedField(script); - } else if (type === "script") { + } else if (type === 'script') { field = new ScriptField(script); } else { const res = script.run({ this: target }, console.log); @@ -96,7 +103,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { if (e.buttons === 1 && this.props.isSelected(true)) { e.stopPropagation(); } - } + }; onPointerWheel = (e: React.WheelEvent): void => e.stopPropagation(); rowHeight = () => 30; @@ -104,7 +111,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @computed get createTable() { const doc = this.fieldDocToLayout; if (!doc) { - return <tr><td>Loading...</td></tr>; + return ( + <tr> + <td>Loading...</td> + </tr> + ); } const realDoc = doc; @@ -122,83 +133,102 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let i = 0; const self = this; for (const key of Object.keys(ids).slice().sort()) { - rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} PanelWidth={this.props.PanelWidth} PanelHeight={this.rowHeight} - ref={(function () { - let oldEl: KeyValuePair | undefined; - return (el: KeyValuePair) => { - if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); - oldEl = el; - if (el) self.rows.push(el); - }; - })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); + rows.push( + <KeyValuePair + doc={realDoc} + addDocTab={this.props.addDocTab} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.rowHeight} + ref={(function () { + let oldEl: KeyValuePair | undefined; + return (el: KeyValuePair) => { + if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); + oldEl = el; + if (el) self.rows.push(el); + }; + })()} + keyWidth={100 - this.splitPercentage} + rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')} + key={key} + keyName={key} + /> + ); } return rows; } @computed get newKeyValue() { - return <tr className="keyValueBox-valueRow"> - <td className="keyValueBox-td-key" onClick={(e) => { this._keyInput.current!.select(); e.stopPropagation(); }} style={{ width: `${100 - this.splitPercentage}%` }}> - <input style={{ width: "100%" }} ref={this._keyInput} type="text" placeholder="Key" /> - </td> - <td className="keyValueBox-td-value" onClick={(e) => { this._valInput.current!.select(); e.stopPropagation(); }} style={{ width: `${this.splitPercentage}%` }}> - <input style={{ width: "100%" }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} /> - </td> - </tr>; + return ( + <tr className="keyValueBox-valueRow"> + <td + className="keyValueBox-td-key" + onClick={e => { + this._keyInput.current!.select(); + e.stopPropagation(); + }} + style={{ width: `${100 - this.splitPercentage}%` }}> + <input style={{ width: '100%' }} ref={this._keyInput} type="text" placeholder="Key" /> + </td> + <td + className="keyValueBox-td-value" + onClick={e => { + this._valInput.current!.select(); + e.stopPropagation(); + }} + style={{ width: `${this.splitPercentage}%` }}> + <input style={{ width: '100%' }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} /> + </td> + </tr> + ); } @action onDividerMove = (e: PointerEvent): void => { const nativeWidth = this._mainCont.current!.getBoundingClientRect(); - this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); - } + this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100)); + }; @action onDividerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointermove', this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); - } + }; onDividerDown = (e: React.PointerEvent) => { e.stopPropagation(); e.preventDefault(); - document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointermove', this.onDividerMove); document.addEventListener('pointerup', this.onDividerUp); - } + }; - getTemplate = async () => { - const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template", _chromeHidden: true }); - parent._columnWidth = 100; - for (const row of this.rows.filter(row => row.isChecked)) { - await this.createTemplateField(parent, row); - row.uncheck(); + getFieldView = async () => { + const rows = this.rows.filter(row => row.isChecked); + if (rows.length > 1) { + const parent = Docs.Create.StackingDocument([], { _autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true }); + for (const row of rows) { + const field = this.createFieldView(DocCast(this.props.Document.data), row); + field && Doc.AddDocToList(parent, 'data', field); + row.uncheck(); + } + return parent; } - return parent; - } + return this.createFieldView(DocCast(this.props.Document.data), rows.lastElement()); + }; - createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => { + createFieldView = (templateDoc: Doc, row: KeyValuePair) => { const metaKey = row.props.keyName; - const sourceDoc = await Cast(this.props.Document.data, Doc); - if (!sourceDoc) { - return; - } + const fieldTemplate = Doc.MakeAlias(templateDoc); + fieldTemplate.title = metaKey; + fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey); + return fieldTemplate; + }; - const fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey); - if (!fieldTemplate) { - return; - } - const previousViewType = fieldTemplate._viewType; - Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc)); - previousViewType && (fieldTemplate._viewType = previousViewType); - - Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate); - } - - inferType = async (data: FieldResult, metaKey: string) => { + inferType = (data: FieldResult, metaKey: string) => { const options = { _width: 300, _height: 300, title: metaKey }; - if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") { - return Docs.Create.TextDocument("", options); + if (data instanceof RichTextField || typeof data === 'string' || typeof data === 'number') { + return FormattedTextBox.LayoutString(metaKey); } else if (data instanceof List) { if (data.length === 0) { return Docs.Create.StackingDocument([], options); } - const first = await Cast(data[0], Doc); + const first = DocCast(data[0]); if (!first || !first.data) { return Docs.Create.StackingDocument([], options); } @@ -212,44 +242,52 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return undefined; } } else if (data instanceof ImageField) { - return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options); + return ImageBox.LayoutString(metaKey); } - return new Doc; - } + return new Doc(); + }; specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - const open = cm.findByDescription("Change Perspective..."); - const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : []; + const open = cm.findByDescription('Change Perspective...'); + const openItems: ContextMenuProps[] = open && 'subitems' in open ? open.subitems : []; openItems.push({ - description: "Default Perspective", event: () => { - this.props.addDocTab(this.props.Document, "close"); - this.props.addDocTab(this.fieldDocToLayout, "add:right"); - }, icon: "image" + description: 'Default Perspective', + event: () => { + this.props.addDocTab(this.props.Document, 'close'); + this.props.addDocTab(this.fieldDocToLayout, 'add:right'); + }, + icon: 'image', }); - !open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" }); - } + !open && cm.addItem({ description: 'Change Perspective...', subitems: openItems, icon: 'external-link-alt' }); + }; render() { - const dividerDragger = this.splitPercentage === 0 ? (null) : - <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> - <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> - </div>; - - return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}> - <table className="keyValueBox-table"> - <tbody className="keyValueBox-tbody"> - <tr className="keyValueBox-header"> - <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} - onPointerDown={SetupDrag(this._keyHeader, this.getTemplate)} - >Key</th> - <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th> - </tr> - {this.createTable} - {this.newKeyValue} - </tbody> - </table> - {dividerDragger} - </div>); + const dividerDragger = + this.splitPercentage === 0 ? null : ( + <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> + <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> + </div> + ); + + return ( + <div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}> + <table className="keyValueBox-table"> + <tbody className="keyValueBox-tbody"> + <tr className="keyValueBox-header"> + <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}> + Key + </th> + <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}> + Fields + </th> + </tr> + {this.createTable} + {this.newKeyValue} + </tbody> + </table> + {dividerDragger} + </div> + ); } } diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 85a8622ec..5102eae51 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -136,7 +136,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className={`linkAnchorBox-cont${small ? '-small' : ''}`} - onPointerLeave={LinkDocPreview.Clear} + //onPointerLeave={} //LinkDocPreview.Clear} onPointerEnter={e => LinkDocPreview.SetLinkInfo({ docProps: this.props, diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index ccac66996..91bd505c5 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -1,26 +1,24 @@ -import React = require("react"); -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; -import { LinkManager } from "../../util/LinkManager"; -import "./LinkDescriptionPopup.scss"; -import { TaskCompletionBox } from "./TaskCompletedBox"; - +import React = require('react'); +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../fields/Doc'; +import { LinkManager } from '../../util/LinkManager'; +import './LinkDescriptionPopup.scss'; +import { TaskCompletionBox } from './TaskCompletedBox'; @observer export class LinkDescriptionPopup extends React.Component<{}> { - @observable public static descriptionPopup: boolean = false; - @observable public static showDescriptions: string = "ON"; + @observable public static showDescriptions: string = 'ON'; @observable public static popupX: number = 700; @observable public static popupY: number = 350; - @observable description: string = ""; + @observable description: string = ''; @observable popupRef = React.createRef<HTMLDivElement>(); @action descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.currentTarget.value; - } + }; @action onDismiss = (add: boolean) => { @@ -28,7 +26,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { if (add) { LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description); } - } + }; @action onClick = (e: PointerEvent) => { @@ -36,35 +34,43 @@ export class LinkDescriptionPopup extends React.Component<{}> { LinkDescriptionPopup.descriptionPopup = false; TaskCompletionBox.taskCompleted = false; } - } + }; @action componentDidMount() { - document.addEventListener("pointerdown", this.onClick); + document.addEventListener('pointerdown', this.onClick, true); } componentWillUnmount() { - document.removeEventListener("pointerdown", this.onClick); + document.removeEventListener('pointerdown', this.onClick, true); } render() { - return <div className="linkDescriptionPopup" ref={this.popupRef} - style={{ - left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700, - top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350, - }}> - <input className="linkDescriptionPopup-input" - onKeyDown={e => e.stopPropagation()} - onKeyPress={e => e.key === "Enter" && this.onDismiss(true)} - placeholder={"(Optional) Enter link description..."} - onChange={(e) => this.descriptionChanged(e)}> - </input> - <div className="linkDescriptionPopup-btn"> - <div className="linkDescriptionPopup-btn-dismiss" - onPointerDown={e => this.onDismiss(false)}> Dismiss </div> - <div className="linkDescriptionPopup-btn-add" - onPointerDown={e => this.onDismiss(true)}> Add </div> + return ( + <div + className="linkDescriptionPopup" + ref={this.popupRef} + style={{ + left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700, + top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350, + }}> + <input + className="linkDescriptionPopup-input" + onKeyDown={e => e.stopPropagation()} + onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)} + placeholder={'(Optional) Enter link description...'} + onChange={e => this.descriptionChanged(e)}></input> + <div className="linkDescriptionPopup-btn"> + <div className="linkDescriptionPopup-btn-dismiss" onPointerDown={e => this.onDismiss(false)}> + {' '} + Dismiss{' '} + </div> + <div className="linkDescriptionPopup-btn-add" onPointerDown={e => this.onDismiss(true)}> + {' '} + Add{' '} + </div> + </div> </div> - </div>; + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss index 3febbcecb..c68e55f73 100644 --- a/src/client/views/nodes/LinkDocPreview.scss +++ b/src/client/views/nodes/LinkDocPreview.scss @@ -8,6 +8,7 @@ border-bottom: 8px solid white; border-right: 8px solid white; z-index: 2004; + cursor: pointer; .linkDocPreview-inner { background-color: white; @@ -63,13 +64,16 @@ cursor: pointer; } } + } - .linkDocPreview-preview-wrapper { - overflow: hidden; - align-content: center; - justify-content: center; - background-color: rgb(160, 160, 160); - } + .linkDocPreview-preview-wrapper { + overflow: hidden; + align-content: center; + justify-content: center; + pointer-events: all; + background-color: rgb(160, 160, 160); + overflow: auto; + cursor: grab; } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index c2fb5d4ec..93ca22d5d 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -3,20 +3,21 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import wiki from 'wikijs'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from '../../../fields/Doc'; -import { NumCast, StrCast, Cast } from '../../../fields/Types'; -import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils'; +import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager } from '../../util/DragManager'; +import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { DocumentView, DocumentViewSharedProps } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); -import { DocumentType } from '../../documents/DocumentTypes'; -import { DragManager } from '../../util/DragManager'; -import { LinkFollower } from '../../util/LinkFollower'; +import { LinkEditor } from '../linking/LinkEditor'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -36,6 +37,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } _infoRef = React.createRef<HTMLDivElement>(); + _linkDocRef = React.createRef<HTMLDivElement>(); @observable public static LinkInfo: Opt<LinkDocPreviewProps>; @observable _targetDoc: Opt<Doc>; @observable _linkDoc: Opt<Doc>; @@ -65,16 +67,16 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } componentDidMount() { this.init(); - document.addEventListener('pointerdown', this.onPointerDown); + document.addEventListener('pointerdown', this.onPointerDown, true); } componentWillUnmount() { LinkDocPreview.SetLinkInfo(undefined); - document.removeEventListener('pointerdown', this.onPointerDown); + document.removeEventListener('pointerdown', this.onPointerDown, true); } onPointerDown = (e: PointerEvent) => { - !this._infoRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview + !this._linkDocRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview }; @computed get href() { @@ -117,13 +119,15 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } return undefined; } - deleteLink = (e: React.PointerEvent) => { + @observable _showEditor = false; + editLink = (e: React.PointerEvent): void => { + LinkManager.currentLink = this.props.linkDoc; setupMoveUpEvents( this, e, returnFalse, emptyFunction, - undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc)) + action(() => (this._showEditor = !this._showEditor)) ); }; nextHref = (e: React.PointerEvent) => { @@ -143,7 +147,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { ); }; - followLink = (e: React.PointerEvent) => { + followLink = () => { if (this._linkDoc && this._linkSrc) { LinkDocPreview.Clear(); LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); @@ -151,6 +155,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right'); } }; + + followLinkPointerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.followLink); + width = () => { if (!this._targetDoc) return 225; if (this._targetDoc[WidthSym]() < this._targetDoc?.[HeightSym]()) { @@ -167,15 +174,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { }; @computed get previewHeader() { return !this._linkDoc || !this._targetDoc || !this._linkSrc ? null : ( - <div className="linkDocPreview-info" ref={this._infoRef}> - <div - className="linkDocPreview-title" - style={{ pointerEvents: 'all' }} - onPointerDown={e => { - DragManager.StartDocumentDrag([this._infoRef.current!], new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY); - e.stopPropagation(); - LinkDocPreview.Clear(); - }}> + <div className="linkDocPreview-info"> + <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}> {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + '...' : StrCast(this._targetDoc.title)} <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p> </div> @@ -186,9 +186,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">Delete Link</div>} placement="top"> - <div className="linkDocPreview-button" onPointerDown={this.deleteLink}> - <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="trash" color="white" size="sm" /> + <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top"> + <div className="linkDocPreview-button" onPointerDown={this.editLink}> + <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" /> </div> </Tooltip> </div> @@ -201,7 +201,27 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : ( <div className="linkDocPreview-inner"> {!this.props.showHeader ? null : this.previewHeader} - <div className="linkDocPreview-preview-wrapper" style={{ maxHeight: this._toolTipText ? '100%' : undefined, overflow: 'auto' }}> + <div + className="linkDocPreview-preview-wrapper" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + (e, down, delta) => { + if (Math.abs(e.clientX - down[0]) + Math.abs(e.clientY - down[1]) > 100) { + DragManager.StartDocumentDrag([this._infoRef.current!], new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY); + LinkDocPreview.Clear(); + return true; + } + return false; + }, + emptyFunction, + this.followLink, + true + ) + } + ref={this._infoRef} + style={{ maxHeight: this._toolTipText ? '100%' : undefined }}> {this._toolTipText ? ( this._toolTipText ) : ( @@ -217,8 +237,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { docViewPath={returnEmptyDoclist} ScreenToLocalTransform={Transform.Identity} isDocumentActive={returnFalse} - isContentActive={emptyFunction} + isContentActive={returnFalse} addDocument={returnFalse} + showTitle={returnEmptyString} removeDocument={returnFalse} addDocTab={returnFalse} pinToPres={returnFalse} @@ -232,6 +253,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { suppressSetHeight={true} PanelWidth={this.width} PanelHeight={this.height} + pointerEvents={returnNone} focus={DocUtils.DefaultFocus} whenChildContentsActiveChanged={returnFalse} ignoreAutoHeight={true} // need to ignore autoHeight otherwise autoHeight text boxes will expand beyond the preview panel size. @@ -248,8 +270,13 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { render() { const borders = 16; // 8px border on each side return ( - <div className="linkDocPreview" onPointerDown={this.followLink} style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> - {this.docPreview} + <div + className="linkDocPreview" + ref={this._linkDocRef} + onPointerDown={this.followLinkPointerDown} + style={{ left: this.props.location[0], top: this.props.location[1], width: this._showEditor ? 'auto' : this.width() + borders, height: this._showEditor ? 'max-content' : this.height() + borders + (this.props.showHeader ? 37 : 0) }}> + {this._showEditor ? null : this.docPreview} + {!this._showEditor || !this._linkSrc || !this._linkDoc ? null : <LinkEditor sourceDoc={this._linkSrc} linkDoc={this._linkDoc} showLinks={action(() => (this._showEditor = !this._showEditor))} />} </div> ); } diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss new file mode 100644 index 000000000..d63ed2575 --- /dev/null +++ b/src/client/views/nodes/LoadingBox.scss @@ -0,0 +1,35 @@ +.loadingBoxContainer { + display: flex; + flex-direction: column; + align-content: center; + justify-content: center; + background-color: #fdfdfd; + height: 100%; + align-items: center; +} + +.textContainer { + margin: 5px; +} + +.textContainer { + justify-content: center; + align-content: center; +} + +.textContainer, +.text { + overflow: hidden; + text-overflow: ellipsis; + max-width: 80%; + text-align: center; +} + +.headerText { + text-align: center; + font-weight: bold; +} + +.spinner { + text-align: center; +} diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx new file mode 100644 index 000000000..acf728ebb --- /dev/null +++ b/src/client/views/nodes/LoadingBox.tsx @@ -0,0 +1,47 @@ +import { observer } from 'mobx-react'; +import * as React from 'react'; +import ReactLoading from 'react-loading'; +import { StrCast } from '../../../fields/Types'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import { FieldView, FieldViewProps } from './FieldView'; +import './LoadingBox.scss'; + +/** + * LoadingBox Class represents a placeholder doc for documents that are currently + * being uploaded to the server and being fetched by the client. The LoadingBox doc is then used to + * generate the actual type of doc that is required once the document has been successfully uploaded. + * + * Design considerations: + * We are using the docToFiles map in Documents to keep track of all files being uploaded in one session of the client. + * If the file is not found we assume an error has occurred with the file upload, e.g. it has been interrupted by a client refresh + * or network issues. The docToFiles essentially gets reset everytime the page is refreshed. + * + * TODOs: + * 1) ability to query server to retrieve files that already exist if users upload duplicate files. + * 2) ability to restart upload if there is an error + * 3) detect network error and notify the user + * 4 )if file upload gets interrupted, it still gets uploaded to the server if there are no network interruptions which leads to unused space. this could be + * handled with (1) + * 5) Fixing the stacking view bug + * 6) Fixing the CSS + * + * @author naafiyan + */ +@observer +export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(LoadingBox, fieldKey); + } + + render() { + return ( + <div className="loadingBoxContainer" style={{ background: this.rootDoc.isLoading ? '' : 'red' }}> + <div className="textContainer"> + <p className="headerText">{this.rootDoc.isLoading ? 'Loading (can take several minutes):' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}</p> + <span className="text">{StrCast(this.rootDoc.title)}</span> + {!this.rootDoc.isLoading ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />} + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index bf4c029b2..6479e933e 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,17 +1,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; -import { action, computed, IReactionDisposer, observable, ObservableMap } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { NumCast, StrCast } from '../../../../fields/Types'; -import { TraceMobx } from '../../../../fields/util'; import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; +import { UndoManager } from '../../../util/UndoManager'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; @@ -19,7 +19,6 @@ import { MarqueeAnnotator } from '../../MarqueeAnnotator'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { Annotation } from '../../pdf/Annotation'; import { SidebarAnnos } from '../../SidebarAnnos'; -import { StyleProp } from '../../StyleProvider'; import { FieldView, FieldViewProps } from '../FieldView'; import './MapBox.scss'; import { MapBoxInfoWindow } from './MapBoxInfoWindow'; @@ -368,23 +367,27 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setupMoveUpEvents( this, e, - (e, down, delta) => { - const localDelta = this.props - .ScreenToLocalTransform() - .scale(this.props.scaling?.() || 1) - .transformDirection(delta[0], delta[1]); - const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); - const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - const ratio = (curNativeWidth + localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth; - if (ratio >= 1) { - this.layoutDoc.nativeWidth = nativeWidth * ratio; - this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]; - this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; - } - return false; - }, + (e, down, delta) => + runInAction(() => { + const localDelta = this.props + .ScreenToLocalTransform() + .scale(this.props.NativeDimScaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const fullWidth = this.layoutDoc[WidthSym](); + const mapWidth = fullWidth - this.sidebarWidth(); + if (this.sidebarWidth() + localDelta[0] > 0) { + this._showSidebar = true; + this.layoutDoc._width = fullWidth + localDelta[0]; + this.layoutDoc._sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; + } else { + this._showSidebar = false; + this.layoutDoc._width = mapWidth; + this.layoutDoc._sidebarWidthPercent = '0%'; + } + return false; + }), emptyFunction, - this.toggleSidebar + () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') ); }; @@ -448,21 +451,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * Handles toggle of sidebar on click the little comment button */ @computed get sidebarHandle() { - TraceMobx(); - const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; - const color = !annotated ? Colors.WHITE : Colors.BLACK; - const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ':annotated' : '')); - return !annotated ? null : ( + return ( <div - className="formattedTextBox-sidebar-handle" - onPointerDown={this.sidebarDown} + className="mapBox-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" style={{ - left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`, - backgroundColor: backgroundColor, - color: color, - opacity: annotated ? 1 : undefined, - }}> - <FontAwesomeIcon icon={'comment-alt'} /> + display: !this.props.isContentActive() ? 'none' : undefined, + top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={this.sidebarBtnDown}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> </div> ); } @@ -470,13 +470,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // TODO: Adding highlight box layer to Maps @action toggleSidebar = () => { + //1.2 * w * ? = .2 * w .2/1.2 const prevWidth = this.sidebarWidth(); - this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; - this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; + this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; sidebarDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false); + setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const bounds = this._ref.current!.getBoundingClientRect(); @@ -553,8 +554,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // } }; - panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); - panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); + panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); + panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; @@ -583,13 +584,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // docFilters={docFilters || this.props.docFilters} // dontRenderDocuments={docFilters ? false : true} // select={emptyFunction} - // ContentScaling={returnOne} // bringToFront={emptyFunction} // whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} // removeDocument={this.removeDocument} // moveDocument={this.moveDocument} // addDocument={this.sidebarAddDocument} - // childPointerEvents={true} + // childPointerEvents={"all"} // pointerEvents={Doc.ActiveTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; return ( <div className="mapBox" ref={this._ref}> @@ -605,7 +605,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps {this.annotationLayer} <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> - <input className="mapBox-input" ref={this.inputRef} type="text" placeholder="Enter location" /> + <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> </Autocomplete> {this.renderMarkers()} @@ -643,7 +643,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps )} </div> {/* </LoadScript > */} - <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} @@ -660,18 +660,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.sidebarRemoveDocument} /> </div> - <div - className="mapBox-overlayButton-sidebar" - key="sidebar" - title="Toggle Sidebar" - style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, - backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, - }} - onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> - </div> + {this.sidebarHandle} </div> ); } diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 72569135b..00bedafbe 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -1,5 +1,5 @@ import { InfoWindow } from '@react-google-maps/api'; -import { action, computed } from 'mobx'; +import { action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; @@ -7,6 +7,7 @@ import { Id } from '../../../../fields/FieldSymbols'; import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; import { ViewBoxAnnotatableProps } from '../../DocComponent'; import { FieldViewProps } from '../FieldView'; @@ -40,7 +41,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi }); }; - _stack: CollectionStackingView | null | undefined; + _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined; childFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); @@ -62,7 +63,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi setHeight={emptyFunction} isAnnotationOverlay={false} select={emptyFunction} - scaling={returnOne} + NativeDimScaling={returnOne} isContentActive={returnTrue} chromeHidden={true} rootSelected={returnFalse} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 5b9d90780..345407c2f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -8,17 +8,17 @@ import { Id } from '../../../fields/FieldSymbols'; import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { Colors } from '../global/globalEnums'; import { CreateImage } from '../nodes/WebBoxRenderer'; -import { AnchorMenu } from '../pdf/AnchorMenu'; import { PDFViewer } from '../pdf/PDFViewer'; import { SidebarAnnos } from '../SidebarAnnos'; import { FieldView, FieldViewProps } from './FieldView'; @@ -26,7 +26,6 @@ import { ImageBox } from './ImageBox'; import './PDFBox.scss'; import { VideoBox } from './VideoBox'; import React = require('react'); -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; @observer export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { @@ -105,8 +104,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); - const anchw = cropping[WidthSym]() * (this.props.scaling?.() || 1); - const anchh = cropping[HeightSym]() * (this.props.scaling?.() || 1); + const anchw = cropping[WidthSym]() * (this.props.NativeDimScaling?.() || 1); + const anchh = cropping[HeightSym]() * (this.props.NativeDimScaling?.() || 1); const viewScale = 1; cropping.title = 'crop: ' + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); @@ -291,11 +290,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps (e, down, delta) => { const localDelta = this.props .ScreenToLocalTransform() - .scale(this.props.scaling?.() || 1) + .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.scaling?.() || 1)) / nativeWidth; + const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); @@ -303,8 +302,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } return false; }, - () => batch.end(), - () => this.toggleSidebar() + (e, movement, isClick) => !isClick && batch.end(), + () => { + this.toggleSidebar(); + batch.end(); + } ); }; @observable _previewNativeWidth: Opt<number> = undefined; @@ -388,18 +390,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps /> {this._pageControls ? pageBtns : null} </div> - <div - className="pdfBox-sidebarBtn" - key="sidebar" - title="Toggle Sidebar" - style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, - backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, - }} - onPointerDown={e => this.sidebarBtnDown(e, true)}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> - </div> + {this.sidebarHandle} </div> ); } @@ -430,13 +421,28 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } + @computed get sidebarHandle() { + return ( + <div + className="pdfBox-sidebarBtn" + key="sidebar" + title="Toggle Sidebar" + style={{ + display: !this.props.isContentActive() ? 'none' : undefined, + top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={e => this.sidebarBtnDown(e, true)}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + </div> + ); + } - contentScaling = () => 1; isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected(); @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; - const scale = previewScale * (this.props.scaling?.() || 1); + const scale = previewScale * (this.props.NativeDimScaling?.() || 1); return ( <div className={'pdfBox'} @@ -471,23 +477,24 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.removeDocument} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} crop={this.crop} - ContentScaling={returnOne} /> </div> - <SidebarAnnos - ref={this._sidebarRef} - {...this.props} - rootDoc={this.rootDoc} - layoutDoc={this.layoutDoc} - dataDoc={this.dataDoc} - setHeight={emptyFunction} - nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} - showSidebar={this.SidebarShown} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - sidebarAddDocument={this.sidebarAddDocument} - moveDocument={this.moveDocument} - removeDocument={this.removeDocument} - /> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}> + <SidebarAnnos + ref={this._sidebarRef} + {...this.props} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + setHeight={emptyFunction} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} + showSidebar={this.SidebarShown} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + /> + </div> {this.settingsPanel()} </div> ); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 942072524..76a24d831 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -295,7 +295,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl isAnnotationOverlay={true} select={emptyFunction} isContentActive={returnFalse} - scaling={returnOne} + NativeDimScaling={returnOne} whenChildContentsActiveChanged={emptyFunction} removeDocument={returnFalse} moveDocument={returnFalse} @@ -317,7 +317,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl isAnnotationOverlay={true} select={emptyFunction} isContentActive={emptyFunction} - scaling={returnOne} + NativeDimScaling={returnOne} xPadding={25} yPadding={10} whenChildContentsActiveChanged={emptyFunction} diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 05ff40f22..1c9b0bc0e 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -6,7 +6,7 @@ import { Doc } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; @@ -60,6 +60,14 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable constructor(props: any) { super(props); + if (!this.compileParams.length) { + const params = ScriptCast(this.rootDoc[this.props.fieldKey])?.script.options.params as { [key: string]: any }; + if (params) { + this.compileParams = Array.from(Object.keys(params)) + .filter(p => !p.startsWith('_')) + .map(key => key + ':' + params[key]); + } + } } // vars included in fields that store parameters types and names and the script itself @@ -70,30 +78,30 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable return this.compileParams.map(p => p.split(':')[1].trim()); } @computed({ keepAlive: true }) get rawScript() { - return StrCast(this.dataDoc[this.props.fieldKey + '-rawScript'], ''); + return ScriptCast(this.rootDoc[this.fieldKey])?.script.originalScript ?? ''; } @computed({ keepAlive: true }) get functionName() { - return StrCast(this.dataDoc[this.props.fieldKey + '-functionName'], ''); + return StrCast(this.rootDoc[this.props.fieldKey + '-functionName'], ''); } @computed({ keepAlive: true }) get functionDescription() { - return StrCast(this.dataDoc[this.props.fieldKey + '-functionDescription'], ''); + return StrCast(this.rootDoc[this.props.fieldKey + '-functionDescription'], ''); } @computed({ keepAlive: true }) get compileParams() { - return Cast(this.dataDoc[this.props.fieldKey + '-params'], listSpec('string'), []); + return Cast(this.rootDoc[this.props.fieldKey + '-params'], listSpec('string'), []); } set rawScript(value) { - this.dataDoc[this.props.fieldKey + '-rawScript'] = value; + Doc.SetInPlace(this.rootDoc, this.props.fieldKey, new ScriptField(undefined, undefined, value), true); } set functionName(value) { - this.dataDoc[this.props.fieldKey + '-functionName'] = value; + Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionName', value, true); } set functionDescription(value) { - this.dataDoc[this.props.fieldKey + '-functionDescription'] = value; + Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionDescription', value, true); } set compileParams(value) { - this.dataDoc[this.props.fieldKey + '-params'] = new List<string>(value); + Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-params', new List<string>(value), true); } getValue(result: any, descrip: boolean) { @@ -107,8 +115,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable @action componentDidMount() { - this.rawScript = ScriptCast(this.dataDoc[this.props.fieldKey])?.script?.originalScript ?? this.rawScript; - + this.rawText = this.rawScript; const observer = new _global.ResizeObserver( action((entries: any) => { const area = document.querySelector('textarea'); @@ -171,13 +178,13 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const params: ScriptParam = {}; this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim())); - const result = CompileScript(this.rawScript, { + const result = CompileScript(this.rawText, { editable: true, transformer: DocumentIconContainer.getTransformer(), params, typecheck: false, }); - this.dataDoc[this.fieldKey] = result.compiled ? new ScriptField(result) : undefined; + Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true); this.onError(result.compiled ? undefined : result.errors); return result.compiled; }; @@ -187,9 +194,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable onRun = () => { if (this.onCompile()) { const bindings: { [name: string]: any } = {}; - this.paramsNames.forEach(key => (bindings[key] = this.dataDoc[key])); + this.paramsNames.forEach(key => (bindings[key] = this.rootDoc[key])); // binds vars so user doesnt have to refer to everything as self.<var> - ScriptCast(this.dataDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); + ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); } }; @@ -257,14 +264,14 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // sets field of the corresponding field key (param name) to be dropped document @action onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => { - this.dataDoc[fieldKey] = de.complete.docDragData?.droppedDocuments[0]; + Doc.SetInPlace(this.rootDoc, fieldKey, de.complete.docDragData?.droppedDocuments[0], true); e.stopPropagation(); }; // deletes a param from all areas in which it is stored @action onDelete = (num: number) => { - this.dataDoc[this.paramsNames[num]] = undefined; + Doc.SetInPlace(this.rootDoc, this.paramsNames[num], undefined, true); this.compileParams.splice(num, 1); return true; }; @@ -274,7 +281,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable viewChanged = (e: React.ChangeEvent, name: string) => { //@ts-ignore const val = e.target.selectedOptions[0].value; - this.dataDoc[name] = val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true'; + Doc.SetInPlace(this.rootDoc, name, val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true', true); }; // creates a copy of the script document @@ -330,8 +337,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable maxHeight={72} height={35} fontSize={14} - contents={this.dataDoc[parameter]?.title ?? 'undefined'} - GetValue={() => this.dataDoc[parameter]?.title ?? 'undefined'} + contents={StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')} + GetValue={() => StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')} SetValue={action((value: string) => { const script = CompileScript(value, { addReturn: true, @@ -341,7 +348,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const results = script.compiled && script.run(); if (results && results.success) { this._errorMessage = ''; - this.dataDoc[parameter] = results.result; + Doc.SetInPlace(this.rootDoc, parameter, results.result, true); return true; } this._errorMessage = 'invalid document'; @@ -354,7 +361,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // rendering when a string's value can be set in applied UI renderBasicType(parameter: string, isNum: boolean) { - const strVal = isNum ? NumCast(this.dataDoc[parameter]).toString() : StrCast(this.dataDoc[parameter]); + const strVal = isNum ? NumCast(this.rootDoc[parameter]).toString() : StrCast(this.rootDoc[parameter]); return ( <div className="scriptingBox-paramInputs" style={{ overflowY: 'hidden' }}> <EditableView @@ -368,7 +375,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const setValue = isNum ? parseInt(value) : value; if (setValue !== undefined && setValue !== ' ') { this._errorMessage = ''; - this.dataDoc[parameter] = setValue; + Doc.SetInPlace(this.rootDoc, parameter, setValue, true); return true; } this._errorMessage = 'invalid input'; @@ -389,7 +396,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable className="scriptingBox-viewPicker" onPointerDown={e => e.stopPropagation()} onChange={e => this.viewChanged(e, parameter)} - value={typeof this.dataDoc[parameter] === 'string' ? 'S' + StrCast(this.dataDoc[parameter]) : typeof this.dataDoc[parameter] === 'number' ? 'N' + NumCast(this.dataDoc[parameter]) : 'B' + BoolCast(this.dataDoc[parameter])}> + value={typeof this.rootDoc[parameter] === 'string' ? 'S' + StrCast(this.rootDoc[parameter]) : typeof this.rootDoc[parameter] === 'number' ? 'N' + NumCast(this.rootDoc[parameter]) : 'B' + BoolCast(this.rootDoc[parameter])}> {types.map(type => ( <option className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}> {' '} @@ -442,7 +449,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable @action handleFunc(pos: number) { - const scriptString = this.rawScript.slice(0, pos - 2); + const scriptString = this.rawText.slice(0, pos - 2); this._currWord = scriptString.split(' ')[scriptString.split(' ').length - 1]; this._suggestions = [StrCast(this._scriptingParams[this._currWord])]; return this._suggestions; @@ -474,7 +481,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable } getSuggestedParams(pos: number) { - const firstScript = this.rawScript.slice(0, pos); + const firstScript = this.rawText.slice(0, pos); const indexP = firstScript.lastIndexOf('.'); const indexS = firstScript.lastIndexOf(' '); const func = firstScript.slice((indexP > indexS ? indexP : indexS) + 1, firstScript.length + 1); @@ -494,8 +501,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable @action keyHandler(e: any, pos: number) { + e.stopPropagation(); if (this._lastChar === 'Enter') { - this.rawScript = this.rawScript + ' '; + this.rawText = this.rawText + ' '; } if (e.key === '(') { this.suggestionPos(); @@ -504,7 +512,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable this._scriptSuggestedParams = this.getSuggestedParams(pos); if (this._scriptParamsText !== undefined && this._scriptParamsText.length > 0) { - if (this.rawScript[pos - 2] !== '(') { + if (this.rawText[pos - 2] !== '(') { this._paramSuggestion = true; } } @@ -515,22 +523,22 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable if (this._lastChar === '(') { this._paramSuggestion = false; } else if (this._lastChar === ')') { - if (this.rawScript.slice(0, this.rawScript.length - 1).split('(').length - 1 > this.rawScript.slice(0, this.rawScript.length - 1).split(')').length - 1) { + if (this.rawText.slice(0, this.rawText.length - 1).split('(').length - 1 > this.rawText.slice(0, this.rawText.length - 1).split(')').length - 1) { if (this._scriptParamsText.length > 0) { this._paramSuggestion = true; } } } - } else if (this.rawScript.split('(').length - 1 <= this.rawScript.split(')').length - 1) { + } else if (this.rawText.split('(').length - 1 <= this.rawText.split(')').length - 1) { this._paramSuggestion = false; } } - this._lastChar = e.key === 'Backspace' ? this.rawScript[this.rawScript.length - 2] : e.key; + this._lastChar = e.key === 'Backspace' ? this.rawText[this.rawText.length - 2] : e.key; if (this._paramSuggestion) { const parameters = this._scriptParamsText.split(','); - const index = this.rawScript.lastIndexOf('('); - const enteredParams = this.rawScript.slice(index, this.rawScript.length); + const index = this.rawText.lastIndexOf('('); + const enteredParams = this.rawText.slice(index, this.rawText.length); const splitEntered = enteredParams.split(','); const numEntered = splitEntered.length; @@ -564,15 +572,16 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable handlePosChange(number: any) { this._caretPos = number; if (this._caretPos === 0) { - this.rawScript = ' ' + this.rawScript; + this.rawText = ' ' + this.rawText; } else if (this._spaced) { this._spaced = false; - if (this.rawScript[this._caretPos - 1] === ' ') { - this.rawScript = this.rawScript.slice(0, this._caretPos - 1) + this.rawScript.slice(this._caretPos, this.rawScript.length); + if (this.rawText[this._caretPos - 1] === ' ') { + this.rawText = this.rawText.slice(0, this._caretPos - 1) + this.rawText.slice(this._caretPos, this.rawText.length); } } } + @observable rawText: string = ''; @computed({ keepAlive: true }) get renderScriptingBox() { TraceMobx(); return ( @@ -583,8 +592,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable placeholder="write your script here" onFocus={this.onFocus} onBlur={() => this._overlayDisposer?.()} - onChange={(e: any) => (this.rawScript = e.target.value)} - value={this.rawScript} + onChange={action((e: any) => (this.rawText = e.target.value))} + value={this.rawText} movePopupAsYouType={true} loadingComponent={() => <span>Loading</span>} trigger={{ diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index aa51714da..c2aee7a1b 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; .mini-viewer { cursor: grab; @@ -97,7 +97,7 @@ height: 40px; padding: 0 10px 0 7px; transition: opacity 0.3s; - z-index: 100001; + z-index: 10001; .timecode-controls { display: flex; @@ -114,7 +114,8 @@ } } - .toolbar-slider.volume, .toolbar-slider.zoom { + .toolbar-slider.volume, + .toolbar-slider.zoom { width: 50px; } @@ -157,7 +158,8 @@ } } -.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive { +.videoBox-content-fullScreen, +.videoBox-content-fullScreen-interactive { display: flex; justify-content: center; align-items: flex-end; @@ -175,16 +177,16 @@ video::-webkit-media-controls { display: none !important; } -input[type="range"] { +input[type='range'] { -webkit-appearance: none; background: none; } -input[type="range"]:focus { +input[type='range']:focus { outline: none; } -input[type="range"]::-webkit-slider-runnable-track { +input[type='range']::-webkit-slider-runnable-track { width: 100%; height: 10px; cursor: pointer; @@ -193,7 +195,7 @@ input[type="range"]::-webkit-slider-runnable-track { border-radius: 10px; } -input[type="range"]::-webkit-slider-thumb { +input[type='range']::-webkit-slider-thumb { box-shadow: 0; border: 0; height: 12px; @@ -203,4 +205,4 @@ input[type="range"]::-webkit-slider-thumb { cursor: pointer; -webkit-appearance: none; margin-top: -1px; -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b1f7d8023..b45ee7f6e 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -396,10 +396,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // sets video info on load videoLoad = action(() => { - const aspect = this.player!.videoWidth / this.player!.videoHeight; - Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); - Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); - this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; + const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); + if (aspect) { + Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); + Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); + this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; + } if (Number.isFinite(this.player!.duration)) { this.rawDuration = this.player!.duration; } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']); @@ -418,31 +420,26 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // extracts video thumbnails and saves them as field of doc getVideoThumbnails = () => { - const video = document.createElement('video'); + if (this.dataDoc.thumbnails !== undefined) return; + this.dataDoc.thumbnails = new List<string>(); const thumbnailPromises: Promise<any>[] = []; + const video = document.createElement('video'); - video.onloadedmetadata = () => { - video.currentTime = 0; - }; + video.onloadedmetadata = () => (video.currentTime = 0); video.onseeked = () => { const canvas = document.createElement('canvas'); - canvas.height = video.videoHeight; - canvas.width = video.videoWidth; - const ctx = canvas.getContext('2d'); - ctx?.drawImage(video, 0, 0, canvas.width, canvas.height); - const imgUrl = canvas.toDataURL(); + canvas.height = 100; + canvas.width = 100; + canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100); const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, ''); const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_')); - const filename = basename(encodedFilename); - thumbnailPromises.push(VideoBox.convertDataUri(imgUrl, filename)); + thumbnailPromises?.push(VideoBox.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true)); const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1); if (newTime < video.duration) { video.currentTime = newTime; } else { - Promise.all(thumbnailPromises).then(thumbnails => { - this.dataDoc.thumbnails = new List<string>(thumbnails); - }); + Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc.thumbnails = new List<string>(thumbnails))); } }; @@ -554,7 +551,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return !field ? ( <div key="loading">Loading</div> ) : ( - <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}> + <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'default' }}> <div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}> {this._fullScreen && ( <div @@ -836,16 +833,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // stretches vertically or horizontally depending on video orientation so video fits full screen fullScreenSize() { if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) { - return { height: '100%' }; - } else { - return { width: '100%' }; + //prettier-ignore + return ({ height: '100%' }); } + //prettier-ignore + return ({ width: '100%' }); } // for zoom slider, sets timeline waveform zoom - zoom = (zoom: number) => { - this.timeline?.setZoom(zoom); - }; + zoom = (zoom: number) => this.timeline?.setZoom(zoom); // plays link playLink = (doc: Doc) => { @@ -859,7 +855,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // starts marquee selection marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, @@ -898,7 +894,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; - scaling = () => this.props.scaling?.() || 1; + scaling = () => this.props.NativeDimScaling?.() || 1; panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100; panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100); @@ -911,140 +907,46 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp .scale(100 / this.heightPercent); }; - marqueeFitScaling = () => ((this.props.scaling?.() || 1) * this.heightPercent) / 100; + marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 1) * this.heightPercent) / 100; marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0]; timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`]; // renders video controls componentUI = (boundsLeft: number, boundsTop: number) => { - const bounds = this.props.docViewPath().lastElement().getBounds(); - const left = bounds?.left || 0; - const right = bounds?.right || 0; - const top = bounds?.top || 0; - const height = (bounds?.bottom || 0) - top; - const width = Math.max(right - left, 100); - const uiHeight = Math.max(25, Math.min(50, height / 10)); - const uiMargin = Math.min(10, height / 20); - const vidHeight = (height * this.heightPercent) / 100; - const yPos = top + vidHeight - uiHeight - uiMargin; - const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10; + const xf = this.props.ScreenToLocalTransform().inverse(); + const height = this.props.PanelHeight(); + const vidHeight = (height * this.heightPercent) / 100 / this.scaling(); + const vidWidth = this.props.PanelWidth() / this.scaling(); + const uiHeight = 25; + const uiMargin = 10; + const yBot = xf.transformPoint(0, vidHeight)[1]; + // prettier-ignore + const yMid = (xf.transformPoint(0, 0)[1] + + xf.transformPoint(0, height / this.scaling())[1]) / 2; + const xPos = xf.transformPoint(vidWidth / 2, 0)[0]; + const xRight = xf.transformPoint(vidWidth, 0)[0]; const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0; - return this._fullScreen || right - left < 50 ? null : ( + return this._fullScreen || (xRight - xPos) * 2 < 50 ? null : ( <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> - <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? 'top 0.5s' : '', opacity: opacity }}> + <div + className="videoBox-ui" + style={{ + transformOrigin: 'top left', + transform: `rotate(${NumCast(this.rootDoc.jitterRotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`, + left: xPos, + top: yMid, + height: uiHeight, + width: (xRight - xPos) * 2 - 20, + transition: this._clicking ? 'top 0.5s' : '', + opacity, + }}> {this.UIButtons} </div> </div> ); }; - @computed get UIButtons() { - const bounds = this.props.docViewPath().lastElement().getBounds(); - const width = (bounds?.right || 0) - (bounds?.left || 0); - const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0); - return ( - <> - <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}> - <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} /> - </div> - - {this.timeline && width > 150 && ( - <div className="timecode-controls"> - <div className="timecode-current">{formatTime(curTime)}</div> - - {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( - <div className="timeline-slider"> - <input - type="range" - step="0.1" - min={this.timeline.clipStart} - max={this.timeline.clipEnd} - value={curTime} - className="toolbar-slider time-progress" - onPointerDown={action((e: React.PointerEvent) => { - e.stopPropagation(); - this._scrubbing = true; - })} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))} - onPointerUp={action((e: React.PointerEvent) => { - e.stopPropagation(); - this._scrubbing = false; - })} - /> - </div> - ) : ( - <div>/</div> - )} - - <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div> - </div> - )} - - <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}> - <FontAwesomeIcon icon="expand" /> - </div> - - {!this._fullScreen && width > 300 && ( - <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}> - <FontAwesomeIcon icon="eye" /> - </div> - )} - - {!this._fullScreen && width > 300 && ( - <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}> - <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} /> - </div> - )} - - <div - className="videobox-button" - title={this._muted ? 'unmute' : 'mute'} - onPointerDown={e => { - e.stopPropagation(); - this.toggleMute(); - }}> - <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} /> - </div> - {width > 300 && ( - <input - type="range" - style={{ width: `min(25%, 50px)` }} - step="0.1" - min="0" - max="1" - value={this._muted ? 0 : this._volume} - className="toolbar-slider volume" - onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} - /> - )} - - {!this._fullScreen && this.heightPercent !== 100 && width > 300 && ( - <> - <div className="videobox-button" title="zoom"> - <FontAwesomeIcon icon="search-plus" /> - </div> - <input - type="range" - step="0.1" - min="1" - max="5" - value={this.timeline?._zoomFactor} - className="toolbar-slider zoom" - onPointerDown={(e: React.PointerEvent) => { - e.stopPropagation(); - }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - this.zoom(Number(e.target.value)); - }} - /> - </> - )} - </> - ); - } - // renders CollectionStackedTimeline @computed get renderTimeline() { return ( @@ -1124,7 +1026,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ScreenToLocalTransform={this.screenToLocalTransform} docFilters={this.timelineDocFilter} select={emptyFunction} - scaling={returnOne} + NativeDimScaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} @@ -1153,6 +1055,112 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp </div> ); } + + @computed get UIButtons() { + const bounds = this.props.docViewPath().lastElement().getBounds(); + const width = (bounds?.right || 0) - (bounds?.left || 0); + const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0); + return ( + <> + <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}> + <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} /> + </div> + + {this.timeline && width > 150 && ( + <div className="timecode-controls"> + <div className="timecode-current">{formatTime(curTime)}</div> + + {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( + <div className="timeline-slider"> + <input + type="range" + step="0.1" + min={this.timeline.clipStart} + max={this.timeline.clipEnd} + value={curTime} + className="toolbar-slider time-progress" + onPointerDown={action((e: React.PointerEvent) => { + e.stopPropagation(); + this._scrubbing = true; + })} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))} + onPointerUp={action((e: React.PointerEvent) => { + e.stopPropagation(); + this._scrubbing = false; + })} + /> + </div> + ) : ( + <div>/</div> + )} + + <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div> + </div> + )} + + <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}> + <FontAwesomeIcon icon="expand" /> + </div> + + {!this._fullScreen && width > 300 && ( + <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}> + <FontAwesomeIcon icon="eye" /> + </div> + )} + + {!this._fullScreen && width > 300 && ( + <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} /> + </div> + )} + + <div + className="videobox-button" + title={this._muted ? 'unmute' : 'mute'} + onPointerDown={e => { + e.stopPropagation(); + this.toggleMute(); + }}> + <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} /> + </div> + {width > 300 && ( + <input + type="range" + style={{ width: `min(25%, 50px)` }} + step="0.1" + min="0" + max="1" + value={this._muted ? 0 : this._volume} + className="toolbar-slider volume" + onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} + /> + )} + + {!this._fullScreen && this.heightPercent !== 100 && width > 300 && ( + <> + <div className="videobox-button" title="zoom"> + <FontAwesomeIcon icon="search-plus" /> + </div> + <input + type="range" + step="0.1" + min="1" + max="5" + value={this.timeline?._zoomFactor} + className="toolbar-slider zoom" + onPointerDown={(e: React.PointerEvent) => { + e.stopPropagation(); + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + this.zoom(Number(e.target.value)); + }} + /> + </> + )} + </> + ); + } } VideoBox._nativeControls = false; diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index d8dd074a5..85986ff27 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,9 +1,11 @@ -@import "../global/globalCssVariables.scss"; - +@import '../global/globalCssVariables.scss'; .webBox { height: 100%; - position: relative; + width: 100%; + top: 0; + left: 0; + position: absolute; display: flex; .webBox-sideResizer { @@ -84,7 +86,6 @@ background: none; } - .webBox-overlayCont { position: absolute; width: calc(100% - 40px); @@ -95,7 +96,7 @@ justify-content: center; align-items: center; overflow: hidden; - transition: left .5s; + transition: left 0.5s; pointer-events: all; .webBox-searchBar { @@ -158,7 +159,7 @@ left: 0; cursor: text; padding: 15px; - height: 100% + height: 100%; } .webBox-cont { @@ -235,7 +236,7 @@ height: 25px; align-items: center; - >svg { + > svg { margin: auto; } } @@ -257,4 +258,4 @@ } } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index d97277c2b..2c5deba88 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; @@ -11,11 +11,11 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; +import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; -import { undoBatch } from '../../util/UndoManager'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenu } from '../ContextMenu'; @@ -35,6 +35,7 @@ import { LinkDocPreview } from './LinkDocPreview'; import { VideoBox } from './VideoBox'; import './WebBox.scss'; import React = require('react'); +import { DragManager } from '../../util/DragManager'; const { CreateImage } = require('./WebBoxRenderer'); const _global = (window /* browser */ || global) /* node */ as any; const htmlToText = require('html-to-text'); @@ -134,25 +135,30 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; - lockout = false; updateThumb = async () => { const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href; const scrollTop = NumCast(this.layoutDoc._scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); - if (!this.lockout && this._iframe && !imageBitmap && (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight)) { + if ( + !this.rootDoc.thumbLockout && + !this.props.dontRegisterView && + this._iframe && + !imageBitmap && + (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight) + ) { var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); } this.layoutDoc.thumb = undefined; - this.lockout = true; // lock to prevent multiple thumb updates. + this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) .then((data_url: any) => { VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => setTimeout( action(() => { - this.lockout = false; + this.rootDoc.thumbLockout = false; this.layoutDoc.thumb = new ImageField(returnedfilename); this.layoutDoc.thumbScrollTop = scrollTop; this.layoutDoc.thumbNativeWidth = nativeWidth; @@ -200,7 +206,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps autoHeight => { if (autoHeight) { this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']); - this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.scaling?.() || 1)); + this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); } } ); @@ -276,10 +282,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; if (doc !== this.rootDoc && this._outerRef.current) { - const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1); + const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight())); if (scrollTo !== undefined && this._initialScroll === undefined) { - const focusSpeed = smooth ? 500 : 0; + const focusSpeed = smooth ? NumCast(doc.focusSpeed, 500) : 0; this.goTo(scrollTo, focusSpeed); return focusSpeed; } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) { @@ -310,7 +316,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); - const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; + const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); if (sel) { this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined); @@ -322,7 +328,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps iframeDown = (e: PointerEvent) => { const sel = this._iframe?.contentWindow?.getSelection?.(); const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); - const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; + const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; const word = getWordAtPoint(e.target, e.clientX, e.clientY); this._setPreviewCursor?.(e.clientX, e.clientY, false, true); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); @@ -604,7 +610,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps funcs.push({ description: (!this.layoutDoc.forceReflow ? 'Force' : 'Prevent') + ' Reflow', event: () => { - const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1); + const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1); this.layoutDoc.forceReflow = !nw; if (nw) { Doc.SetInPlace(this.layoutDoc, this.fieldKey + '-nativeWidth', nw, true); @@ -708,6 +714,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @observable _draggingSidebar = false; sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { + const batch = UndoManager.StartBatch('sidebar'); // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf setupMoveUpEvents( this, @@ -716,12 +723,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._draggingSidebar = true; const localDelta = this.props .ScreenToLocalTransform() - .scale(this.props.scaling?.() || 1) + .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.scaling?.() || 1)) / nativeWidth; + const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio); @@ -730,10 +737,32 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } return false; }), - action(() => (this._draggingSidebar = false)), - () => this.toggleSidebar() + action((e, movement, isClick) => { + this._draggingSidebar = false; + !isClick && batch.end(); + }), + () => { + this.toggleSidebar(); + batch.end(); + } ); }; + @computed get sidebarHandle() { + return ( + <div + className="webBox-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" + style={{ + display: !this.props.isContentActive() ? 'none' : undefined, + top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={e => this.sidebarBtnDown(e, true)}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + </div> + ); + } @observable _previewNativeWidth: Opt<number> = undefined; @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { @@ -790,6 +819,56 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } + @computed get webpage() { + const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; + const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); + const scale = previewScale * (this.props.NativeDimScaling?.() || 1); + const renderAnnotations = (docFilters?: () => string[]) => ( + <CollectionFreeFormView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + renderDepth={this.props.renderDepth + 1} + isAnnotationOverlay={true} + fieldKey={this.annotationKey} + CollectionView={undefined} + setPreviewCursor={this.setPreviewCursor} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.scrollXf} + NativeDimScaling={returnOne} + dropAction={'alias'} + docFilters={docFilters} + select={emptyFunction} + bringToFront={emptyFunction} + styleProvider={this.childStyleProvider} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + childPointerEvents={this.props.isContentActive() ? 'all' : undefined} + pointerEvents={this.annotationPointerEvents} + /> + ); + return ( + <div + className={'webBox-outerContent'} + ref={this._outerRef} + style={{ + height: `${100 / scale}%`, + pointerEvents, + }} + onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe + onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} + onPointerDown={this.onMarqueeDown}> + <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}> + {this.content} + {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>} + {renderAnnotations(this.opaqueFilter)} + {this.annotationLayer} + </div> + </div> + ); + } + @computed get searchUI() { return ( <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}> @@ -827,13 +906,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); - panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); - panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); + panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); + panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')]; transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (doc.textInlineAnnotations) return 'none'; @@ -841,40 +919,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this.props.styleProvider?.(doc, props, property); }; pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); - annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() ? 'all' : 'none'); + annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none'); render() { - const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; - const scale = previewScale * (this.props.scaling?.() || 1); - const renderAnnotations = (docFilters?: () => string[]) => ( - <CollectionFreeFormView - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} - renderDepth={this.props.renderDepth + 1} - isAnnotationOverlay={true} - fieldKey={this.annotationKey} - CollectionView={undefined} - setPreviewCursor={this.setPreviewCursor} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.scrollXf} - scaling={returnOne} - dropAction={'alias'} - docFilters={docFilters || this.basicFilter} - dontRenderDocuments={docFilters ? false : true} - select={emptyFunction} - ContentScaling={returnOne} - bringToFront={emptyFunction} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - styleProvider={this.childStyleProvider} - childPointerEvents={this.props.isContentActive() ? 'all' : undefined} - pointerEvents={this.annotationPointerEvents} - /> - ); + const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); + const scale = previewScale * (this.props.NativeDimScaling?.() || 1); return ( - <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? 'none' : undefined }}> + <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}> <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} /> <div className="webBox-container" @@ -884,27 +935,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps pointerEvents, }} onContextMenu={this.specificContextMenu}> - <div - className={'webBox-outerContent'} - ref={this._outerRef} - style={{ - height: `${100 / scale}%`, - pointerEvents, - }} - onWheel={e => { - e.stopPropagation(); - e.preventDefault(); - }} // block wheel events from propagating since they're handled by the iframe - onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} - onPointerDown={this.onMarqueeDown}> - <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}> - {this.content} - <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - {this.annotationLayer} - </div> - </div> + {this.webpage} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( <div style={{ transformOrigin: 'top left', transform: `scale(${1 / scale})` }}> <MarqueeAnnotator @@ -934,33 +965,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }} onPointerDown={e => this.sidebarBtnDown(e, false)} /> - <SidebarAnnos - ref={this._sidebarRef} - {...this.props} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - fieldKey={this.fieldKey + '-' + this._urlHash} - rootDoc={this.rootDoc} - layoutDoc={this.layoutDoc} - dataDoc={this.dataDoc} - setHeight={emptyFunction} - nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth) - WebBox.sidebarResizerWidth / (this.props.scaling?.() || 1)} - showSidebar={this.SidebarShown} - sidebarAddDocument={this.sidebarAddDocument} - moveDocument={this.moveDocument} - removeDocument={this.removeDocument} - /> - <div - className="webBox-overlayButton-sidebar" - key="sidebar" - title="Toggle Sidebar" - style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, - backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, - }} - onPointerDown={e => this.sidebarBtnDown(e, true)}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}> + <SidebarAnnos + ref={this._sidebarRef} + {...this.props} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + fieldKey={this.fieldKey + '-' + this._urlHash} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + setHeight={emptyFunction} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth) - WebBox.sidebarResizerWidth / (this.props.NativeDimScaling?.() || 1)} + showSidebar={this.SidebarShown} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + /> </div> + {this.sidebarHandle} {!this.props.isContentActive() ? null : this.searchUI} </div> ); diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss index 6cd56f84e..a1ca777b3 100644 --- a/src/client/views/nodes/button/FontIconBox.scss +++ b/src/client/views/nodes/button/FontIconBox.scss @@ -1,4 +1,4 @@ -@import "../../global/globalCssVariables"; +@import '../../global/globalCssVariables'; .menuButton { height: 100%; @@ -31,8 +31,6 @@ text-transform: uppercase; font-weight: bold; transition: 0.15s; - - } .fontIconBox-icon { @@ -106,31 +104,31 @@ right: 0; bottom: 0; background-color: lightgrey; - -webkit-transition: .4s; - transition: .4s; + -webkit-transition: 0.4s; + transition: 0.4s; } .slider:before { position: absolute; - content: ""; + content: ''; height: 21px; width: 21px; left: 2px; bottom: 2px; background-color: $white; - -webkit-transition: .4s; - transition: .4s; + -webkit-transition: 0.4s; + transition: 0.4s; } - input:checked+.slider { + input:checked + .slider { background-color: $medium-blue; } - input:focus+.slider { + input:focus + .slider { box-shadow: 0 0 1px $medium-blue; } - input:checked+.slider:before { + input:checked + .slider:before { -webkit-transform: translateX(26px); -ms-transform: translateX(26px); transform: translateX(26px); @@ -197,8 +195,6 @@ } } - - &.colorBtn, &.colorBtnLabel { color: black; @@ -311,18 +307,21 @@ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); border-radius: $standard-border-radius; - input[type=range]::-webkit-slider-runnable-track { + .menu-slider { + height: 10px; + } + input[type='range']::-webkit-slider-runnable-track { background: gray; height: 3px; } - input[type=range]::-webkit-slider-thumb { + input[type='range']::-webkit-slider-thumb { box-shadow: 1px 1px 1px #000000; border: 1px solid #000000; height: 10px; width: 10px; border-radius: 5px; - background: #FFFFFF; + background: #ffffff; cursor: pointer; -webkit-appearance: none; margin-top: -4px; @@ -456,5 +455,4 @@ background: transparent; position: fixed; } - -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 1ce97979e..4a6099fb3 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -1,7 +1,8 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, observable } from 'mobx'; +import Color from 'color'; +import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorState, SketchPicker } from 'react-color'; @@ -21,7 +22,7 @@ import { DocComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { GestureOverlay } from '../../GestureOverlay'; import { Colors } from '../../global/globalEnums'; -import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, SetActiveIsInkMask } from '../../InkingStroke'; import { InkTranscription } from '../../InkTranscription'; import { StyleProp } from '../../StyleProvider'; import { FieldView, FieldViewProps } from '.././FieldView'; @@ -86,11 +87,23 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } // Determining UI Specs - @observable private label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); - @observable private icon = StrCast(this.dataDoc.icon, 'user') as any; - @observable private dropdown: boolean = BoolCast(this.rootDoc.dropDownOpen); - @observable private buttonList: string[] = StrListCast(this.rootDoc.btnList); - @observable private type = StrCast(this.rootDoc.btnType); + @computed get label() { + return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); + } + Icon = (color: string) => { + const icon = StrCast(this.dataDoc.icon, 'user') as any; + const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; + return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; + }; + @computed get dropdown() { + return BoolCast(this.rootDoc.dropDownOpen); + } + @computed get buttonList() { + return StrListCast(this.rootDoc.btnList); + } + @computed get type() { + return StrCast(this.rootDoc.btnType); + } /** * Types of buttons in dash: @@ -217,7 +230,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { className={`menuButton ${this.type} ${active}`} style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {this.Icon(color)} {!this.label || !FontIconBox.GetShowLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {' '} @@ -241,16 +254,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); const script = ScriptCast(this.rootDoc.script); - if (!script) { - return null; - } let noviceList: string[] = []; let text: string | undefined; let dropdown = true; let icon: IconProp = 'caret-down'; try { - if (script.script.originalScript.startsWith('setView')) { + if (script?.script.originalScript.startsWith('setView')) { const selected = SelectionManager.Docs().lastElement(); if (selected) { if (StrCast(selected.type) === DocumentType.COL) { @@ -266,7 +276,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { text = 'User Default'; } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking]; - } else if (script.script.originalScript.startsWith('setFont')) { + } else if (script?.script.originalScript.startsWith('setFont')) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text']; @@ -276,11 +286,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } // Get items to place into the list - const list = this.buttonList.map(value => { - if (Doc.noviceMode && !noviceList.includes(value)) { - return; - } - return ( + const list = this.buttonList + .filter(value => !Doc.noviceMode || noviceList.includes(value)) + .map(value => ( <div className="list-item" key={`${value}`} @@ -291,8 +299,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { onClick={() => script.script.run({ value }).result}> {value[0].toUpperCase() + value.slice(1)} </div> - ); - }); + )); const label = !this.label || !FontIconBox.GetShowLabels() ? null : ( @@ -373,7 +380,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))} onPointerDown={e => e.stopPropagation()}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {this.Icon(color)} <div className="colorButton-color" style={{ backgroundColor: curColor }} /> {label} {/* {dropdownCaret} */} @@ -425,7 +432,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } else { return ( <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {this.Icon(color)} {label} </div> ); @@ -438,11 +445,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get defaultButton() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const active: string = StrCast(this.rootDoc.dropDownOpen); return ( <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}> <div className="menuButton-wrap"> - <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={'black'} size={'sm'} /> + {this.Icon(color)} {!this.label || !FontIconBox.GetShowLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {' '} @@ -474,90 +480,50 @@ export class FontIconBox extends DocComponent<ButtonProps>() { render() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const label = - !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color, backgroundColor }}> - {this.label} - </div> - ); - - const menuLabel = + const label = (noBackground: boolean = false) => !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color, backgroundColor: 'transparent' }}> + <div className="fontIconBox-label" style={{ color, backgroundColor: noBackground ? 'transparent' : backgroundColor }}> {this.label} </div> ); - - const buttonText = StrCast(this.rootDoc.buttonText); - // TODO:glr Add label of button type - let button: JSX.Element | null = this.defaultButton; + let button: JSX.Element = this.defaultButton; + // prettier-ignore switch (this.type) { - case ButtonType.TextButton: - button = ( + case ButtonType.DropdownList: return this.dropdownListButton; + case ButtonType.ColorButton: return this.colorButton; + case ButtonType.NumberButton: return this.numberButton; + case ButtonType.EditableText: return this.editableText; + case ButtonType.DropdownButton: button = this.dropdownButton; break; + case ButtonType.ToggleButton: button = this.toggleButton; break; + case ButtonType.TextButton: button = ( <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> - {buttonText ? <div className="button-text">{buttonText}</div> : null} - {label} - </div> - ); - // button = <TextButton {...buttonProps}></TextButton> - break; - case ButtonType.EditableText: - button = this.editableText; - break; - case ButtonType.NumberButton: - button = this.numberButton; - break; - case ButtonType.DropdownButton: - button = this.dropdownButton; - break; - case ButtonType.DropdownList: - button = this.dropdownListButton; - break; - case ButtonType.ColorButton: - button = this.colorButton; - break; - case ButtonType.ToolButton: - button = ( - <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> - {label} + {this.Icon(color)} + {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null} + {label()} </div> ); break; - case ButtonType.ToggleButton: - button = this.toggleButton; - // button = <ToggleButton {...buttonProps}></ToggleButton> - break; case ButtonType.ClickButton: - button = ( - <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ color, backgroundColor, opacity: 1 }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> - {label} + case ButtonType.ToolButton: button = ( + <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}> + {this.Icon(color)} + {label()} </div> ); break; - case ButtonType.MenuButton: - const trailsIcon = <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; - button = ( + case ButtonType.MenuButton: button = ( <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}> - {this.icon === 'pres-trail' ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />} - {menuLabel} + {this.Icon(color)} + {label(true)} <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} /> </div> ); break; - default: - break; } - return !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? ( - button - ) : button !== null ? ( - <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip> - ) : null; + return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>; } } @@ -569,11 +535,11 @@ ScriptingGlobals.add(function setView(view: string) { // toggle: Set overlay status of selected document ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); + const selected = SelectionManager.Views().lastElement(); if (checkResult) { - return selected?._backgroundColor ?? 'transparent'; + return selected?.props.Document._backgroundColor ?? 'transparent'; } - if (selected) selected._backgroundColor = color; + if (selected) selected.props.Document._backgroundColor = color; }); // toggle: Set overlay status of selected document @@ -610,27 +576,19 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { // toggle: Set overlay status of selected document ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) { - SelectionManager.Docs().map(doc => (doc._fontFamily = font)); - const editorView = RichTextMenu.Instance.TextView?.EditorView; - if (checkResult) { - return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - } - if (editorView) RichTextMenu.Instance.setFontFamily(font); - else Doc.UserDoc().fontFamily = font; + if (checkResult) return RichTextMenu.Instance?.fontFamily; + font && RichTextMenu.Instance.setFontFamily(font); }); ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') { const editorView = RichTextMenu.Instance.TextView?.EditorView; const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection(); + // prettier-ignore switch (info) { - case 'family': - return style?.activeFamilies[0]; - case 'size': - return style?.activeSizes[0]; - case 'color': - return style?.activeColors[0]; - case 'highlight': - return style?.activeHighlights[0]; + case 'family': return style?.activeFamilies[0]; + case 'size': return style?.activeSizes[0]; + case 'color': return style?.activeColors[0]; + case 'highlight': return style?.activeHighlights[0]; } }); @@ -655,14 +613,8 @@ ScriptingGlobals.add(function setBulletList(mapStyle: 'bullet' | 'decimal', chec // toggle: Set overlay status of selected document ScriptingGlobals.add(function setFontColor(color?: string, checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - - if (checkResult) { - return editorView ? RichTextMenu.Instance.fontColor : Doc.UserDoc().fontColor; - } - - if (editorView) color && RichTextMenu.Instance.setColor(color, editorView, editorView?.dispatch); - else Doc.UserDoc().fontColor = color; + if (checkResult) return RichTextMenu.Instance.fontColor; + color && RichTextMenu.Instance.setColor(color); }); // toggle: Set overlay status of selected document @@ -684,14 +636,12 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo // toggle: Set overlay status of selected document ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { - return RichTextMenu.Instance.fontSize.replace('px', ''); + return RichTextMenu.Instance?.fontSize.replace('px', ''); } if (typeof size === 'number') size = size.toString(); if (size && Number(size).toString() === size) size += 'px'; - if (editorView) RichTextMenu.Instance.setFontSize(size); - else Doc.UserDoc()._fontSize = size; + RichTextMenu.Instance.setFontSize(size); }); ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; @@ -700,6 +650,13 @@ ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) { } if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor(); }); +ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) { + const textView = RichTextMenu.Instance?.TextView; + if (checkResult) { + return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent'; + } + if (textView) runInAction(() => (textView._recording = !textView._recording)); +}); ScriptingGlobals.add(function toggleBold(checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; @@ -844,6 +801,21 @@ ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean) }); // toggle: Set overlay status of selected document +ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult) { + if (selected?.type === DocumentType.INK) { + return BoolCast(selected.isInkMask) ? Colors.MEDIUM_BLUE : 'transparent'; + } + return ActiveIsInkMask() ? Colors.MEDIUM_BLUE : 'transparent'; + } + SetActiveIsInkMask(!ActiveIsInkMask()); + SelectionManager.Docs() + .filter(doc => doc.type === DocumentType.INK) + .map(doc => (doc.isInkMask = !doc.isInkMask)); +}); + +// toggle: Set overlay status of selected document ScriptingGlobals.add(function setFillColor(color?: string, checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); if (checkResult) { diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 08f255cab..73a711b9d 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -70,6 +70,8 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { @observable _dashDoc: Doc | undefined; @observable _finalLayout: any; @observable _resolvedDataDoc: any; + @observable _width: number = 0; + @observable _height: number = 0; updateDoc = action((dashDoc: Doc) => { this._dashDoc = dashDoc; @@ -83,12 +85,14 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { } if (this.props.width !== (this._dashDoc?._width ?? '') + 'px' || this.props.height !== (this._dashDoc?._height ?? '') + 'px') { try { + this._width = NumCast(this._dashDoc?._width); + this._height = NumCast(this._dashDoc?._height); // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made this.props.view.dispatch( this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { ...this.props.node.attrs, - width: (this._dashDoc?._width ?? '') + 'px', - height: (this._dashDoc?._height ?? '') + 'px', + width: this._width + 'px', + height: this._height + 'px', }) ); } catch (e) { @@ -121,17 +125,17 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { componentDidMount() { this._disposers.upater = reaction( () => this._dashDoc && NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width), - () => { + action(() => { if (this._dashDoc) { - this.props.view.dispatch( - this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { - ...this.props.node.attrs, - width: (this._dashDoc?._width ?? '') + 'px', - height: (this._dashDoc?._height ?? '') + 'px', - }) - ); + this._width = NumCast(this._dashDoc._width); + this._height = NumCast(this._dashDoc._height); + const parent = this._spanRef.current?.parentNode as HTMLElement; + if (parent) { + parent.style.width = this._width + 'px'; + parent.style.height = this._height + 'px'; + } } - } + }) ); } @@ -172,8 +176,8 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { ref={this._spanRef} className="dash-span" style={{ - width: this.props.width, - height: this.props.height, + width: this._width, + height: this._height, position: 'absolute', display: 'inline-block', }} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 8c8b74560..35d919f38 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -178,7 +178,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } else { if (Number(newText).toString() === newText) { if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText); - Doc.SetInPlace(this._dashDoc!, this._fieldKey, newText, true); + Doc.SetInPlace(this._dashDoc!, this._fieldKey, Number(newText), true); } else { const splits = newText.split(DashFieldViewInternal.multiValueDelimeter); if (this._fieldKey !== 'PARAMS' || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) { @@ -216,7 +216,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna onPointerDownLabelSpan = (e: any) => { setupMoveUpEvents(this, e, returnFalse, returnFalse, e => { DashFieldViewMenu.createFieldView = this.createPivotForField; - DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16); + DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey); }); }; @@ -253,17 +253,21 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { DashFieldViewMenu.Instance.fadeOut(true); }; - public show = (x: number, y: number) => { + @observable _fieldKey = ''; + + @action + public show = (x: number, y: number, fieldKey: string) => { + this._fieldKey = fieldKey; this.jumpTo(x, y, true); const hideMenu = () => { this.fadeOut(true); - document.removeEventListener('pointerdown', hideMenu); + document.removeEventListener('pointerdown', hideMenu, true); }; - document.addEventListener('pointerdown', hideMenu); + document.addEventListener('pointerdown', hideMenu, true); }; render() { const buttons = [ - <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}> + <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> <button className="antimodeMenu-button" onPointerDown={this.showFields}> <FontAwesomeIcon icon="eye" size="lg" /> </button> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 27817f317..d3d8c47c0 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -1,4 +1,4 @@ -@import "../../global/globalCssVariables"; +@import '../../global/globalCssVariables'; .ProseMirror { width: 100%; @@ -11,13 +11,13 @@ } audiotag { - left: 0; - position: absolute; - cursor: pointer; - border-radius: 10px; - width: 10px; - margin-top: -2px; - font-size: 4px; + left: 0; + position: absolute; + cursor: pointer; + border-radius: 10px; + width: 10px; + margin-top: -2px; + font-size: 4px; background: lightblue; } audiotag:hover { @@ -63,12 +63,11 @@ audiotag:hover { .formattedTextBox-outer-selected { cursor: text; } - + .formattedTextBox-sidebar-handle { position: absolute; top: 0; - left: 17px; - //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views + right: 0; width: 17px; height: 17px; font-size: 11px; @@ -79,15 +78,14 @@ audiotag:hover { display: flex; justify-content: center; align-items: center; - cursor:grabbing; + cursor: grabbing; box-shadow: $standard-box-shadow; // transition: 0.2s; opacity: 0.3; - &:hover{ + &:hover { opacity: 1 !important; filter: brightness(0.85); } - } .formattedTextBox-sidebar, @@ -117,14 +115,15 @@ audiotag:hover { left: 10%; } -.formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected, -.formattedTextBox-inner, +.formattedTextBox-inner-rounded, +.formattedTextBox-inner-rounded-selected, +.formattedTextBox-inner, .formattedTextBox-inner-minimal, .formattedTextBox-inner-selected { height: 100%; white-space: pre-wrap; .ProseMirror:hover { - background: rgba(200,200,200,0.2); + background: rgba(200, 200, 200, 0.2); } hr { display: block; @@ -141,7 +140,7 @@ audiotag:hover { .formattedTextBox-inner-rounded-selected, .formattedTextBox-inner-selected { > .ProseMirror { - padding:10px; + padding: 10px; } } .formattedTextBox-outer-selected { @@ -236,18 +235,17 @@ footnote::after { position: absolute; top: -5px; left: 27px; - content: " "; + content: ' '; height: 0; width: 0; } - .formattedTextBox-inlineComment { position: relative; width: 40px; height: 20px; &::before { - content: "→"; + content: '→'; } &:hover { background: orange; @@ -260,7 +258,7 @@ footnote::after { width: 40px; height: 20px; &::after { - content: "←"; + content: '←'; } } @@ -270,21 +268,21 @@ footnote::after { width: 40px; height: 20px; &::after { - content: "..."; + content: '...'; } } .prosemirror-anchor { - overflow:hidden; - display:inline-grid; + overflow: hidden; + display: inline-grid; } .prosemirror-linkBtn { - background:unset; - color:unset; - padding:0; + background: unset; + color: unset; + padding: 0; text-transform: unset; letter-spacing: unset; - font-size:unset; + font-size: unset; } .prosemirror-links { display: none; @@ -294,28 +292,28 @@ footnote::after { z-index: 1; padding: 5; border-radius: 2px; - } - .prosemirror-hrefoptions{ - width:0px; - border:unset; - padding:0px; - } - - .prosemirror-links a { +} +.prosemirror-hrefoptions { + width: 0px; + border: unset; + padding: 0px; +} + +.prosemirror-links a { float: left; color: white; text-decoration: none; border-radius: 3px; - } +} - .prosemirror-links a:hover { +.prosemirror-links a:hover { background-color: #eee; color: black; - } +} - .prosemirror-anchor:hover .prosemirror-links { +.prosemirror-anchor:hover .prosemirror-links { display: grid; - } +} .ProseMirror { padding: 0px; @@ -334,7 +332,8 @@ footnote::after { border-left: solid 2px dimgray; } - ol, ul { + ol, + ul { counter-reset: deci1 0 multi1 0; padding-left: 1em; font-family: inherit; @@ -342,42 +341,231 @@ footnote::after { ol { font-family: inherit; } - .bullet { p { font-family: inherit} margin-left: 0; } - .bullet1 { p { font-family: inherit} } - .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p { font-family: inherit} font-size: smaller; } + .bullet { + p { + font-family: inherit; + } + margin-left: 0; + } + .bullet1 { + p { + font-family: inherit; + } + } + .bullet2, + .bullet3, + .bullet4, + .bullet5, + .bullet6 { + p { + font-family: inherit; + } + font-size: smaller; + } - .decimal1-ol { counter-reset: deci1; p {display: inline-block; font-family: inherit} margin-left: 0; } - .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.1em;} - .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;} - .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;} - .decimal5-ol { counter-reset: deci5; p {display: inline-block; font-family: inherit} font-size: smaller; } - .decimal6-ol { counter-reset: deci6; p {display: inline-block; font-family: inherit} font-size: smaller; } - .decimal7-ol { counter-reset: deci7; p {display: inline-block; font-family: inherit} font-size: smaller; } + .decimal1-ol { + counter-reset: deci1; + p { + display: inline-block; + font-family: inherit; + } + margin-left: 0; + } + .decimal2-ol { + counter-reset: deci2; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + padding-left: 2.1em; + } + .decimal3-ol { + counter-reset: deci3; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + padding-left: 2.85em; + } + .decimal4-ol { + counter-reset: deci4; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + padding-left: 3.85em; + } + .decimal5-ol { + counter-reset: deci5; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + } + .decimal6-ol { + counter-reset: deci6; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + } + .decimal7-ol { + counter-reset: deci7; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + } - .multi1-ol { counter-reset: multi1; p {display: inline-block; font-family: inherit} margin-left: 0; padding-left: 1.2em } - .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;} - .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;} - .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;} + .multi1-ol { + counter-reset: multi1; + p { + display: inline-block; + font-family: inherit; + } + margin-left: 0; + padding-left: 1.2em; + } + .multi2-ol { + counter-reset: multi2; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + padding-left: 2em; + } + .multi3-ol { + counter-reset: multi3; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + padding-left: 2.85em; + } + .multi4-ol { + counter-reset: multi4; + p { + display: inline-block; + font-family: inherit; + } + font-size: smaller; + padding-left: 3.85em; + } //.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " } - .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } - .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; vertical-align: top; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } - .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; vertical-align: top; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } - .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; vertical-align: top; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } - .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; vertical-align: top; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } - .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; vertical-align: top; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } - .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; vertical-align: top; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } + .decimal1:before { + transition: 0.5s; + counter-increment: deci1; + display: inline-block; + vertical-align: top; + margin-left: -1em; + width: 1em; + content: counter(deci1) '. '; + } + .decimal2:before { + transition: 0.5s; + counter-increment: deci2; + display: inline-block; + vertical-align: top; + margin-left: -2.1em; + width: 2.1em; + content: counter(deci1) '.' counter(deci2) '. '; + } + .decimal3:before { + transition: 0.5s; + counter-increment: deci3; + display: inline-block; + vertical-align: top; + margin-left: -2.85em; + width: 2.85em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '. '; + } + .decimal4:before { + transition: 0.5s; + counter-increment: deci4; + display: inline-block; + vertical-align: top; + margin-left: -3.85em; + width: 3.85em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '. '; + } + .decimal5:before { + transition: 0.5s; + counter-increment: deci5; + display: inline-block; + vertical-align: top; + margin-left: -2em; + width: 5em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '. '; + } + .decimal6:before { + transition: 0.5s; + counter-increment: deci6; + display: inline-block; + vertical-align: top; + margin-left: -2em; + width: 6em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '. '; + } + .decimal7:before { + transition: 0.5s; + counter-increment: deci7; + display: inline-block; + vertical-align: top; + margin-left: -2em; + width: 7em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '.' counter(deci7) '. '; + } - .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1.3em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } - .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; vertical-align: top; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } - .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; vertical-align: top; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } - .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } + .multi1:before { + transition: 0.5s; + counter-increment: multi1; + display: inline-block; + vertical-align: top; + margin-left: -1.3em; + width: 1.2em; + content: counter(multi1, upper-alpha) '. '; + } + .multi2:before { + transition: 0.5s; + counter-increment: multi2; + display: inline-block; + vertical-align: top; + margin-left: -2em; + width: 2em; + content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '. '; + } + .multi3:before { + transition: 0.5s; + counter-increment: multi3; + display: inline-block; + vertical-align: top; + margin-left: -2.85em; + width: 2.85em; + content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '. '; + } + .multi4:before { + transition: 0.5s; + counter-increment: multi4; + display: inline-block; + vertical-align: top; + margin-left: -4.2em; + width: 4.2em; + content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '.' counter(multi4, lower-roman) '. '; + } } - @media only screen and (max-width: 1000px) { - @import "../../global/globalCssVariables"; + @import '../../global/globalCssVariables'; .ProseMirror { width: 100%; @@ -425,7 +613,7 @@ footnote::after { width: 100%; height: 100%; } - + .formattedTextBox-sidebar-handle { position: absolute; background: lightgray; @@ -562,18 +750,17 @@ footnote::after { position: absolute; top: -5px; left: 27px; - content: " "; + content: ' '; height: 0; width: 0; } - .formattedTextBox-inlineComment { position: relative; width: 40px; height: 20px; &::before { - content: "→"; + content: '→'; } &:hover { background: orange; @@ -586,7 +773,7 @@ footnote::after { width: 40px; height: 20px; &::after { - content: "←"; + content: '←'; } } @@ -596,7 +783,7 @@ footnote::after { width: 40px; height: 20px; &::after { - content: "..."; + content: '...'; } } @@ -606,7 +793,8 @@ footnote::after { font-family: inherit; } - ol, ul { + ol, + ul { counter-reset: deci1 0 multi1 0; padding-left: 1em; font-family: inherit; @@ -616,30 +804,191 @@ footnote::after { font-family: inherit; } - .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; } - .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;} - .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} - .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;} - .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; } - .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; } - .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; } - - .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em } - .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;} - .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} - .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;} - - .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } - .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } - .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } - .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } - .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } - .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } - .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } - - .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } - .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } - .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } - .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } + .decimal1-ol { + counter-reset: deci1; + p { + display: inline; + font-family: inherit; + } + margin-left: 0; + } + .decimal2-ol { + counter-reset: deci2; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + padding-left: 1em; + } + .decimal3-ol { + counter-reset: deci3; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + padding-left: 2em; + } + .decimal4-ol { + counter-reset: deci4; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + padding-left: 3em; + } + .decimal5-ol { + counter-reset: deci5; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + } + .decimal6-ol { + counter-reset: deci6; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + } + .decimal7-ol { + counter-reset: deci7; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + } + + .multi1-ol { + counter-reset: multi1; + p { + display: inline; + font-family: inherit; + } + margin-left: 0; + padding-left: 1.2em; + } + .multi2-ol { + counter-reset: multi2; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + padding-left: 1.4em; + } + .multi3-ol { + counter-reset: multi3; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + padding-left: 2em; + } + .multi4-ol { + counter-reset: multi4; + p { + display: inline; + font-family: inherit; + } + font-size: smaller; + padding-left: 3.4em; + } + + .decimal1:before { + transition: 0.5s; + counter-increment: deci1; + display: inline-block; + margin-left: -1em; + width: 1em; + content: counter(deci1) '. '; + } + .decimal2:before { + transition: 0.5s; + counter-increment: deci2; + display: inline-block; + margin-left: -2.1em; + width: 2.1em; + content: counter(deci1) '.' counter(deci2) '. '; + } + .decimal3:before { + transition: 0.5s; + counter-increment: deci3; + display: inline-block; + margin-left: -2.85em; + width: 2.85em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '. '; + } + .decimal4:before { + transition: 0.5s; + counter-increment: deci4; + display: inline-block; + margin-left: -3.85em; + width: 3.85em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '. '; + } + .decimal5:before { + transition: 0.5s; + counter-increment: deci5; + display: inline-block; + margin-left: -2em; + width: 5em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '. '; + } + .decimal6:before { + transition: 0.5s; + counter-increment: deci6; + display: inline-block; + margin-left: -2em; + width: 6em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '. '; + } + .decimal7:before { + transition: 0.5s; + counter-increment: deci7; + display: inline-block; + margin-left: -2em; + width: 7em; + content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '.' counter(deci7) '. '; + } + + .multi1:before { + transition: 0.5s; + counter-increment: multi1; + display: inline-block; + margin-left: -1em; + width: 1.2em; + content: counter(multi1, upper-alpha) '. '; + } + .multi2:before { + transition: 0.5s; + counter-increment: multi2; + display: inline-block; + margin-left: -2em; + width: 2em; + content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '. '; + } + .multi3:before { + transition: 0.5s; + counter-increment: multi3; + display: inline-block; + margin-left: -2.85em; + width: 2.85em; + content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '. '; + } + .multi4:before { + transition: 0.5s; + counter-increment: multi4; + display: inline-block; + margin-left: -4.2em; + width: 4.2em; + content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '.' counter(multi4, lower-roman) '. '; + } } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index cfaa428f9..5cb805f80 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -20,11 +20,11 @@ import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { DocumentType } from '../../../documents/DocumentTypes'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; @@ -35,6 +35,7 @@ import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; +import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; @@ -44,6 +45,7 @@ import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; +import { DocumentViewInternal } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { LinkDocPreview } from '../LinkDocPreview'; import { DashDocCommentView } from './DashDocCommentView'; @@ -63,20 +65,12 @@ import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); const translateGoogleApi = require('translate-google-api'); -export interface FormattedTextBoxProps { - makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text - xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView - yPadding?: number; - noSidebar?: boolean; - dontScale?: boolean; - dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field) -} export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } @@ -123,6 +117,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return DocListCast(this.dataDoc[this.SidebarKey]); } + @computed get noSidebar() { + return this.props.docViewPath?.()[this.props.docViewPath().length - 2]?.rootDoc.type === DocumentType.RTF || this.props.noSidebar || this.Document._noSidebar; + } @computed get sidebarWidthPercent() { return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%'); } @@ -244,6 +241,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.layoutDoc.showSidebar && this.toggleSidebar(); this._sidebarRef.current?.anchorMenuClick(this.getAnchor()); }; + AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { + !this.layoutDoc.showSidebar && this.toggleSidebar(); + const anchor = this.getAnchor(); + const target = this._sidebarRef.current?.anchorMenuClick(anchor); + if (target) { + anchor.followLinkAudio = true; + DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target)); + target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`); + } + }; AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch); return undefined; @@ -377,9 +384,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; autoLink = () => { + const newAutoLinks = new Set<Doc>(); + const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { - const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); const f = this._editorView.state.selection.from; const t = this._editorView.state.selection.to; var tr = this._editorView.state.tr as any; @@ -388,12 +395,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); - oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); } + oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); }; updateTitle = () => { - const title = StrCast(this.dataDoc.title); + const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( !this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing (title.startsWith('-') || title.startsWith('@')) && @@ -421,28 +428,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const editorView = this._editorView; if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) { const autoLinkTerm = StrCast(target.title).replace(/^@/, ''); - const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm); var alink: Doc | undefined; - flattened1.forEach((flat, i) => { - const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm); - this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; + this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); - const sel = flattened[i]; - tr = tr.addMark(sel.from, sel.to, splitter); - tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { - if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { - alink = - alink ?? - (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || - DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!); - newAutoLinks.add(alink); - const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; - allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); - const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' }); - tr = tr.addMark(pos, pos + node.nodeSize, link); - } - }); - tr = tr.removeMark(sel.from, sel.to, splitter); + if (!sel.$anchor.pos || editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim() === autoLinkTerm) { + tr = tr.addMark(sel.from, sel.to, splitter); + tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + alink = + alink ?? + (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || + DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!); + newAutoLinks.add(alink); + const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; + allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); + const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' }); + tr = tr.addMark(pos, pos + node.nodeSize, link); + } + }); + tr = tr.removeMark(sel.from, sel.to, splitter); + } }); } return tr; @@ -633,18 +638,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action toggleSidebar = (preview: boolean = false) => { - const prevWidth = this.sidebarWidth(); + const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); if (preview) this._showSidebar = true; else this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; - this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false); + const batch = UndoManager.StartBatch('sidebar'); + setupMoveUpEvents( + this, + e, + this.sidebarMove, + (e, movement, isClick) => !isClick && batch.end(), + () => { + this.toggleSidebar(); + batch.end(); + }, + true + ); }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { - const bounds = this._ref.current!.getBoundingClientRect(); - this.layoutDoc._sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; + const localDelta = this.props + .ScreenToLocalTransform() + .scale(this.props.NativeDimScaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100; + const width = this.layoutDoc[WidthSym]() + localDelta[0]; + this.layoutDoc._sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; + this.layoutDoc.width = width; this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%'; e.preventDefault(); return false; @@ -672,9 +694,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const editor = this._editorView!; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> + let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; - if (target) { + if (target && !(e.nativeEvent as any).dash) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') @@ -693,6 +715,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); }) ); + e.preventDefault(); e.stopPropagation(); return; } @@ -751,7 +774,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const uicontrols: ContextMenuProps[] = []; !Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ''} Show Menu on Selections`, event: () => (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' }); uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' }); - uicontrols.push({ description: `${this.layoutDoc._showAudio ? 'Hide' : 'Show'} Dictation Icon`, event: () => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: 'expand-arrows-alt' }); uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && uicontrols.push({ @@ -833,23 +855,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setDictationContent = (value: string) => { if (this._editorView && this._recordingStart) { if (this._break) { - const textanchor = Docs.Create.TextanchorDocument({ title: 'dictation anchor' }); - this.addDocument(textanchor); - const link = DocUtils.MakeLinkToActiveAudio(() => textanchor, false).lastElement(); - link && (Doc.GetProto(link).isDictation = true); - if (!link) return; - const audioanchor = Cast(link.anchor2, Doc, null); - if (!audioanchor) return; - audioanchor.backgroundColor = 'tan'; - const audiotag = this._editorView.state.schema.nodes.audiotag.create({ - timeCode: NumCast(audioanchor._timecodeToShow), - audioId: audioanchor[Id], - textId: textanchor[Id], - }); - Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode; - const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); - const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); + const textanchorFunc = () => { + const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' }); + return this.addDocument(tanch) ? tanch : undefined; + }; + const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); + if (link) { + Doc.GetProto(link).isDictation = true; + const audioanchor = Cast(link.anchor2, Doc, null); + const textanchor = Cast(link.anchor1, Doc, null); + if (audioanchor) { + audioanchor.backgroundColor = 'tan'; + const audiotag = this._editorView.state.schema.nodes.audiotag.create({ + timeCode: NumCast(audioanchor._timecodeToShow), + audioId: audioanchor[Id], + textId: textanchor[Id], + }); + Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode; + const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); + const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); + } + } } const from = this._editorView.state.selection.from; this._break = false; @@ -957,7 +984,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; @computed get contentScaling() { - return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.scaling?.() || 1 : 1; + return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.NativeDimScaling?.() || 1 : 1; } componentDidMount() { !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. @@ -978,7 +1005,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // set the document height when one of the component heights changes and autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }), ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => { - autoHeight && this.props.setHeight?.(this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight))); + const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); + if (autoHeight && newHeight && newHeight !== this.rootDoc.height) { + this.props.setHeight?.(newHeight); + } }, { fireImmediately: true } ); @@ -1357,10 +1387,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } selectOnLoad && this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. - if (this._editorView && !this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) { + if (this._editorView) { this._editorView.state.storedMarks = [ ...(this._editorView.state.storedMarks ?? []), - schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }), + ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []), ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), @@ -1436,14 +1466,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps document.removeEventListener('pointermove', this.onSelectMove); }; onPointerUp = (e: React.PointerEvent): void => { - if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate) this.setupAnchorMenu(); + if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu(); if (!this._downEvent) return; this._downEvent = false; - if (this.props.isContentActive(true)) { + if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) { const editor = this._editorView!; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); - let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> + let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); } @@ -1479,7 +1509,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onFocused = (e: React.FocusEvent): void => { //applyDevTools.applyDevTools(this._editorView); FormattedTextBox.Focused = this; - this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); }; @observable public static Focused: FormattedTextBox | undefined; @@ -1567,6 +1597,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action onBlur = (e: any) => { + if (this.ProseRef?.children[0] !== e.nativeEvent.target) return; this.autoLink(); FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined); if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) { @@ -1666,12 +1697,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (children) { - const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')), margins); + const proseHeight = !this.ProseRef + ? 0 + : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')) + Number(getComputedStyle(child).marginTop.replace('px', '')) + Number(getComputedStyle(child).marginBottom.replace('px', '')), margins); const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); if (this.props.setHeight && scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => (this.rootDoc[this.fieldKey + '-scrollHeight'] = scrollHeight); - if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { + if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); } else { setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... @@ -1680,8 +1713,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }; fitContentsToBox = () => this.props.Document._fitContentsToBox; - sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); // console.log("printting allSideBarDocs"); // console.log(this.allSidebarDocs); @@ -1694,13 +1727,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps sidebarScreenToLocal = () => this.props .ScreenToLocalTransform() - .translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.scaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._viewScale, 1)); + .translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.NativeDimScaling?.() || 1), 0) + .scale(1 / NumCast(this.layoutDoc._viewScale, 1) / (this.props.NativeDimScaling?.() || 1)); @computed get audioHandle() { - return ( - <div className="formattedTextBox-dictation" onClick={action(e => (this._recording = !this._recording))}> - <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? 'red' : 'blue', transitionDelay: '0.6s', opacity: this._recording ? 1 : 0.25 }} icon={'microphone'} size="sm" /> + return !this._recording ? null : ( + <div + className="formattedTextBox-dictation" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + action(e => (this._recording = !this._recording)) + ) + }> + <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" /> </div> ); } @@ -1715,7 +1758,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} style={{ - left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`, backgroundColor: backgroundColor, color: color, opacity: annotated ? 1 : undefined, @@ -1726,7 +1768,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @computed get sidebarCollection() { const renderComponent = (tag: string) => { - const ComponentTag = tag === 'freeform' ? CollectionFreeFormView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; + const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} @@ -1735,7 +1777,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - // usePanelWidth={true} + ScreenToLocalTransform={this.sidebarScreenToLocal} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} showSidebar={this.SidebarShown} @@ -1746,30 +1788,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps removeDocument={this.removeDocument} /> ) : ( - <ComponentTag - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} - NativeWidth={returnZero} - NativeHeight={returnZero} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.sidebarWidth} - xPadding={0} - yPadding={0} - scaleField={this.SidebarKey + '-scale'} - isAnnotationOverlay={false} - select={emptyFunction} - scaling={this.sidebarContentScaling} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.sidebarRemDocument} - moveDocument={this.sidebarMoveDocument} - addDocument={this.sidebarAddDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} - setHeight={this.setSidebarHeight} - fitContentsToBox={this.fitContentsToBox} - noSidebar={true} - fieldKey={this.layoutDoc.sidebarViewType === 'translation' ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} - /> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> + <ComponentTag + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + NativeWidth={returnZero} + NativeHeight={returnZero} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.sidebarWidth} + xPadding={0} + yPadding={0} + scaleField={this.SidebarKey + '-scale'} + isAnnotationOverlay={false} + select={emptyFunction} + NativeDimScaling={this.sidebarContentScaling} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.sidebarRemDocument} + moveDocument={this.sidebarMoveDocument} + addDocument={this.sidebarAddDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.sidebarScreenToLocal} + renderDepth={this.props.renderDepth + 1} + setHeight={this.setSidebarHeight} + fitContentsToBox={this.fitContentsToBox} + noSidebar={true} + treeViewHideTitle={true} + fieldKey={this.layoutDoc.sidebarViewType === 'translation' ? `${this.fieldKey}-translation` : `${this.fieldKey}-sidebar`} + /> + </div> ); }; return ( @@ -1782,7 +1827,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps TraceMobx(); const active = this.props.isContentActive() || this.props.isSelected(); const selected = active; - const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === '100%' ? '-rounded' : ''; const interactive = (Doc.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); @@ -1791,18 +1836,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? '-selected' : ''; - const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > - return styleFromString?.height === '0px' ? null : ( + const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox-cont" onWheel={e => this.props.isContentActive() && e.stopPropagation()} style={{ - transform: this.props.dontScale ? undefined : `scale(${scale})`, - transformOrigin: this.props.dontScale ? undefined : 'top left', - width: this.props.dontScale ? undefined : `${100 / scale}%`, - height: this.props.dontScale ? undefined : `${100 / scale}%`, + ...(this.props.dontScale + ? {} + : { + transform: `scale(${scale})`, + transformOrigin: 'top left', + width: `${100 / scale}%`, + height: `${100 / scale}%`, + }), + transition: 'inherit', // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, - ...styleFromString, + color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), + fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), + fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily), + fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight), + ...styleFromLayoutString, }}> <div className={`formattedTextBox-cont`} @@ -1810,11 +1864,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps style={{ overflow: this.autoHeight ? 'hidden' : undefined, height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), - background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), - color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), - fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), - fontWeight: Cast(this.layoutDoc._fontWeight, 'string', null) as any, - fontFamily: StrCast(this.layoutDoc._fontFamily, 'inherit'), pointerEvents: interactive ? undefined : 'none', }} onContextMenu={this.specificContextMenu} @@ -1832,7 +1881,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps style={{ width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined, - overflow: this.layoutDoc._singleLine ? 'hidden' : undefined, + overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} onDrop={this.ondrop}> @@ -1849,9 +1898,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }} /> </div> - {this.props.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.props.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle} - {!this.layoutDoc._showAudio ? null : this.audioHandle} + {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle} + {this.audioHandle} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 22ca76b2e..6c6d26af5 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -16,7 +16,7 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { FieldViewProps } from '../FieldView'; -import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox'; +import { FormattedTextBox } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; @@ -29,7 +29,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { private _linkToRef = React.createRef<HTMLInputElement>(); @observable public view?: EditorView; - public editorProps: (FieldViewProps & FormattedTextBoxProps) | undefined; + public editorProps: FieldViewProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); @@ -60,26 +60,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private showLinkDropdown: boolean = false; _reaction: IReactionDisposer | undefined; - _delayHide = false; constructor(props: Readonly<{}>) { super(props); runInAction(() => { RichTextMenu.Instance = this; + this.updateMenu(undefined, undefined, props); this._canFade = false; this.Pinned = true; }); } - componentDidMount() { - this._reaction = reaction( - () => SelectionManager.Views(), - () => this._delayHide && !(this._delayHide = false) && this.fadeOut(true) - ); - } - componentWillUnmount() { - this._reaction?.(); - } - @computed get noAutoLink() { return this._noLinkActive; } @@ -108,21 +98,18 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return this._activeAlignment; } - public delayHide = () => (this._delayHide = true); - @action public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) { if (this._linkToRef.current?.getBoundingClientRect().width) { return; } this.view = view; - if (!view || !view.hasFocus()) { - return; - } props && (this.editorProps = props); // Don't do anything if the document/selection didn't change - if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return; + if (view && view.hasFocus()) { + if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return; + } // update active marks const activeMarks = this.getActiveMarksOnSelection(); @@ -137,9 +124,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); - this._activeFontFamily = !activeFamilies.length ? 'Arial' : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; - this._activeFontSize = !activeSizes.length ? StrCast(this.TextView.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0]; - this._activeFontColor = !activeColors.length ? 'black' : activeColors.length > 0 ? String(activeColors[0]) : '...'; + this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0]; + this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this.activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; // update link in current selection @@ -157,13 +144,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); } else if (dontToggle) { - toggleMark(mark.type, mark.attrs)(state, (tx: any) => { - const { from, $from, to, empty } = tx.selection; - if (!tx.doc.rangeHasMark(from, to, mark.type)) { - // hack -- should have just set the mark in the first place - toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch); - } else dispatch(tx); - }); + const tr = state.tr.addMark(state.selection.from, state.selection.to, mark); + dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise } else { toggleMark(mark.type, mark.attrs)(state, dispatch); } @@ -172,7 +154,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveAlignment() { - if (this.view && this.TextView.props.isSelected(true)) { + if (this.view && this.TextView?.props.isSelected(true)) { const path = (this.view.state.selection.$from as any).path; for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) { @@ -185,7 +167,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveListStyle() { - if (this.view && this.TextView.props.isSelected(true)) { + if (this.view && this.TextView?.props.isSelected(true)) { const path = (this.view.state.selection.$from as any).path; for (let i = 0; i < path.length; i += 3) { if (path[i].type === this.view.state.schema.nodes.ordered_list) { @@ -201,17 +183,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveFontStylesOnSelection() { - if (!this.view) return { activeFamilies: [], activeSizes: [], activeColors: [], activeHighlights: [] }; - - const activeFamilies: string[] = []; - const activeSizes: string[] = []; - const activeColors: string[] = []; - const activeHighlights: string[] = []; - if (this.TextView.props.isSelected(true)) { + const activeFamilies = new Set<string>(); + const activeSizes = new Set<string>(); + const activeColors = new Set<string>(); + const activeHighlights = new Set<string>(); + if (this.view && this.TextView?.props.isSelected(true)) { const state = this.view.state; const pos = this.view.state.selection.$from; const marks: Mark[] = [...(state.storedMarks ?? [])]; - if (state.selection.empty) { + if (state.storedMarks !== null) { + } else if (state.selection.empty) { const ref_node = this.reference_node(pos); marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : [])); } else { @@ -220,13 +201,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } marks.forEach(m => { - m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); - m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); - m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize); - m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight)); + m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.family); + m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.color); + m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); + m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); }); } - return { activeFamilies, activeSizes, activeColors, activeHighlights }; + return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } getMarksInSelection(state: EditorState) { @@ -239,7 +220,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { //finds all active marks on selection in given group getActiveMarksOnSelection() { let activeMarks: MarkType[] = []; - if (!this.view || !this.TextView.props.isSelected(true)) return activeMarks; + if (!this.view || !this.TextView?.props.isSelected(true)) return activeMarks; const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); @@ -292,28 +273,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._superscriptActive = false; activeMarks.forEach(mark => { + // prettier-ignore switch (mark.name) { - case 'noAutoLinkAnchor': - this._noLinkActive = true; - break; - case 'strong': - this._boldActive = true; - break; - case 'em': - this._italicsActive = true; - break; - case 'underline': - this._underlineActive = true; - break; - case 'strikethrough': - this._strikethroughActive = true; - break; - case 'subscript': - this._subscriptActive = true; - break; - case 'superscript': - this._superscriptActive = true; - break; + case 'noAutoLinkAnchor': this._noLinkActive = true; break; + case 'strong': this._boldActive = true; break; + case 'em': this._italicsActive = true; break; + case 'underline': this._underlineActive = true; break; + case 'strikethrough': this._strikethroughActive = true; break; + case 'subscript': this._subscriptActive = true; break; + case 'superscript': this._superscriptActive = true; break; } }); } @@ -355,14 +323,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) { this.TextView.dataDoc.fontSize = fontSize; this.view.focus(); - this.updateMenu(this.view, undefined, this.props); } else { const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); - this.updateMenu(this.view, undefined, this.props); } - } + } else Doc.UserDoc()._fontSize = fontSize; + this.updateMenu(this.view, undefined, this.props); }; setFontFamily = (family: string) => { @@ -370,8 +337,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); - this.updateMenu(this.view, undefined, this.props); - } + } else Doc.UserDoc()._fontFamily = family; + this.updateMenu(this.view, undefined, this.props); }; setHighlight(color: String, view: EditorView, dispatch: any) { @@ -381,20 +348,20 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(highlightMark, view.state, dispatch, false); } - setColor(color: String, view: EditorView, dispatch: any) { + setColor(color: string) { if (this.view) { - const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color }); + const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color }); this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); - view.focus(); - this.updateMenu(this.view, undefined, this.props); - } + this.view.focus(); + } else Doc.UserDoc().fontColor = color; + this.updateMenu(this.view, undefined, this.props); } // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { const active = this.view?.state && RichTextMenu.Instance.getActiveListStyle(); - const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? "" : mapStyle }); + const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle }); if (!this.view || nodeType?.attrs.mapStyle === '') return; const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list; @@ -443,7 +410,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { - if (this.TextView.props.isSelected(true)) { + if (this.TextView?.props.isSelected(true)) { var tr = view.state.tr; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 1916b94bf..2eb62c38d 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -80,7 +80,7 @@ export class RichTextRules { textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument('', { _layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: '9px', title: 'inline comment' }); + const textDocInline = Docs.Create.TextDocument('', { _layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _fitWidth: true, _autoHeight: true, _fontSize: '9px', title: 'inline comment' }); textDocInline.title = inlineFieldKey; // give the annotation its own title textDocInline['title-custom'] = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point @@ -343,8 +343,14 @@ export class RichTextRules { const node = (state.doc.resolve(start) as any).nodeAfter; if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); - - return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_mark) !== -1) { + } + return node + ? state.tr + .removeMark(start, end, schema.marks.user_mark) + .addMark(start, end, schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })) + .addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) + : state.tr; }), new InputRule(new RegExp(/%\(/), (state, match, start, end) => { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index c95ece3da..82b8a8f90 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -1,16 +1,18 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; import { Doc, DocListCast, DocListCastAsync, FieldResult } from '../../../../fields/Doc'; +import { Copy } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; +import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents, StopEvent } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; @@ -19,7 +21,7 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; -import { MarqueeViewBounds } from '../../collections/collectionFreeForm'; +import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm'; import { CollectionView } from '../../collections/CollectionView'; import { TabDocView } from '../../collections/TabDocView'; import { ViewBoxBaseComponent } from '../../DocComponent'; @@ -32,7 +34,7 @@ import { PresEffect, PresMovement, PresStatus } from './PresEnums'; export interface PinProps { audioRange?: boolean; - setPosition?: boolean; + activeFrame?: number; hidePresBox?: boolean; pinWithView?: PinViewProps; pinDocView?: boolean; // whether the current view specs of the document should be saved the pinned document @@ -66,52 +68,51 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { delay: presDoc.presTransition, // when: this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc, }; + //prettier-ignore switch (presDoc.presEffect) { - case PresEffect.Zoom: - return <Zoom {...effectProps}>{renderDoc}</Zoom>; - case PresEffect.Fade: - return <Fade {...effectProps}>{renderDoc}</Fade>; - case PresEffect.Flip: - return <Flip {...effectProps}>{renderDoc}</Flip>; - case PresEffect.Rotate: - return <Rotate {...effectProps}>{renderDoc}</Rotate>; - case PresEffect.Bounce: - return <Bounce {...effectProps}>{renderDoc}</Bounce>; - case PresEffect.Roll: - return <Roll {...effectProps}>{renderDoc}</Roll>; - case PresEffect.Lightspeed: - return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; - case PresEffect.None: - default: - return renderDoc; + default: + case PresEffect.None: return renderDoc; + case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; + case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; + case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; + case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; + case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; + case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; + case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; } } public static EffectsProvider(layoutDoc: Doc, renderDoc: any) { return PresBox.Instance && layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc ? PresBox.renderEffectsDoc(renderDoc, layoutDoc, PresBox.Instance.childDocs[PresBox.Instance.itemIndex]) : renderDoc; } + private _disposers: { [name: string]: IReactionDisposer } = {}; + public selectedArray = new ObservableSet<Doc>(); + + constructor(props: any) { + super(props); + if ((Doc.ActivePresentation = this.rootDoc)) runInAction(() => (PresBox.Instance = this)); + } + @observable public static Instance: PresBox; + @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView @observable _isChildActive = false; @observable _moveOnFromAudio: boolean = true; @observable _presTimer!: NodeJS.Timeout; - @observable _presKeyEventsActive: boolean = false; - @observable _selectedArray: ObservableMap = new ObservableMap<Doc, any>(); @observable _eleArray: HTMLElement[] = []; @observable _dragArray: HTMLElement[] = []; @observable _pathBoolean: boolean = false; @observable _expandBoolean: boolean = false; - - private _disposers: { [name: string]: IReactionDisposer } = {}; - - @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView - @observable private transitionTools: boolean = false; - @observable private newDocumentTools: boolean = false; - @observable private progressivizeTools: boolean = false; - @observable private openMovementDropdown: boolean = false; - @observable private openEffectDropdown: boolean = false; - @observable private presentTools: boolean = false; + @observable _transitionTools: boolean = false; + @observable _newDocumentTools: boolean = false; + @observable _progressivizeTools: boolean = false; + @observable _openMovementDropdown: boolean = false; + @observable _openEffectDropdown: boolean = false; + @observable _presentTools: boolean = false; + @observable _treeViewMap: Map<Doc, number> = new Map(); + @observable _presKeyEvents: boolean = false; + @observable _forceKeyEvents: boolean = false; @computed get isTreeOrStack() { return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any); } @@ -124,15 +125,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get childDocs() { return DocListCast(this.rootDoc[this.presFieldKey]); } - @observable _treeViewMap: Map<Doc, number> = new Map(); - @computed get tagDocs() { - const tagDocs: Doc[] = []; - for (const doc of this.childDocs) { - const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); - tagDocs.push(tagDoc); - } - return tagDocs; + return this.childDocs.map(doc => Cast(doc.presentationTargetDoc, Doc, null)); } @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); @@ -143,42 +137,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get targetDoc() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); } - @computed get scrollable(): boolean { + @computed get scrollable() { if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true; - else return false; + return false; } - @computed get panable(): boolean { + @computed get panable() { if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; - else return false; - } - constructor(props: any) { - super(props); - if ((Doc.ActivePresentation = this.rootDoc)) runInAction(() => (PresBox.Instance = this)); - this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox + return false; } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; - if (this._selectedArray.size) return DocumentManager.Instance.getDocumentView(this.rootDoc); + if (this.selectedArray.size) return DocumentManager.Instance.getDocumentView(this.rootDoc); } - @computed get isPres(): boolean { - document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); - if (this.selectedDoc?.type === DocumentType.PRES) { - document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); - document.addEventListener('keydown', PresBox.keyEventsWrapper, true); - return true; - } - return false; + @computed get isPres() { + return this.selectedDoc === this.rootDoc; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + clearSelectedArray = () => this.selectedArray.clear(); + addToSelectedArray = (doc: Doc) => this.selectedArray.add(doc); + removeFromSelectedArray = (doc: Doc) => this.selectedArray.delete(doc); _unmounting = false; @action componentWillUnmount() { this._unmounting = true; document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); - this._presKeyEventsActive = false; this.resetPresentation(); // Turn of progressivize editors this.turnOffEdit(true); @@ -187,6 +172,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action componentDidMount() { + this._disposers.keyboard = reaction( + () => this.selectedDoc, + selected => { + document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); + (this._presKeyEvents = selected === this.rootDoc) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true); + }, + { fireImmediately: true } + ); + this._disposers.forcekeyboard = reaction( + () => this._forceKeyEvents, + force => { + if (force) { + document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); + document.addEventListener('keydown', PresBox.keyEventsWrapper, true); + this._presKeyEvents = true; + } + this._forceKeyEvents = false; + }, + { fireImmediately: true } + ); + this.props.setContentView?.(this); this._unmounting = false; this.rootDoc._forceRenderEngine = 'timeline'; this.layoutDoc.presStatus = PresStatus.Edit; @@ -204,11 +210,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action updateCurrentPresentation = (pres?: Doc) => { - if (pres) Doc.ActivePresentation = pres; - else Doc.ActivePresentation = this.rootDoc; - document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); - document.addEventListener('keydown', PresBox.keyEventsWrapper, true); - this._presKeyEventsActive = true; + Doc.ActivePresentation = pres ?? this.rootDoc; PresBox.Instance = this; }; @@ -312,12 +314,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //The function that is called when a document is clicked or reached through next or back. //it'll also execute the necessary actions if presentation is playing. + @undoBatch public gotoDocument = action((index: number, from?: Doc, group?: boolean) => { Doc.UnBrushAllDocs(); + if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; + if (activeItem.presActiveFrame !== undefined) { + const transTime = NumCast(activeItem.presTransition, 500); + const context = DocCast(DocCast(activeItem.presentationTargetDoc).context); + if (context) { + const contextView = DocumentManager.Instance.getFirstDocumentView(context); + if (contextView?.ComponentView) { + CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime); + context._currentFrame = NumCast(activeItem.presActiveFrame); + } + } + } if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) { DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } @@ -330,62 +345,97 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (targetDoc) { Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc); - targetDoc && - runInAction(() => { - if (activeItem.presMovement === PresMovement.Jump) targetDoc.focusSpeed = 0; - else targetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500; - }); - setTimeout(() => (targetDoc.focusSpeed = 500), this.activeItem.presTransition ? NumCast(this.activeItem.presTransition) + 10 : 510); + targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500))); + setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10); } if (targetDoc?.lastFrame !== undefined) { targetDoc._currentFrame = 0; } - if (!group) this._selectedArray.clear(); - this.childDocs[index] && this._selectedArray.set(this.childDocs[index], undefined); //Update selected array + if (!group) this.clearSelectedArray(); + this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list this.onHideDocument(); //Handles hide after/before } }); + static pinDataTypes(target: Doc) { + const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking; + const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform); + const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any); + const clippable = [DocumentType.COMPARISON].includes(target.type as any); + const dataview = [DocumentType.INK].includes(target.type as any) && target.activeFrame === undefined; + const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined; + return { scrollable, pannable, temporal, clippable, dataview, textview }; + } - _navTimer!: NodeJS.Timeout; - navigateToView = (targetDoc: Doc, activeItem: Doc) => { - clearTimeout(this._navTimer); - const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; - if (bestTarget) console.log(bestTarget.title, bestTarget.type); - else console.log('no best target'); - if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false); - }; - - // navigates to the bestTarget document by making sure it is on screen, - // then it applies the view specs stored in activeItem to @action - static navigateToDoc(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) { - if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { - bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; - bestTarget._scrollTop = activeItem.presPinViewScroll; - } else if (bestTarget.type === DocumentType.COMPARISON) { - bestTarget._clipWidth = activeItem.presPinClipWidth; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(bestTarget.type as any)) { - bestTarget._currentTimecode = activeItem.presStartTime; - } else { - const contentBounds = Cast(activeItem.contentBounds, listSpec('number')); - bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; + static restoreTargetDocView(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) { + const transTime = NumCast(activeItem.presTransition, 500); + const presTransitionTime = `all ${transTime}ms`; + const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(bestTarget); + bestTarget._viewTransition = presTransitionTime; + if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth; + if (temporal) bestTarget._currentTimecode = activeItem.presStartTime; + if (scrollable) bestTarget._scrollTop = activeItem.presPinViewScroll; + if (dataview) Doc.GetProto(bestTarget).data = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + if (textview) Doc.GetProto(bestTarget).text = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + if (pannable) { + bestTarget._panX = activeItem.presPinViewX; + bestTarget._panY = activeItem.presPinViewY; + bestTarget._viewScale = activeItem.presPinViewScale; + const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); if (contentBounds) { - bestTarget._panX = (contentBounds[0] + contentBounds[2]) / 2; - bestTarget._panY = (contentBounds[1] + contentBounds[3]) / 2; const dv = DocumentManager.Instance.getDocumentView(bestTarget); - if (dv) { - bestTarget._viewScale = Math.min(dv.props.PanelHeight() / (contentBounds[3] - contentBounds[1]), dv.props.PanelWidth() / (contentBounds[2] - contentBounds[0])); + dv && (bestTarget._viewScale = Math.min(dv.props.PanelHeight() / (contentBounds[3] - contentBounds[1]), dv.props.PanelWidth() / (contentBounds[2] - contentBounds[0]))); + } + } + return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10); + } + + /// copies values from the targetDoc (which is the prototype of the pinDoc) to + /// reserved fields on the pinDoc so that those values can be restored to the + /// target doc when navigating to it. + @action + static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) { + if (pinProps?.pinWithView) { + // If pinWithView option set then update scale and x / y props of slide + const bounds = pinProps.pinWithView.bounds; + pinDoc.presPinView = true; + pinDoc.presPinViewX = bounds.left + bounds.width / 2; + pinDoc.presPinViewY = bounds.top + bounds.height / 2; + pinDoc.presPinViewScale = pinProps.pinWithView.scale; + pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); + } + if (pinProps?.pinDocView) { + const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(pinDoc); + pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable || dataview || textview || pinProps.activeFrame !== undefined; + pinDoc.presX = NumCast(targetDoc.x); + pinDoc.presY = NumCast(targetDoc.y); + pinDoc.presRot = NumCast(targetDoc.jitterRotation); + pinDoc.presWidth = NumCast(targetDoc.width); + pinDoc.presHeight = NumCast(targetDoc.height); + + if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop; + if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth; + if (temporal) pinDoc.presEndTime = NumCast((pinDoc.presStartTime = pinDoc._currentTimecode)) + 0.1; + if (textview) pinDoc.presData = targetDoc.text instanceof ObjectField ? targetDoc.text[Copy]() : targetDoc.text; + if (dataview) pinDoc.presData = targetDoc.data instanceof ObjectField ? targetDoc.data[Copy]() : targetDoc.data; + if (pannable) { + const panX = NumCast(pinDoc._panX); + const panY = NumCast(pinDoc._panY); + const pw = NumCast(pinProps.panelWidth); + const ph = NumCast(pinProps.panelHeight); + const ps = NumCast(pinDoc._viewScale, 1); + if (pw && ph && ps) { + pinDoc.presPinViewBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]); } - } else { - bestTarget._panX = activeItem.presPinViewX; - bestTarget._panY = activeItem.presPinViewY; - bestTarget._viewScale = activeItem.presPinViewScale; + pinDoc.presPinViewX = panX; + pinDoc.presPinViewY = panY; + pinDoc.presPinViewScale = ps; } } - return setTimeout(() => (bestTarget._viewTransition = undefined), activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510); } + _navTimer!: NodeJS.Timeout; /** * This method makes sure that cursor navigates to the element that * has the option open and last in the group. @@ -401,7 +451,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined; const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc); - const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext); + const tab = CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext); this.turnOffEdit(); // Handles the setting of presCollection if (includesDoc) { @@ -411,7 +461,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc.presCollection = srcContext; } const presStatus = this.rootDoc.presStatus; - const selViewCache = Array.from(this._selectedArray.keys()); + const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); const eleViewCache = Array.from(this._eleArray); const self = this; @@ -419,8 +469,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc); if (presDocView) SelectionManager.SelectView(presDocView, false); self.rootDoc.presStatus = presStatus; - self._selectedArray.clear(); - selViewCache.forEach(doc => self._selectedArray.set(doc, undefined)); + self.clearSelectedArray(); + selViewCache.forEach(doc => self.addToSelectedArray(doc)); self._dragArray.splice(0, self._dragArray.length, ...dragViewCache); self._eleArray.splice(0, self._eleArray.length, ...eleViewCache); }); @@ -435,25 +485,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { finished?.(); } }; + if (activeItem.presPinView && DocCast(targetDoc.context)?._currentFrame === undefined) { + const transTime = NumCast(activeItem.presTransition, 500); + const presTransitionTime = `all ${transTime}ms`; + targetDoc._dataTransition = presTransitionTime; + targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x)); + targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y)); + targetDoc.jitterRotation = NumCast(activeItem.presRot, NumCast(targetDoc.jitterRotation)); + targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width)); + targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height)); + setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10); + } // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { - LightboxView.SetLightboxDoc(targetDoc); - // openInTab(targetDoc); - } else if (curDoc.presMovement === PresMovement.Pan && targetDoc) { - LightboxView.SetLightboxDoc(undefined); - await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right - } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { + LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc); + } else if (targetDoc && curDoc.presMovement !== PresMovement.None && targetDoc) { LightboxView.SetLightboxDoc(undefined); - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right + const zooming = curDoc.presMovement !== PresMovement.Pan; + DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); } // After navigating to the document, if it is added as a presPinView then it will // adjust the pan and scale to that of the pinView when it was added. if (activeItem.presPinView) { - console.log(targetDoc.title); - console.log('presPinView in PresBox.tsx:420'); // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target - this.navigateToView(targetDoc, activeItem); + clearTimeout(this._navTimer); + const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; + if (bestTarget) this._navTimer = PresBox.restoreTargetDocView(bestTarget, activeItem, false); } }; @@ -511,26 +568,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.childDocs.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); - if (tagDoc) tagDoc.opacity = 1; + //if (tagDoc) tagDoc.opacity = 1; const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); const curInd: number = itemIndexes.indexOf(index); if (tagDoc === this.layoutDoc.presCollection) { tagDoc.opacity = 1; } else { - if (itemIndexes.length > 1 && curDoc.presHideBefore && curInd !== 0) { - } else if (curDoc.presHideBefore) { - if (index > this.itemIndex) { - tagDoc.opacity = 0; - } else if (!curDoc.presHideAfter) { + if (curDoc.presHideBefore) { + if (itemIndexes.length > 1 && curInd !== 0) { tagDoc.opacity = 1; + } else { + if (index > this.itemIndex) { + tagDoc.opacity = 0; + } else if (index === this.itemIndex || !curDoc.presHideAfter) { + tagDoc.opacity = 1; + } } } - if (itemIndexes.length > 1 && curDoc.presHideAfter && curInd !== itemIndexes.length - 1) { - } else if (curDoc.presHideAfter) { - if (index < this.itemIndex) { - tagDoc.opacity = 0; - } else if (!curDoc.presHideBefore) { + if (curDoc.presHideAfter) { + if (itemIndexes.length > 1 && curInd !== itemIndexes.length - 1) { tagDoc.opacity = 1; + } else { + if (index < this.itemIndex) { + tagDoc.opacity = 0; + } else if (index === this.itemIndex || !curDoc.presHideBefore) { + tagDoc.opacity = 1; + } } } } @@ -707,27 +770,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }); - setMovementName = action((movement: any, activeItem: Doc): string => { - let output: string = 'none'; - switch (movement) { - case PresMovement.Zoom: - output = 'Pan & Zoom'; - break; //Pan and zoom - case PresMovement.Pan: - output = 'Pan'; - break; //Pan - case PresMovement.Jump: - output = 'Jump cut'; - break; //Jump Cut - case PresMovement.None: - output = 'None'; - break; //None - default: - output = 'Zoom'; - activeItem.presMovement = 'zoom'; - break; //default set as zoom + movementName = action((activeItem: Doc) => { + if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) { + activeItem.presMovement = 'zoom'; } - return output; + return StrCast(activeItem.presMovement); }); whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isChildActive = isActive))); @@ -773,16 +820,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /** * For sorting the array so that the order is maintained when it is dropped. */ - @action - sortArray = (): Doc[] => { - return this.childDocs.filter(doc => this._selectedArray.has(doc)); - }; + sortArray = () => this.childDocs.filter(doc => this.selectedArray.has(doc)); /** * Method to get the list of selected items in the order in which they have been selected */ @computed get listOfSelected() { - const list = Array.from(this._selectedArray.keys()).map((doc: Doc, index: any) => { + return Array.from(this.selectedArray).map((doc: Doc, index: any) => { const curDoc = Cast(doc, Doc, null); const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); if (curDoc && curDoc === this.activeItem) @@ -806,7 +850,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); }); - return list; } @action @@ -818,54 +861,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //Regular click @action selectElement = async (doc: Doc) => { - const context = Cast(doc.context, Doc, null); this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); - if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0); - else this.updateCurrentPresentation(context); - - if (this.activeItem.setPosition && this.activeItem.y !== undefined && this.activeItem.x !== undefined && this.targetDoc.x !== undefined && this.targetDoc.y !== undefined) { - const timer = (ms: number) => new Promise(res => (this._presTimer = setTimeout(res, ms))); - const time = 10; - const ydiff = NumCast(this.activeItem.y) - NumCast(this.targetDoc.y); - const xdiff = NumCast(this.activeItem.x) - NumCast(this.targetDoc.x); - - for (let i = 0; i < time; i++) { - this.targetDoc.x = NumCast(this.targetDoc.x) + xdiff / time; - this.targetDoc.y = NumCast(this.targetDoc.y) + ydiff / time; - await timer(0.1); - } - } + if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0); + else this.updateCurrentPresentation(DocCast(doc.context)); }; //Command click @action multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { - if (!this._selectedArray.has(doc)) { - this._selectedArray.set(doc, undefined); + if (!this.selectedArray.has(doc)) { + this.addToSelectedArray(doc); this._eleArray.push(ref); this._dragArray.push(drag); } else { - this._selectedArray.delete(doc); - this.removeFromArray(this._eleArray, doc); - this.removeFromArray(this._dragArray, doc); + this.removeFromSelectedArray(doc); + this._eleArray.splice(this._eleArray.indexOf(ref)); + this._dragArray.splice(this._dragArray.indexOf(drag)); } this.selectPres(); }; - removeFromArray = (arr: any[], val: any) => { - const index: number = arr.indexOf(val); - const ret: any[] = arr.splice(index, 1); - arr = ret; - }; - //Shift click @action shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { - this._selectedArray.clear(); + this.clearSelectedArray(); // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); if (this.activeItem) { for (let i = Math.min(this.itemIndex, this.childDocs.indexOf(doc)); i <= Math.max(this.itemIndex, this.childDocs.indexOf(doc)); i++) { - this._selectedArray.set(this.childDocs[i], undefined); + this.addToSelectedArray(this.childDocs[i]); this._eleArray.push(ref); this._dragArray.push(drag); } @@ -876,8 +899,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //regular click @action regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, selectPres = true) => { - this._selectedArray.clear(); - this._selectedArray.set(doc, undefined); + this.clearSelectedArray(); + this.addToSelectedArray(doc); this._eleArray.splice(0, this._eleArray.length, ref); this._dragArray.splice(0, this._dragArray.length, drag); focus && this.selectElement(doc); @@ -890,9 +913,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { else this.regularSelect(doc, ref, drag, focus); }; - static keyEventsWrapper = (e: KeyboardEvent) => { - PresBox.Instance.keyEvents(e); - }; + static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance.keyEvents(e); // Key for when the presentaiton is active @action @@ -906,10 +927,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.layoutDoc.presStatus === 'edit') { undoBatch( action(() => { - for (const doc of Array.from(this._selectedArray.keys())) { + for (const doc of this.selectedArray) { this.removeDocument(doc); } - this._selectedArray.clear(); + this.clearSelectedArray(); this._eleArray.length = 0; this._dragArray.length = 0; }) @@ -921,7 +942,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { this.updateMinimize(); } else if (this.layoutDoc.presStatus === 'edit') { - this._selectedArray.clear(); + this.clearSelectedArray(); this._eleArray.length = this._dragArray.length = 0; } else this.layoutDoc.presStatus = 'edit'; if (this._presTimer) clearTimeout(this._presTimer); @@ -934,7 +955,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (e.shiftKey && this.itemIndex < this.childDocs.length - 1) { // TODO: update to work properly this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) + 1; - this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined); + this.addToSelectedArray(this.childDocs[this.rootDoc._itemIndex]); } else { this.next(); if (this._presTimer) { @@ -951,7 +972,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (e.shiftKey && this.itemIndex !== 0) { // TODO: update to work properly this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) - 1; - this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined); + this.addToSelectedArray(this.childDocs[this.rootDoc._itemIndex]); } else { this.back(); if (this._presTimer) { @@ -969,8 +990,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { break; case 'a': if ((e.metaKey || e.altKey) && this.layoutDoc.presStatus === 'edit') { - this._selectedArray.clear(); - this.childDocs.forEach(doc => this._selectedArray.set(doc, undefined)); + this.clearSelectedArray(); + this.childDocs.forEach(doc => this.addToSelectedArray(doc)); handled = true; } default: @@ -993,13 +1014,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - getAllIndexes = (arr: Doc[], val: Doc): number[] => { - const indexes = []; - for (let i = 0; i < arr.length; i++) { - arr[i] === val && indexes.push(i); - } - return indexes; - }; + getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1); // Adds the index in the pres path graphically @computed get order() { @@ -1118,7 +1133,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 10000) timeInMS = 10000; - Array.from(this._selectedArray.keys()).forEach(doc => (doc.presTransition = timeInMS)); + this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)); }; // Converts seconds to ms and updates presTransition @@ -1127,7 +1142,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) scale += change; if (scale < 0.01) scale = 0.01; if (scale > 1.5) scale = 1.5; - Array.from(this._selectedArray.keys()).forEach(doc => (doc.presZoom = scale)); + this.selectedArray.forEach(doc => (doc.presZoom = scale)); }; // Converts seconds to ms and updates presDuration @@ -1136,114 +1151,43 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; - Array.from(this._selectedArray.keys()).forEach(doc => (doc.presDuration = timeInMS)); + this.selectedArray.forEach(doc => (doc.presDuration = timeInMS)); }; /** * When the movement dropdown is changes */ @undoBatch - updateMovement = action((movement: any, all?: boolean) => { - const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); - array.forEach(doc => { - switch (movement) { - case PresMovement.Zoom: //Pan and zoom - doc.presMovement = PresMovement.Zoom; - break; - case PresMovement.Pan: //Pan - doc.presMovement = PresMovement.Pan; - break; - case PresMovement.Jump: //Jump Cut - doc.presJump = true; - doc.presMovement = PresMovement.Jump; - break; - case PresMovement.None: - default: - doc.presMovement = PresMovement.None; - break; - } - }); - }); + updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement))); @undoBatch @action updateHideBefore = (activeItem: Doc) => { activeItem.presHideBefore = !activeItem.presHideBefore; - Array.from(this._selectedArray.keys()).forEach(doc => (doc.presHideBefore = activeItem.presHideBefore)); + this.selectedArray.forEach(doc => (doc.presHideBefore = activeItem.presHideBefore)); }; @undoBatch @action updateHideAfter = (activeItem: Doc) => { activeItem.presHideAfter = !activeItem.presHideAfter; - Array.from(this._selectedArray.keys()).forEach(doc => (doc.presHideAfter = activeItem.presHideAfter)); + this.selectedArray.forEach(doc => (doc.presHideAfter = activeItem.presHideAfter)); }; @undoBatch @action updateOpenDoc = (activeItem: Doc) => { activeItem.openDocument = !activeItem.openDocument; - Array.from(this._selectedArray.keys()).forEach(doc => { - doc.openDocument = activeItem.openDocument; - }); + this.selectedArray.forEach(doc => (doc.openDocument = activeItem.openDocument)); }; @undoBatch @action - updateEffectDirection = (effect: any, all?: boolean) => { - const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); - array.forEach(doc => { - const tagDoc = doc; // Cast(doc.presentationTargetDoc, Doc, null); - switch (effect) { - case PresEffect.Left: - tagDoc.presEffectDirection = PresEffect.Left; - break; - case PresEffect.Right: - tagDoc.presEffectDirection = PresEffect.Right; - break; - case PresEffect.Top: - tagDoc.presEffectDirection = PresEffect.Top; - break; - case PresEffect.Bottom: - tagDoc.presEffectDirection = PresEffect.Bottom; - break; - case PresEffect.Center: - default: - tagDoc.presEffectDirection = PresEffect.Center; - break; - } - }); - }; + updateEffectDirection = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect)); @undoBatch @action - updateEffect = (effect: any, all?: boolean) => { - const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); - array.forEach(doc => { - const tagDoc = doc; //Cast(doc.presentationTargetDoc, Doc, null); - switch (effect) { - case PresEffect.Bounce: - tagDoc.presEffect = PresEffect.Bounce; - break; - case PresEffect.Fade: - tagDoc.presEffect = PresEffect.Fade; - break; - case PresEffect.Flip: - tagDoc.presEffect = PresEffect.Flip; - break; - case PresEffect.Roll: - tagDoc.presEffect = PresEffect.Roll; - break; - case PresEffect.Rotate: - tagDoc.presEffect = PresEffect.Rotate; - break; - case PresEffect.None: - default: - tagDoc.presEffect = PresEffect.None; - break; - } - }); - }; + updateEffect = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffect = effect)); _batch: UndoManager.Batch | undefined = undefined; @@ -1252,6 +1196,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetDoc: Doc = this.targetDoc; const isPresCollection: boolean = targetDoc === this.layoutDoc.presCollection; const isPinWithView: boolean = BoolCast(activeItem.presPinView); + const presEffect = (effect: PresEffect) => ( + <div className={`presBox-dropdownOption ${this.activeItem.presEffect === effect || (effect === PresEffect.None && !this.activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}> + {effect} + </div> + ); + const presMovement = (movement: PresMovement) => ( + <div className={`presBox-dropdownOption ${activeItem.presMovement === movement ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateMovement(movement)}> + {movement} + </div> + ); + const presDirection = (diretion: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => { + const color = this.activeItem.presEffectDirection === diretion || (diretion === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black'; + return ( + <Tooltip title={<div className="dash-tooltip">{diretion}</div>}> + <div + style={{ ...opts, border: diretion === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }} + onClick={() => this.updateEffectDirection(diretion)}> + {icon ? <FontAwesomeIcon icon={icon as any} /> : null} + </div> + </Tooltip> + ); + }; + const inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => { + return ( + <input + type="range" + step={step} + min={min} + max={max} + value={value} + className={`toolbar-slider ${active ? '' : 'none'}`} + onPointerDown={() => (this._batch = UndoManager.StartBatch('pres slider'))} + onPointerUp={() => this._batch?.end()} + onChange={e => { + e.stopPropagation(); + change(e.target.value); + }} + /> + ); + }; if (activeItem && targetDoc) { const type = targetDoc.type; const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; @@ -1262,13 +1246,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom'; return ( <div - className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`} - onPointerDown={e => e.stopPropagation()} - onPointerUp={e => e.stopPropagation()} + className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`} + onPointerDown={StopEvent} + onPointerUp={StopEvent} onClick={action(e => { e.stopPropagation(); - this.openMovementDropdown = false; - this.openEffectDropdown = false; + this._openMovementDropdown = false; + this._openEffectDropdown = false; })}> <div className="ribbon-box"> Movement @@ -1281,24 +1265,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); - this.openMovementDropdown = !this.openMovementDropdown; + this._openMovementDropdown = !this._openMovementDropdown; })} - style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> - {this.setMovementName(activeItem.presMovement, activeItem)} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this.openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? 'grid' : 'none' }}> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None)}> - None - </div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom)}> - Pan {'&'} Zoom - </div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan)}> - Pan - </div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump)}> - Jump cut - </div> + style={{ borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, border: this._openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + {this.movementName(activeItem)} + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={StopEvent} style={{ display: this._openMovementDropdown ? 'grid' : 'none' }}> + {presMovement(PresMovement.None)} + {presMovement(PresMovement.Zoom)} + {presMovement(PresMovement.Pan)} + {presMovement(PresMovement.Jump)} </div> </div> )} @@ -1316,21 +1292,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - <input - type="range" - step="1" - min="0" - max="150" - value={zoom} - className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`} - id="toolbar-slider" - onPointerDown={() => (this._batch = UndoManager.StartBatch('presZoom'))} - onPointerUp={() => this._batch?.end()} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - e.stopPropagation(); - this.setZoom(e.target.value); - }} - /> + {inputter('0', '1', '150', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)} <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> <div className="presBox-subheading">Movement Speed</div> <div className="ribbon-property"> @@ -1345,21 +1307,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - <input - type="range" - step="0.1" - min="0.1" - max="10" - value={transitionSpeed} - className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`} - id="toolbar-slider" - onPointerDown={() => (this._batch = UndoManager.StartBatch('presTransition'))} - onPointerUp={() => this._batch?.end()} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - e.stopPropagation(); - this.setTransitionTime(e.target.value); - }} - /> + {inputter('0.1', '0.1', '10', transitionSpeed, [PresMovement.Pan, PresMovement.Zoom].includes(activeItem.presMovement as any), this.setTransitionTime)} <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> @@ -1370,35 +1318,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Visibility {'&'} Duration <div className="ribbon-doubleButton"> {isPresCollection ? null : ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Hide before presented'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}> <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}> Hide before </div> </Tooltip> )} {isPresCollection ? null : ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Hide after presented'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}> Hide after </div> </Tooltip> )} - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Open in lightbox view'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> <div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}> Lightbox </div> @@ -1420,26 +1353,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - <input - type="range" - step="0.1" - min="0.1" - max="20" - value={duration} - style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'block' }} - className={'toolbar-slider'} - id="duration-slider" - onPointerDown={() => { - this._batch = UndoManager.StartBatch('presDuration'); - }} - onPointerUp={() => { - if (this._batch) this._batch.end(); - }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - e.stopPropagation(); - this.setDurationTime(e.target.value); - }} - /> + {inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)} <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}> <div className="slider-text">Short</div> <div className="slider-text">Medium</div> @@ -1455,98 +1369,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); - this.openEffectDropdown = !this.openEffectDropdown; + this._openEffectDropdown = !this._openEffectDropdown; })} - style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> {effect.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this.openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> - <div - className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.None || !this.activeItem.presEffect ? 'active' : ''}`} - onPointerDown={e => e.stopPropagation()} - onClick={() => this.updateEffect(PresEffect.None)}> - None - </div> - <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Fade ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}> - Fade In - </div> - <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Flip ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}> - Flip - </div> - <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Rotate ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}> - Rotate - </div> - <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Bounce ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}> - Bounce - </div> - <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Roll ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}> - Roll - </div> + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> + {presEffect(PresEffect.None)} + {presEffect(PresEffect.Fade)} + {presEffect(PresEffect.Flip)} + {presEffect(PresEffect.Rotate)} + {presEffect(PresEffect.Bounce)} + {presEffect(PresEffect.Roll)} </div> </div> <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? 'none' : 'inline-flex' }}> <div className="presBox-subheading">Effect direction</div> - <div className="ribbon-property">{this.effectDirection}</div> + <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div> </div> <div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}> - <Tooltip title={<div className="dash-tooltip">{'Enter from left'}</div>}> - <div - style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }} - onClick={() => this.updateEffectDirection(PresEffect.Left)}> - <FontAwesomeIcon icon={'angle-right'} /> - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Enter from right'}</div>}> - <div - style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }} - onClick={() => this.updateEffectDirection(PresEffect.Right)}> - <FontAwesomeIcon icon={'angle-left'} /> - </div> - </Tooltip> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Enter from top'}</div> - </> - }> - <div - style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }} - onClick={() => this.updateEffectDirection(PresEffect.Top)}> - <FontAwesomeIcon icon={'angle-down'} /> - </div> - </Tooltip> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Enter from bottom'}</div> - </> - }> - <div - style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }} - onClick={() => this.updateEffectDirection(PresEffect.Bottom)}> - <FontAwesomeIcon icon={'angle-up'} /> - </div> - </Tooltip> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Enter from center'}</div> - </> - }> - <div - style={{ - gridColumn: 2, - gridRow: 2, - width: 10, - height: 10, - alignSelf: 'center', - justifySelf: 'center', - border: this.activeItem.presEffectDirection === PresEffect.Center || !this.activeItem.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : 'solid 2px black', - borderRadius: '100%', - cursor: 'pointer', - }} - onClick={() => this.updateEffectDirection(PresEffect.Center)}></div> - </Tooltip> + {presDirection(PresEffect.Left, 'angle-right', 1, 2, {})} + {presDirection(PresEffect.Right, 'angle-left', 3, 2, {})} + {presDirection(PresEffect.Top, 'angle-down', 2, 1, {})} + {presDirection(PresEffect.Bottom, 'angle-up', 2, 3, {})} + {presDirection(PresEffect.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} </div> </div> )} @@ -1560,243 +1406,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } - @computed get effectDirection(): string { - let effect = ''; - switch (this.activeItem.presEffectDirection) { - case 'left': - effect = 'Enter from left'; - break; - case 'right': - effect = 'Enter from right'; - break; - case 'top': - effect = 'Enter from top'; - break; - case 'bottom': - effect = 'Enter from bottom'; - break; - default: - effect = 'Enter from center'; - break; - } - return effect; - } - @undoBatch @action applyTo = (array: Doc[]) => { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - this.updateMovement(activeItem.presMovement, true); - this.updateEffect(activeItem.presEffect, true); - this.updateEffectDirection(activeItem.presEffectDirection, true); - array.forEach(doc => { - const curDoc = Cast(doc, Doc, null); - const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); - if (tagDoc && targetDoc) { - curDoc.presTransition = activeItem.presTransition; - curDoc.presDuration = activeItem.presDuration; - curDoc.presHideBefore = activeItem.presHideBefore; - curDoc.presHideAfter = activeItem.presHideAfter; - } + this.updateMovement(this.activeItem.presMovement as PresMovement, true); + this.updateEffect(this.activeItem.presEffect as PresEffect, true); + this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffect, true); + const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem; + array.forEach(curDoc => { + curDoc.presTransition = presTransition; + curDoc.presDuration = presDuration; + curDoc.presHideBefore = presHideBefore; + curDoc.presHideAfter = presHideAfter; }); }; - @computed get presPinViewOptionsDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 16, filter: 'invert(1)' }} />; - return ( - <> - {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : null} - <div className="ribbon-doubleButton"> - <Tooltip - title={ - <> - <div className="dash-tooltip">{activeItem.presPinView ? 'Turn off pin with view' : 'Turn on pin with view'}</div> - </> - }> - <div - className="ribbon-toggle" - style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : '' }} - onClick={() => { - activeItem.presPinView = !activeItem.presPinView; - targetDoc.presPinView = activeItem.presPinView; - if (activeItem.presPinView) { - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) { - const scroll = targetDoc._scrollTop; - activeItem.presPinView = true; - activeItem.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) { - activeItem.presStartTime = targetDoc._currentTimecode; - activeItem.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1; - } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeItem.presPinView = true; - activeItem.presPinViewX = x; - activeItem.presPinViewY = y; - activeItem.presPinViewScale = scale; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const width = targetDoc._clipWidth; - activeItem.presPinClipWidth = width; - activeItem.presPinView = true; - } - } - }}> - {presPinWithViewIcon} - </div> - </Tooltip> - {activeItem.presPinView ? ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Update the pinned view with the view of the selected document'}</div> - </> - }> - <div - className="ribbon-button" - onClick={() => { - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) { - const scroll = targetDoc._scrollTop; - activeItem.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) { - activeItem.presStartTime = targetDoc._currentTimecode; - activeItem.presStartTime = NumCast(targetDoc._currentTimecode) + 0.1; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const clipWidth = targetDoc._clipWidth; - activeItem.presPinClipWidth = clipWidth; - } else { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeItem.presPinViewX = x; - activeItem.presPinViewY = y; - activeItem.presPinViewScale = scale; - } - }}> - Update - </div> - </Tooltip> - ) : null} - </div> - </> - ); - } - - @computed get panOptionsDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - return ( - <> - {this.panable ? ( - <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Pan X</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewX)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewX = Number(val); - })} - /> - </div> - </div> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Pan Y</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewY)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewY = Number(val); - })} - /> - </div> - </div> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Scale</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewScale)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewScale = Number(val); - })} - /> - </div> - </div> - </div> - ) : null} - </> - ); - } - - @computed get scrollOptionsDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - return ( - <> - {this.scrollable ? ( - <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Scroll</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewScroll)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewScroll = Number(val); - })} - /> - </div> - </div> - </div> - ) : null} - </> - ); - } - - @computed get mediaStopSlides() { - const activeItem: Doc = this.activeItem; - const list = this.childDocs.map((doc, i) => { - if (i > this.itemIndex) { - return ( - <option> - {i + 1}. {StrCast(doc.title)} - </option> - ); - } - }); - return list; - } - @computed get mediaOptionsDropdown() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; const clipStart: number = NumCast(activeItem.clipStart); const clipEnd: number = NumCast(activeItem.clipEnd); - const duration = Math.round(NumCast(activeItem[`${Doc.LayoutFieldKey(activeItem)}-duration`]) * 10); const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc); - const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + '. ' + this.childDocs[mediaStopDocInd - 1].title : ''; if (activeItem && targetDoc) { return ( <div> @@ -1967,55 +1597,53 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get newDocumentToolbarDropdown() { return ( - <div> - <div - className={'presBox-toolbar-dropdown'} - style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }} - onClick={e => e.stopPropagation()} - onPointerUp={e => e.stopPropagation()} - onPointerDown={e => e.stopPropagation()}> - <div className="layout-container" style={{ height: 'max-content' }}> - <div - className="layout" - style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} - onClick={action(() => { - this.layout = 'blank'; - this.createNewSlide(this.layout); - })} - /> - <div - className="layout" - style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} - onClick={action(() => { - this.layout = 'title'; - this.createNewSlide(this.layout); - })}> - <div className="title">Title</div> - <div className="subtitle">Subtitle</div> - </div> - <div - className="layout" - style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} - onClick={action(() => { - this.layout = 'header'; - this.createNewSlide(this.layout); - })}> - <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}> - Section header - </div> + <div + className={'presBox-toolbar-dropdown'} + style={{ display: this._newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }} + onClick={e => e.stopPropagation()} + onPointerUp={e => e.stopPropagation()} + onPointerDown={e => e.stopPropagation()}> + <div className="layout-container" style={{ height: 'max-content' }}> + <div + className="layout" + style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'blank'; + this.createNewSlide(this.layout); + })} + /> + <div + className="layout" + style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'title'; + this.createNewSlide(this.layout); + })}> + <div className="title">Title</div> + <div className="subtitle">Subtitle</div> + </div> + <div + className="layout" + style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'header'; + this.createNewSlide(this.layout); + })}> + <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}> + Section header </div> - <div - className="layout" - style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} - onClick={action(() => { - this.layout = 'content'; - this.createNewSlide(this.layout); - })}> - <div className="title" style={{ alignSelf: 'center' }}> - Title - </div> - <div className="content">Text goes here</div> + </div> + <div + className="layout" + style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'content'; + this.createNewSlide(this.layout); + })}> + <div className="title" style={{ alignSelf: 'center' }}> + Title </div> + <div className="content">Text goes here</div> </div> </div> </div> @@ -2029,73 +1657,71 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get newDocumentDropdown() { return ( - <div> - <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> - <div className="ribbon-box"> - Slide Title: <br></br> - <input - className="ribbon-textInput" - placeholder="..." - type="text" - name="fname" - onChange={e => { - e.stopPropagation(); - e.preventDefault(); - runInAction(() => (this.title = e.target.value)); - }}></input> + <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + Slide Title: <br></br> + <input + className="ribbon-textInput" + placeholder="..." + type="text" + name="fname" + onChange={e => { + e.stopPropagation(); + e.preventDefault(); + runInAction(() => (this.title = e.target.value)); + }}></input> + </div> + <div className="ribbon-box"> + Choose type: + <div className="ribbon-doubleButton"> + <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> + Text + </div> + <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> + Freeform + </div> </div> - <div className="ribbon-box"> - Choose type: - <div className="ribbon-doubleButton"> - <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> - Text - </div> - <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> - Freeform + </div> + <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}> + Preset layouts: + <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> + <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} /> + <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}> + <div className="title">Title</div> + <div className="subtitle">Subtitle</div> + </div> + <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}> + <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}> + Section header </div> </div> - </div> - <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}> - Preset layouts: - <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> - <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} /> - <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}> - <div className="title">Title</div> - <div className="subtitle">Subtitle</div> + <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}> + <div className="title" style={{ alignSelf: 'center' }}> + Title </div> - <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}> - <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}> - Section header - </div> + <div className="content">Text goes here</div> + </div> + <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}> + <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}> + Title </div> - <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}> - <div className="title" style={{ alignSelf: 'center' }}> - Title - </div> - <div className="content">Text goes here</div> + <div className="content" style={{ gridColumn: 1, gridRow: 2 }}> + Column one text </div> - <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}> - <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}> - Title - </div> - <div className="content" style={{ gridColumn: 1, gridRow: 2 }}> - Column one text - </div> - <div className="content" style={{ gridColumn: 2, gridRow: 2 }}> - Column two text - </div> + <div className="content" style={{ gridColumn: 2, gridRow: 2 }}> + Column two text </div> </div> - <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}> - <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} /> - </div> </div> - <div className="ribbon-final-box"> - <div - className={this.title !== '' && ((this.addFreeform && this.layout !== '') || !this.addFreeform) ? 'ribbon-final-button-hidden' : 'ribbon-final-button'} - onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}> - Create New Slide - </div> + <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}> + <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} /> + </div> + </div> + <div className="ribbon-final-box"> + <div + className={this.title !== '' && ((this.addFreeform && this.layout !== '') || !this.addFreeform) ? 'ribbon-final-button-hidden' : 'ribbon-final-button'} + onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}> + Create New Slide </div> </div> </div> @@ -2122,48 +1748,29 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; createTemplate = (layout: string, input?: string) => { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - let x = 0; - let y = 0; - if (activeItem && targetDoc) { - x = NumCast(targetDoc.x); - y = NumCast(targetDoc.y) + NumCast(targetDoc._height) + 20; - } - let doc = undefined; - const title = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' }); - const subtitle = Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' }); - const header = Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' }); - const contentTitle = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' }); - const content = Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' }); - const content1 = Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' }); - const content2 = Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' }); + const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0; + const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0; + const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' }); + const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' }); + const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' }); + const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' }); + const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' }); + const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' }); + const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' }); + // prettier-ignore switch (layout) { - case 'blank': - doc = Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x: x, y: y }); - break; - case 'title': - doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - case 'header': - doc = Docs.Create.FreeformDocument([header], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - case 'content': - doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - case 'twoColumns': - doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - default: - break; + case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y }); + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x, y }) } - return doc; }; // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view) @computed get presentDropdown() { return ( - <div className={`dropdown-play ${this.presentTools ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className={`dropdown-play ${this._presentTools ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="dropdown-play-button" onClick={undoBatch( @@ -2221,42 +1828,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /** * Returns the collection type as a string for headers */ - @computed get stringType(): string { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - let type: string = ''; - if (activeItem) { - switch (targetDoc.type) { - case DocumentType.PDF: - type = 'PDF'; - break; - case DocumentType.RTF: - type = 'Text node'; - break; - case DocumentType.COL: - type = 'Collection'; - break; - case DocumentType.AUDIO: - type = 'Audio'; - break; - case DocumentType.VID: - type = 'Video'; - break; - case DocumentType.IMG: - type = 'Image'; - break; - case DocumentType.WEB: - type = 'Web page'; - break; - case DocumentType.MAP: - type = 'Map'; - break; - default: - type = 'Other node'; - break; + @computed get stringType() { + if (this.activeItem) { + // prettier-ignore + switch (this.targetDoc.type) { + case DocumentType.PDF: return 'PDF'; + case DocumentType.RTF: return 'Text node'; + case DocumentType.COL: return 'Collection'; + case DocumentType.AUDIO: return 'Audio'; + case DocumentType.VID: return 'Video'; + case DocumentType.IMG: return 'Image'; + case DocumentType.WEB: return 'Web page'; + case DocumentType.MAP: return 'Map'; + default: return 'Other node'; } } - return type; + return ''; } @observable private openActiveColorPicker: boolean = false; @@ -2269,115 +1856,109 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeFontColor = targetDoc['pres-text-color'] ? StrCast(targetDoc['pres-text-color']) : 'Black'; const viewedFontColor = targetDoc['pres-text-viewed-color'] ? StrCast(targetDoc['pres-text-viewed-color']) : 'Black'; return ( - <div> - <div - className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === 'edit' ? 'active' : ''}`} - onClick={e => e.stopPropagation()} - onPointerUp={e => e.stopPropagation()} - onPointerDown={e => e.stopPropagation()}> - <div className="ribbon-box"> - {this.stringType} selected - <div - className="ribbon-doubleButton" - style={{ - borderTop: 'solid 1px darkgrey', - display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none', - }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}> - Contents - </div> - <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editProgressivize}> - Edit - </div> - </div> - <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}> - <div className="presBox-subheading">Active text color</div> - <div - className="ribbon-colorBox" - style={{ backgroundColor: activeFontColor, height: 15, width: 15 }} - onClick={action(() => { - this.openActiveColorPicker = !this.openActiveColorPicker; - })}></div> + <div className={`presBox-ribbon ${this._progressivizeTools && this.layoutDoc.presStatus === 'edit' ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + {this.stringType} selected + <div + className="ribbon-doubleButton" + style={{ + borderTop: 'solid 1px darkgrey', + display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none', + }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}> + Contents </div> - {this.activeColorPicker} - <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}> - <div className="presBox-subheading">Viewed font color</div> - <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => (this.openViewedColorPicker = !this.openViewedColorPicker))}></div> + <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editProgressivize}> + Edit </div> - {this.viewedColorPicker} + </div> + <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}> + <div className="presBox-subheading">Active text color</div> <div - className="ribbon-doubleButton" - style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? 'inline-flex' : 'none' }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}> - Zoom - </div> - <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editZoomProgressivize}> - Edit - </div> + className="ribbon-colorBox" + style={{ backgroundColor: activeFontColor, height: 15, width: 15 }} + onClick={action(() => { + this.openActiveColorPicker = !this.openActiveColorPicker; + })}></div> + </div> + {this.activeColorPicker} + <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}> + <div className="presBox-subheading">Viewed font color</div> + <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => (this.openViewedColorPicker = !this.openViewedColorPicker))}></div> + </div> + {this.viewedColorPicker} + <div + className="ribbon-doubleButton" + style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? 'inline-flex' : 'none' }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}> + Zoom </div> - <div - className="ribbon-doubleButton" - style={{ - borderTop: 'solid 1px darkgrey', - display: targetDoc._viewType === 'stacking' || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none', - }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}> - Scroll - </div> - <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}> - Edit - </div> + <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editZoomProgressivize}> + Edit </div> </div> - <div className="ribbon-final-box"> - Frames - <div className="ribbon-doubleButton"> - <div className="ribbon-frameSelector"> - <div - key="back" - title="back frame" - className="backKeyframe" - onClick={e => { - e.stopPropagation(); - this.prevKeyframe(targetDoc, activeItem); - }}> - <FontAwesomeIcon icon={'caret-left'} size={'lg'} /> - </div> - <div - key="num" - title="toggle view all" - className="numKeyframe" - style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }} - onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}> - {NumCast(targetDoc._currentFrame)} - </div> - <div - key="fwd" - title="forward frame" - className="fwdKeyframe" - onClick={e => { - e.stopPropagation(); - this.nextKeyframe(targetDoc, activeItem); - }}> - <FontAwesomeIcon icon={'caret-right'} size={'lg'} /> - </div> - </div> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Last frame'}</div> - </> - }> - <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div> - </Tooltip> + <div + className="ribbon-doubleButton" + style={{ + borderTop: 'solid 1px darkgrey', + display: targetDoc._viewType === 'stacking' || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none', + }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}> + Scroll </div> - <div className="ribbon-frameList"> - {this.frameListHeader} - {this.frameList} + <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}> + Edit </div> - <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}> - Play + </div> + </div> + <div className="ribbon-final-box"> + Frames + <div className="ribbon-doubleButton"> + <div className="ribbon-frameSelector"> + <div + key="back" + title="back frame" + className="backKeyframe" + onClick={e => { + e.stopPropagation(); + this.prevKeyframe(targetDoc, activeItem); + }}> + <FontAwesomeIcon icon={'caret-left'} size={'lg'} /> + </div> + <div + key="num" + title="toggle view all" + className="numKeyframe" + style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }} + onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}> + {NumCast(targetDoc._currentFrame)} + </div> + <div + key="fwd" + title="forward frame" + className="fwdKeyframe" + onClick={e => { + e.stopPropagation(); + this.nextKeyframe(targetDoc, activeItem); + }}> + <FontAwesomeIcon icon={'caret-right'} size={'lg'} /> + </div> </div> + <Tooltip + title={ + <> + <div className="dash-tooltip">{'Last frame'}</div> + </> + }> + <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div> + </Tooltip> + </div> + <div className="ribbon-frameList"> + {this.frameListHeader} + {this.frameList} + </div> + <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}> + Play </div> </div> </div> @@ -2388,42 +1969,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action switchActive = (color: ColorState) => { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const val = String(color.hex); - targetDoc['pres-text-color'] = val; + this.targetDoc['pres-text-color'] = String(color.hex); return true; }; @undoBatch @action switchPresented = (color: ColorState) => { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const val = String(color.hex); - targetDoc['pres-text-viewed-color'] = val; + this.targetDoc['pres-text-viewed-color'] = String(color.hex); return true; }; @computed get activeColorPicker() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; return !this.openActiveColorPicker ? null : ( <SketchPicker onChange={this.switchActive} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={StrCast(targetDoc['pres-text-color'])} + color={StrCast(this.targetDoc['pres-text-color'])} /> ); } @computed get viewedColorPicker() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; return !this.openViewedColorPicker ? null : ( <SketchPicker onChange={this.switchPresented} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={StrCast(targetDoc['pres-text-viewed-color'])} + color={StrCast(this.targetDoc['pres-text-viewed-color'])} /> ); } @@ -2466,12 +2037,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //Toggle whether the user edits or not @action editScrollProgressivize = (e: React.MouseEvent) => { - const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; if (!targetDoc.editScrollProgressivize) { if (!targetDoc.scrollProgressivize) { targetDoc.scrollProgressivize = true; - activeItem.scrollProgressivize = true; + this.activeItem.scrollProgressivize = true; } targetDoc.editScrollProgressivize = true; } else { @@ -2483,8 +2053,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action progressivizeScroll = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem: Doc = this.activeItem; - activeItem.scrollProgressivize = !activeItem.scrollProgressivize; + this.activeItem.scrollProgressivize = !this.activeItem.scrollProgressivize; const targetDoc: Doc = this.targetDoc; targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize; // CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame)); @@ -2586,21 +2155,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @observable - toggleDisplayMovement = (doc: Doc) => { - if (doc.displayMovement) doc.displayMovement = false; - else doc.displayMovement = true; - }; + toggleDisplayMovement = (doc: Doc) => (doc.displayMovement = !doc.displayMovement); @action checkList = (doc: Doc, list: any): number => { const x: List<number> = list; - if (x && x.length >= NumCast(doc._currentFrame) + 1) { + if (x?.length >= NumCast(doc._currentFrame) + 1) { return x[NumCast(doc._currentFrame)]; } else if (x) { x.length = NumCast(doc._currentFrame) + 1; x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1]; return x[NumCast(doc._currentFrame)]; - } else return 100; + } + return 100; }; @computed get progressivizeChildDocs() { @@ -2654,26 +2221,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @action - nextAppearFrame = (doc: Doc, i: number): void => { - // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - const appearFrame = Cast(doc.appearFrame, 'number', null); - if (appearFrame === undefined) { - doc.appearFrame = 0; - } - doc.appearFrame = appearFrame + 1; + nextAppearFrame = (doc: Doc, i: number) => { + doc.appearFrame = (Cast(doc.appearFrame, 'number', null) ?? 0) + 1; this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame)); }; @action - prevAppearFrame = (doc: Doc, i: number): void => { - // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - const appearFrame = Cast(doc.appearFrame, 'number', null); - if (appearFrame === undefined) { - doc.appearFrame = 0; - } - doc.appearFrame = Math.max(0, appearFrame - 1); + prevAppearFrame = (doc: Doc, i: number) => { + doc.appearFrame = Math.max(0, (Cast(doc.appearFrame, 'number', null) ?? 0) - 1); this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame)); }; @@ -2702,31 +2257,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - @computed get moreInfoDropdown() { - return <div></div>; - } - @computed get toolbarWidth(): number { - const width = this.props.PanelWidth(); - return width; + return this.props.PanelWidth(); } @action - toggleProperties = () => { - if (SettingsManager.propertiesWidth > 0) { - SettingsManager.propertiesWidth = 0; - } else { - SettingsManager.propertiesWidth = 250; - } - }; + toggleProperties = () => (SettingsManager.propertiesWidth = SettingsManager.propertiesWidth > 0 ? 0 : 250); @computed get toolbar() { const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation; const activeColor = Colors.LIGHT_BLUE; const inactiveColor = Colors.WHITE; return mode === CollectionViewType.Carousel3D ? null : ( @@ -2735,12 +2278,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={"plus"} /> <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> </div></Tooltip> */} - <Tooltip - title={ - <> - <div className="dash-tooltip">{'View paths'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'View paths'}</div>}> <div style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }} className={'toolbar-button'} @@ -2759,22 +2297,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> <div className="toolbar-divider" /> */} - <Tooltip - title={ - <> - <div className="dash-tooltip">{presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div> - </> - }> - <div className="toolbar-button" style={{ cursor: presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> - <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: presKeyEvents ? activeColor : inactiveColor }} /> + <Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}> + <div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> + <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} /> </div> </Tooltip> - <Tooltip - title={ - <> - <div className="dash-tooltip">{propTitle}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}> <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.propertiesWidth > 0 ? activeColor : inactiveColor }} /> </div> @@ -2797,14 +2325,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> {isMini ? null : ( <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> - <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}> + <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}> List </option> - <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Tree}> + <option onPointerDown={StopEvent} value={CollectionViewType.Tree}> Tree </option> {Doc.noviceMode ? null : ( - <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}> + <option onPointerDown={StopEvent} value={CollectionViewType.Carousel3D}> 3D Carousel </option> )} @@ -2825,11 +2353,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> {mode === CollectionViewType.Carousel3D || isMini ? null : ( <div - className={`presBox-button-right ${this.presentTools ? 'active' : ''}`} + className={`presBox-button-right ${this._presentTools ? 'active' : ''}`} onClick={action(() => { - if (this.childDocs.length) this.presentTools = !this.presentTools; + if (this.childDocs.length) this._presentTools = !this._presentTools; })}> - <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} /> + <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} /> {this.presentDropdown} </div> )} @@ -2841,16 +2369,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @action - getList = (list: any): List<number> => { - const x: List<number> = list; - return x; - }; + getList = (list: any): List<number> => list; @action updateList = (list: any): List<number> => { const targetDoc: Doc = this.targetDoc; const x: List<number> = list; - x.length + 1; x[x.length - 1] = NumCast(targetDoc._scrollY); return x; }; @@ -2858,17 +2382,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action newFrame = () => { const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const type: string = StrCast(targetDoc.type); + const type: string = StrCast(this.targetDoc.type); if (!activeItem.frameList) activeItem.frameList = new List<number>(); switch (type) { case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB: this.updateList(activeItem.frameList); break; - case DocumentType.COL: - break; - default: - break; } }; @@ -2877,12 +2396,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="frameList-header"> Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null} <div className={'frameList-headerButtons'}> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Add frame by example'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Add frame by example'}</div>}> <div className={'headerButton'} onClick={e => { @@ -2892,12 +2406,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Edit in collection'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}> <div className={'headerButton'} onClick={e => { @@ -2913,27 +2422,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get frameList() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const frameList: List<number> = this.getList(activeItem.frameList); - if (frameList) { - const frameItems = frameList.map(value => <div className="framList-item"></div>); - return <div className="frameList-container">{frameItems}</div>; - } else return null; + const frameList: List<number> = this.getList(this.activeItem.frameList); + return !frameList ? null : ( + <div className="frameList-container"> + {frameList.map(value => ( + <div className="framList-item" /> + ))} + </div> + ); } @computed get playButtonFrames() { - const targetDoc: Doc = this.targetDoc; - return ( - <> - {this.targetDoc ? ( - <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}> - <div>{NumCast(targetDoc._currentFrame)}</div> - <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> - <div>{NumCast(targetDoc.lastFrame)}</div> - </div> - ) : null} - </> + const targetDoc = this.targetDoc; + return !this.targetDoc ? null : ( + <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}> + <div>{NumCast(targetDoc._currentFrame)}</div> + <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> + <div>{NumCast(targetDoc.lastFrame)}</div> + </div> ); } @@ -2943,17 +2449,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 1: There are still other frames and should go through all frames before going to next slide return ( <div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Loop'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}> <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop)}> <FontAwesomeIcon icon={'redo-alt'} /> </div> </Tooltip> - <div className="presPanel-divider"></div> + <div className="presPanel-divider" /> <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} @@ -2966,12 +2467,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }}> <FontAwesomeIcon icon={'arrow-left'} /> </div> - <Tooltip - title={ - <> - <div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onClick={this.startOrPause}> <FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} /> </div> @@ -2989,12 +2485,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={'arrow-right'} /> </div> <div className="presPanel-divider"></div> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Click to return to 1st slide'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}> <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}> <b>1</b> </div> @@ -3056,9 +2547,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @action - startMarqueeCreateSlide = () => { - PresBox.startMarquee = true; - }; + startMarqueeCreateSlide = () => (PresBox.startMarquee = true); AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => { var indexNum = 0; @@ -3088,23 +2577,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); render() { - // calling this method for keyEvents - this.isPres; // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; - const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation; const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1; const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; return DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc) ? ( - <div className="miniPres" onClick={e => e.stopPropagation()}> - <div className="presPanelOverlay" style={{ display: 'inline-flex', height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Loop'}</div> - </> - }> + <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> + <div className="presPanelOverlay" style={{ display: 'inline-flex', height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: this._presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> + <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}> <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} @@ -3116,12 +2597,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}> <FontAwesomeIcon icon={'arrow-left'} /> </div> - <Tooltip - title={ - <> - <div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.startOrPause, false, false)}> <FontAwesomeIcon icon={this.layoutDoc.presStatus === 'auto' ? 'pause' : 'play'} /> </div> @@ -3130,12 +2606,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={'arrow-right'} /> </div> <div className="presPanel-divider"></div> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Click to return to 1st slide'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}> <div className="presPanel-button" style={{ border: 'solid 1px white' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.gotoDocument(0, this.activeItem), false, false)}> <b>1</b> </div> @@ -3201,5 +2672,17 @@ ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) { CollectionDockingView.AddSplit(doc, 'right'); finished?.(); }; - DocumentManager.Instance.jumpToDocument(bestTarget, true, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, () => PresBox.navigateToDoc(bestTarget, activeItem, true), undefined, true, NumCast(activeItem.presZoom)); + DocumentManager.Instance.jumpToDocument( + bestTarget, + true, + openInTab, + srcContext ? [srcContext] : [], + undefined, + undefined, + undefined, + () => PresBox.restoreTargetDocView(bestTarget, activeItem, true), + undefined, + true, + NumCast(activeItem.presZoom) + ); }); diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 1a2054676..d88ff45fa 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; +import { Copy, Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; @@ -24,6 +24,8 @@ import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; import React = require('react'); +import { InkField } from '../../../../fields/InkField'; +import { RichTextField } from '../../../../fields/RichTextField'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. @@ -47,6 +49,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get presStatus() { return this.presBox.presStatus; } + @computed get selectedArray() { + return this.presBoxView?.selectedArray; + } + @computed get presBoxView() { + const vpath = this.props.docViewPath(); + return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as PresBox) : undefined; + } @computed get presBox() { return (this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc)!; } @@ -72,9 +81,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord]; @action - presExpandDocumentClick = () => { - this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; - }; + presExpandDocumentClick = () => (this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton); embedHeight = (): number => 97; // embedWidth = () => this.props.PanelWidth(); @@ -129,10 +136,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presItem-groupSlide" onClick={e => { - console.log('Clicked on slide with index: ', ind); e.stopPropagation(); e.preventDefault(); - PresBox.Instance.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); this.presExpandDocumentClick(); }}> <div className="presItem-groupNum">{`${ind + 1}.`}</div> @@ -154,15 +160,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { )); return groupSlides; } - @computed get duration() { - let durationInS: number; - if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { - durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); - durationInS = Math.round(durationInS * 10) / 10; - } else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000; - else durationInS = 2; - return 'D: ' + durationInS + 's'; - } @computed get transition() { let transitionInS: number; @@ -181,15 +178,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); e.preventDefault(); if (element && !(e.ctrlKey || e.metaKey)) { - if (PresBox.Instance._selectedArray.has(this.rootDoc)) { - PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false); + if (this.selectedArray?.has(this.rootDoc)) { + this.selectedArray.size === 1 && this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false); setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); } else { setupMoveUpEvents( this, e, (e: PointerEvent) => { - PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false); + this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false); return this.startDrag(e); }, emptyFunction, @@ -199,19 +196,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - headerUp = (e: React.PointerEvent<HTMLDivElement>) => { - e.stopPropagation(); - e.preventDefault(); - }; - /** * Function to drag and drop the pres element to a diferent location */ startDrag = (e: PointerEvent) => { const miniView: boolean = this.toolbarWidth <= 100; const activeItem = this.rootDoc; - const dragArray = PresBox.Instance._dragArray; - const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray()); + const dragArray = this.presBoxView?._dragArray ?? []; + const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); dragData.dropAction = 'move'; dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc; @@ -219,12 +211,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragItem: HTMLElement[] = []; if (dragArray.length === 1) { const doc = this._itemRef.current || dragArray[0]; - doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide'; - dragItem.push(doc); + if (doc) { + doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide'; + dragItem.push(doc); + } } else if (dragArray.length >= 1) { const doc = document.createElement('div'); doc.className = 'presItem-multiDrag'; - doc.innerText = 'Move ' + PresBox.Instance._selectedArray.size + ' slides'; + doc.innerText = 'Move ' + this.selectedArray?.size + ' slides'; doc.style.position = 'absolute'; doc.style.top = e.clientY + 'px'; doc.style.left = e.clientX - 50 + 'px'; @@ -253,10 +247,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerMove = (e: PointerEvent) => { const slide = this._itemRef.current!; - let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false; - for (const doc of DragManager.docsBeingDragged) { - if (!doc.presentationTargetDoc) dragIsPresItem = false; - } + const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentationTargetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); const y = e.clientY - rect.top; //y position within the element. @@ -289,10 +280,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch removeItem = action((e: React.MouseEvent) => { e.stopPropagation(); - this.props.removeDocument?.(this.rootDoc); - if (PresBox.Instance._selectedArray.has(this.rootDoc)) { - PresBox.Instance._selectedArray.delete(this.rootDoc); + if (this.indexInPres < (this.presBoxView?.itemIndex || 0)) { + this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1; } + this.props.removeDocument?.(this.rootDoc); + this.presBoxView?.removeFromSelectedArray(this.rootDoc); this.removeAllRecordingInOverlay(); }); @@ -319,6 +311,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { case DocumentType.RTF: const scroll = targetDoc._scrollTop; activeItem.presPinViewScroll = scroll; + if (targetDoc.type === DocumentType.RTF) { + activeItem.presData = targetDoc.text instanceof RichTextField ? targetDoc.text[Copy]() : targetDoc.text; + } + break; + case DocumentType.INK: + activeItem.presData = targetDoc.data instanceof InkField ? targetDoc.data[Copy]() : targetDoc.data; break; case DocumentType.VID: case DocumentType.AUDIO: @@ -336,6 +334,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.presPinViewY = y; activeItem.presPinViewScale = scale; } + + activeItem.presX = NumCast(targetDoc.x); + activeItem.presY = NumCast(targetDoc.y); + activeItem.presRot = NumCast(targetDoc.jitterRotation); + activeItem.presWidth = NumCast(targetDoc.width); + activeItem.presHeight = NumCast(targetDoc.height); }; @computed get recordingIsInOverlay() { @@ -380,7 +384,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { hideRecording = (e: React.MouseEvent, iconClick: boolean = false) => { e.stopPropagation(); this.removeAllRecordingInOverlay(); - // if (iconClick) PresElementBox.showVideo = false; }; @@ -400,7 +403,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action - startRecording = (activeItem: Doc) => { + startRecording = (e: React.MouseEvent, activeItem: Doc) => { + e.stopPropagation(); console.log('start recording', 'activeItem', activeItem); if (PresElementBox.videoIsRecorded(activeItem)) { // if we already have an existing recording @@ -444,7 +448,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get mainItem() { - const isSelected: boolean = PresBox.Instance?._selectedArray.has(this.rootDoc); + const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false; const toolbarWidth: number = this.toolbarWidth; const showMore: boolean = this.toolbarWidth >= 300; const miniView: boolean = this.toolbarWidth <= 110; @@ -466,109 +470,71 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={e => { e.stopPropagation(); e.preventDefault(); - PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + this.presBoxView?.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); this.showRecording(activeItem); }} onDoubleClick={action(e => { this.toggleProperties(); - PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); + this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} - onPointerDown={this.headerDown} - onPointerUp={this.headerUp}> - {/* {miniView ? - // when width is LESS than 110 px - <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}> - {`${this.indexInPres + 1}.`} - </div> - : - // when width is MORE than 110 px - <div className="presItem-number"> - {`${this.indexInPres + 1}.`} - </div>} */} - {/* <div className="presItem-number"> - {`${this.indexInPres + 1}.`} - </div> */} - {miniView ? null : ( + onPointerDown={this.headerDown}> + {miniView ? ( + <div className={`presItem-miniSlide ${isSelected ? 'active' : ''}`} ref={this._dragRef}> + {`${this.indexInPres + 1}.`} + </div> + ) : ( <div - ref={miniView ? null : this._dragRef} + ref={this._dragRef} className={`presItem-slide ${isSelected ? 'active' : ''}`} style={{ backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isSelected ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, }}> - <div className="presItem-name" style={{ maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> + <div + className="presItem-name" + style={{ + pointerEvents: isSelected ? undefined : 'none', + maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, + cursor: isSelected ? 'text' : 'grab', + }}> <div>{`${this.indexInPres + 1}. `}</div> <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> </div> {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} - <div className={'presItem-slideButtons'}> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Update view'}</div> - </> - }> + <div className="presItem-slideButtons"> + <Tooltip title={<div className="dash-tooltip">Update view</div>}> <div className="slideButton" onClick={() => this.updateView(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinView ? 'flex' : 'none' }}> V </div> </Tooltip> - - {this.recordingIsInOverlay ? ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Hide Recording'}</div> - </> - }> - <div className="slideButton" onClick={e => this.hideRecording(e, true)} style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={'video-slash'} onPointerDown={e => e.stopPropagation()} /> - </div> - </Tooltip> - ) : ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{`${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div> - </> - }> - <div - className="slideButton" - onClick={e => { - e.stopPropagation(); - this.startRecording(activeItem); - }} - style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={'video'} onPointerDown={e => e.stopPropagation()} /> + <Tooltip title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> + <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> + <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}> + <div + className="slideButton" + onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)} + style={{ + display: this.indexInPres === 0 ? 'none' : '', + zIndex: 1000 - this.indexInPres, + fontWeight: 700, + backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, + height: activeItem.groupWithUp ? 53 : 18, + transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined, + }}> + <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}> + <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} /> </div> - </Tooltip> - )} - - {/* {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}> - <div className="slideButton" - onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp} - style={{ - zIndex: 1000 - this.indexInPres, - fontWeight: 700, - backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined, - height: activeItem.groupWithUp ? 53 : 18, - transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined - }}> - <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}> - <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} /> - </div> - </div> - </Tooltip>} */} - <Tooltip - title={ - <> - <div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div> - </> - }> + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}> <div - className={'slideButton'} + className="slideButton" onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); @@ -576,18 +542,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Remove from presentation'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">Remove from presentation</div>}> <div className={'slideButton'} onClick={this.removeItem}> <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> </div> - {/* <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> */} {this.renderEmbeddedInline} </div> )} diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts index 93ab323fb..c6a222c3a 100644 --- a/src/client/views/nodes/trails/PresEnums.ts +++ b/src/client/views/nodes/trails/PresEnums.ts @@ -1,28 +1,28 @@ export enum PresMovement { - Zoom = "zoom", - Pan = "pan", - Jump = "jump", - None = "none", + Zoom = 'zoom', + Pan = 'pan', + Jump = 'jump', + None = 'none', } export enum PresEffect { - Zoom = "Zoom", - Lightspeed = "Lightspeed", - Fade = "Fade in", - Flip = "Flip", - Rotate = "Rotate", - Bounce = "Bounce", - Roll = "Roll", - None = "None", - Left = "left", - Right = "right", - Center = "center", - Top = "top", - Bottom = "bottom" + Zoom = 'Zoom', + Lightspeed = 'Lightspeed', + Fade = 'Fade in', + Flip = 'Flip', + Rotate = 'Rotate', + Bounce = 'Bounce', + Roll = 'Roll', + None = 'None', + Left = 'Enter from left', + Right = 'Enter from right', + Center = 'Enter from center', + Top = 'Enter from Top', + Bottom = 'Enter from bottom', } export enum PresStatus { - Autoplay = "auto", - Manual = "manual", - Edit = "edit" -}
\ No newline at end of file + Autoplay = 'auto', + Manual = 'manual', + Edit = 'edit', +} diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 29d068817..ee2ae10a7 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -1,16 +1,16 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from "@material-ui/core"; -import { action, computed, observable, IReactionDisposer, reaction, ObservableMap } from "mobx"; -import { observer } from "mobx-react"; -import { ColorState } from "react-color"; -import { Doc, Opt } from "../../../fields/Doc"; -import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from "../../../Utils"; -import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; -import { ButtonDropdown } from "../nodes/formattedText/RichTextMenu"; -import "./AnchorMenu.scss"; -import { SelectionManager } from "../../util/SelectionManager"; -import { LinkPopup } from "../linking/LinkPopup"; +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { ColorState } from 'react-color'; +import { Doc, Opt } from '../../../fields/Doc'; +import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../Utils'; +import { SelectionManager } from '../../util/SelectionManager'; +import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; +import { LinkPopup } from '../linking/LinkPopup'; +import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu'; +import './AnchorMenu.scss'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -19,35 +19,37 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { private _disposer: IReactionDisposer | undefined; private _commentCont = React.createRef<HTMLButtonElement>(); private _palette = [ - "rgba(208, 2, 27, 0.8)", - "rgba(238, 0, 0, 0.8)", - "rgba(245, 166, 35, 0.8)", - "rgba(248, 231, 28, 0.8)", - "rgba(245, 230, 95, 0.616)", - "rgba(139, 87, 42, 0.8)", - "rgba(126, 211, 33, 0.8)", - "rgba(65, 117, 5, 0.8)", - "rgba(144, 19, 254, 0.8)", - "rgba(238, 169, 184, 0.8)", - "rgba(224, 187, 228, 0.8)", - "rgba(225, 223, 211, 0.8)", - "rgba(255, 255, 255, 0.8)", - "rgba(155, 155, 155, 0.8)", - "rgba(0, 0, 0, 0.8)"]; - - @observable private _keyValue: string = ""; - @observable private _valueValue: string = ""; + 'rgba(208, 2, 27, 0.8)', + 'rgba(238, 0, 0, 0.8)', + 'rgba(245, 166, 35, 0.8)', + 'rgba(248, 231, 28, 0.8)', + 'rgba(245, 230, 95, 0.616)', + 'rgba(139, 87, 42, 0.8)', + 'rgba(126, 211, 33, 0.8)', + 'rgba(65, 117, 5, 0.8)', + 'rgba(144, 19, 254, 0.8)', + 'rgba(238, 169, 184, 0.8)', + 'rgba(224, 187, 228, 0.8)', + 'rgba(225, 223, 211, 0.8)', + 'rgba(255, 255, 255, 0.8)', + 'rgba(155, 155, 155, 0.8)', + 'rgba(0, 0, 0, 0.8)', + ]; + + @observable private _keyValue: string = ''; + @observable private _valueValue: string = ''; @observable private _added: boolean = false; - @observable private highlightColor: string = "rgba(245, 230, 95, 0.616)"; + @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)'; @observable private _showLinkPopup: boolean = false; @observable public Highlighting: boolean = false; - @observable public Status: "marquee" | "annotation" | "" = ""; + @observable public Status: 'marquee' | 'annotation' | '' = ''; public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search public OnCrop: (e: PointerEvent) => void = unimplementedFunction; public OnClick: (e: PointerEvent) => void = unimplementedFunction; + public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined; @@ -57,7 +59,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public PinToPres: () => void = unimplementedFunction; public MakePushpin: () => void = unimplementedFunction; public IsPushpin: () => boolean = returnFalse; - public get Active() { return this._left > 0; } + public get Active() { + return this._left > 0; + } constructor(props: Readonly<{}>) { super(props); @@ -67,26 +71,44 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { } componentDidMount() { - this._disposer = reaction(() => SelectionManager.Views(), + this._disposer = reaction( + () => SelectionManager.Views(), selected => { this._showLinkPopup = false; AnchorMenu.Instance.fadeOut(true); - }); + } + ); } pointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e: PointerEvent) => { - this.StartDrag(e, this._commentCont.current!); - return true; - }, returnFalse, e => this.OnClick?.(e)); - } + setupMoveUpEvents( + this, + e, + (e: PointerEvent) => { + this.StartDrag(e, this._commentCont.current!); + return true; + }, + returnFalse, + e => this.OnClick?.(e) + ); + }; + + audioDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); + }; cropDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e: PointerEvent) => { - this.StartCropDrag(e, this._commentCont.current!); - return true; - }, returnFalse, e => this.OnCrop?.(e)); - } + setupMoveUpEvents( + this, + e, + (e: PointerEvent) => { + this.StartCropDrag(e, this._commentCont.current!); + return true; + }, + returnFalse, + e => this.OnCrop?.(e) + ); + }; @action highlightClicked = (e: React.MouseEvent) => { @@ -94,41 +116,45 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this.Highlighting = !this.Highlighting; } AnchorMenu.Instance.fadeOut(true); - } + }; @action toggleLinkPopup = (e: React.MouseEvent) => { //ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button //change popup visibility field to visible this._showLinkPopup = !this._showLinkPopup; - } + }; @computed get highlighter() { - const button = + const button = ( <button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}> - <div className="anchor-color-preview" > - <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> + <div className="anchor-color-preview"> + <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: this.Highlighting ? '' : 'rotate(-45deg)' }} /> <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> </div> - </button>; + </button> + ); - const dropdownContent = + const dropdownContent = ( <div className="dropdown"> <p>Change highlighter color:</p> <div className="color-wrapper"> {this._palette.map(color => { if (color) { - return this.highlightColor === color ? - <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button> : - <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button>; + return this.highlightColor === color ? ( + <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button> + ) : ( + <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button> + ); } })} </div> - </div>; + </div> + ); return ( - <Tooltip key="highlighter" title={<div className="dash-tooltip">{"Click to Highlight"}</div>}> + <Tooltip key="highlighter" title={<div className="dash-tooltip">{'Click to Highlight'}</div>}> <div className="anchorMenu-highlighter"> - <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} pdf={true} /> + <ButtonDropdown key={'highlighter'} button={button} dropdownContent={dropdownContent} pdf={true} /> </div> </Tooltip> ); @@ -136,70 +162,96 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @action changeHighlightColor = (color: string, e: React.PointerEvent) => { const col: ColorState = { - hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, - rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", + hex: color, + hsl: { a: 0, h: 0, s: 0, l: 0, source: '' }, + hsv: { a: 0, h: 0, s: 0, v: 0, source: '' }, + rgb: { a: 0, r: 0, b: 0, g: 0, source: '' }, + oldHue: 0, + source: '', }; e.preventDefault(); e.stopPropagation(); this.highlightColor = Utils.colorString(col); - } + }; - @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this._keyValue = e.currentTarget.value; }; - @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this._valueValue = e.currentTarget.value; }; + @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._keyValue = e.currentTarget.value; + }; + @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._valueValue = e.currentTarget.value; + }; @action addTag = (e: React.PointerEvent) => { if (this._keyValue.length > 0 && this._valueValue.length > 0) { this._added = this.AddTag(this._keyValue, this._valueValue); - setTimeout(action(() => this._added = false), 1000); + setTimeout( + action(() => (this._added = false)), + 1000 + ); } - } + }; render() { - const buttons = this.Status === "marquee" ? - [ - this.highlighter, - - <Tooltip key="annotate" title={<div className="dash-tooltip">{"Drag to Place Annotation"}</div>}> - <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: "grab" }}> - <FontAwesomeIcon icon="comment-alt" size="lg" /> - </button> - </Tooltip>, - //NOTE: link popup is currently in progress - <Tooltip key="link" title={<div className="dash-tooltip">{"Find document to link to selected text"}</div>}> - <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}> - <FontAwesomeIcon style={{ position: "absolute", transform: "scale(1.5)" }} icon={"search"} size="lg" /> - <FontAwesomeIcon style={{ position: "absolute", transform: "scale(0.5)", transformOrigin: "top left", top: 12, left: 12 }} icon={"link"} size="lg" /> - </button> - </Tooltip>, - <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />, - AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? <></> : <Tooltip key="crop" title={<div className="dash-tooltip">{"Click/Drag to create cropped image"}</div>}> - <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: "grab" }}> - <FontAwesomeIcon icon="image" size="lg" /> - </button> - </Tooltip>, - ] : [ - <Tooltip key="trash" title={<div className="dash-tooltip">{"Remove Link Anchor"}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.Delete}> - <FontAwesomeIcon icon="trash-alt" size="lg" /> - </button> - </Tooltip>, - <Tooltip key="Pin" title={<div className="dash-tooltip">{"Pin to Presentation"}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.PinToPres}> - <FontAwesomeIcon icon="map-pin" size="lg" /> - </button> - </Tooltip>, - <Tooltip key="pushpin" title={<div className="dash-tooltip">{"toggle pushpin behavior"}</div>}> - <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? "black" : "white", backgroundColor: this.IsPushpin() ? "white" : "black" }} onPointerDown={this.MakePushpin}> - <FontAwesomeIcon icon="thumbtack" size="lg" /> - </button> - </Tooltip>, - // <div key="7" className="anchorMenu-addTag" > - // <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> - // <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> - // </div>, - // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> - // <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, - ]; + const buttons = + this.Status === 'marquee' + ? [ + this.highlighter, + + <Tooltip key="annotate" title={<div className="dash-tooltip">{'Drag to Place Annotation'}</div>}> + <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}> + <FontAwesomeIcon icon="comment-alt" size="lg" /> + </button> + </Tooltip>, + AnchorMenu.Instance.OnAudio === unimplementedFunction ? ( + <></> + ) : ( + <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">{'Click to Record Annotation'}</div>}> + <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}> + <FontAwesomeIcon icon="microphone" size="lg" /> + </button> + </Tooltip> + ), + //NOTE: link popup is currently in progress + <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}> + <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" /> + </button> + </Tooltip>, + <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />, + AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? ( + <></> + ) : ( + <Tooltip key="crop" title={<div className="dash-tooltip">{'Click/Drag to create cropped image'}</div>}> + <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}> + <FontAwesomeIcon icon="image" size="lg" /> + </button> + </Tooltip> + ), + ] + : [ + <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.Delete}> + <FontAwesomeIcon icon="trash-alt" size="lg" /> + </button> + </Tooltip>, + <Tooltip key="Pin" title={<div className="dash-tooltip">{'Pin to Presentation'}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.PinToPres}> + <FontAwesomeIcon icon="map-pin" size="lg" /> + </button> + </Tooltip>, + <Tooltip key="pushpin" title={<div className="dash-tooltip">{'toggle pushpin behavior'}</div>}> + <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? 'black' : 'white', backgroundColor: this.IsPushpin() ? 'white' : 'black' }} onPointerDown={this.MakePushpin}> + <FontAwesomeIcon icon="thumbtack" size="lg" /> + </button> + </Tooltip>, + // <div key="7" className="anchorMenu-addTag" > + // <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> + // <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> + // </div>, + // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> + // <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, + ]; return this.getElement(buttons); } -}
\ No newline at end of file +} diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 44f815336..ee418a02f 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -107,7 +107,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { pointerEvents: this.props.pointerEvents?.() as any, outline: brushed === Doc.DocBrushStatus.linkHighlighted ? 'solid 1px lightBlue' : undefined, backgroundColor: brushed === Doc.DocBrushStatus.highlighted ? 'orange' : StrCast(this.props.document.backgroundColor), - }}></div> + }} + /> ); } } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 837734edf..53d969c0a 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -9,6 +9,7 @@ import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; +import { DragManager } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -41,7 +42,6 @@ interface IViewerProps extends FieldViewProps { url: string; loaded?: (nw: number, nh: number, np: number) => void; setPdfViewer: (view: PDFViewer) => void; - ContentScaling?: () => number; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined; } @@ -97,7 +97,7 @@ export class PDFViewer extends React.Component<IViewerProps> { autoHeight => { if (autoHeight) { this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']); - this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.scaling?.() || 1)); + this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); } } ); @@ -162,13 +162,11 @@ export class PDFViewer extends React.Component<IViewerProps> { const mainCont = this._mainCont.current; let focusSpeed: Opt<number>; if (doc !== this.props.rootDoc && mainCont) { - const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1); + const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight)); if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) { - focusSpeed = 500; - if (!this._pdfViewer) this._initialScroll = scrollTo; - else if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo); + else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo); else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) }); } } else { @@ -486,11 +484,10 @@ export class PDFViewer extends React.Component<IViewerProps> { showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1)); - panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); - panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); - basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')]; + panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); + panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (doc.textInlineAnnotations) return 'none'; @@ -499,67 +496,50 @@ export class PDFViewer extends React.Component<IViewerProps> { return this.props.styleProvider?.(doc, props, property); }; - renderAnnotations = (docFilters?: () => string[], dontRender?: boolean) => ( - <CollectionFreeFormView - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} - isAnnotationOverlay={true} - fieldKey={this.props.fieldKey + '-annotations'} - setPreviewCursor={this.setPreviewCursor} - PanelHeight={this.panelHeight} - PanelWidth={this.panelWidth} - dropAction={'alias'} - select={emptyFunction} - ContentScaling={this.contentZoom} - bringToFront={emptyFunction} - docFilters={docFilters || this.basicFilter} - styleProvider={this.childStyleProvider} - dontRenderDocuments={dontRender} - CollectionView={undefined} - ScreenToLocalTransform={this.overlayTransform} - renderDepth={this.props.renderDepth + 1} - /> + renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => ( + <div + className="pdfViewerDash-overlay" + style={{ + mixBlendMode: mixBlendMode, + display: display, + transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, + pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined, + }}> + <CollectionFreeFormView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + renderDepth={this.props.renderDepth + 1} + isAnnotationOverlay={true} + fieldKey={this.props.fieldKey + '-annotations'} + CollectionView={undefined} + setPreviewCursor={this.setPreviewCursor} + PanelHeight={this.panelHeight} + PanelWidth={this.panelWidth} + ScreenToLocalTransform={this.overlayTransform} + dropAction={'alias'} + docFilters={docFilters} + select={emptyFunction} + bringToFront={emptyFunction} + styleProvider={this.childStyleProvider} + /> + </div> ); @computed get overlayTransparentAnnotations() { - return this.renderAnnotations(this.transparentFilter, false); + return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined); } @computed get overlayOpaqueAnnotations() { - return this.renderAnnotations(this.opaqueFilter, false); - } - @computed get overlayClickableAnnotations() { - return <div style={{ height: NumCast(this.props.rootDoc.scrollHeight) }}>{this.renderAnnotations(undefined, true)}</div>; + return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined); } @computed get overlayLayer() { return ( <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}> - <div - className="pdfViewerDash-overlay" - style={{ - pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none', - mixBlendMode: 'multiply', - transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, - }}> - {this.overlayTransparentAnnotations} - </div> - <div - className="pdfViewerDash-overlay" - style={{ - pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none', - mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined, - transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, - }}> - {this.overlayOpaqueAnnotations} - {this.overlayClickableAnnotations} - </div> + {this.overlayTransparentAnnotations} + {this.overlayOpaqueAnnotations} </div> ); } @computed get pdfViewerDiv() { return <div className={'pdfViewerDash-text' + (this.props.pointerEvents?.() !== 'none' && this._textSelecting && this.props.isContentActive() ? '-selected' : '')} ref={this._viewer} />; } - @computed get contentScaling() { - return this.props.ContentScaling?.() || 1; - } - contentZoom = () => NumCast(this.props.layoutDoc._viewScale, 1); savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); @@ -574,8 +554,7 @@ export class PDFViewer extends React.Component<IViewerProps> { onClick={this.onClick} style={{ overflowX: NumCast(this.props.layoutDoc._viewScale, 1) !== 1 ? 'scroll' : undefined, - height: !this.props.Document._fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `${100 / this.contentScaling}%`, - transform: `scale(${this.contentScaling})`, + height: !this.props.Document._fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`, }}> {this.pdfViewerDiv} {this.annotationLayer} diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 0bf1575fb..cbcfed06f 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,5 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; +import { MdBugReport } from 'react-icons/md'; import { action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -18,6 +19,7 @@ import { DashboardView } from '../DashboardView'; import { Borders, Colors } from '../global/globalEnums'; import { MainView } from '../MainView'; import './TopBar.scss'; +import { ReportManager } from '../../util/ReportManager'; /** * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user @@ -26,11 +28,12 @@ import './TopBar.scss'; @observer export class TopBar extends React.Component { navigateToHome = () => { - CollectionDockingView.Instance.CaptureThumbnail()?.then(() => { + CollectionDockingView.Instance?.CaptureThumbnail()?.then(() => { Doc.ActivePage = 'home'; DashboardView.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use }); }; + render() { const activeDashboard = Doc.ActiveDashboard; return ( @@ -92,6 +95,9 @@ export class TopBar extends React.Component { {GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'view original'} </div> ) : null} + <div className="topbar-button-icon" onClick={() => ReportManager.Instance.open()}> + <MdBugReport /> + </div> <div className="topbar-button-icon" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')}> <FontAwesomeIcon icon="question-circle" /> </div> diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index ee7dd1fc1..02038c426 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -1,5 +1,4 @@ import { action, configure, observable, ObservableMap, Lambda } from 'mobx'; -import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { observer } from 'mobx-react'; @@ -21,7 +20,6 @@ URLField; ScriptField; CursorField; - function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean; function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean; function applyToDoc(doc: any, key: string | number, scriptString: string): boolean { @@ -37,11 +35,11 @@ function applyToDoc(doc: any, key: string | number, scriptString: string): boole } configure({ - enforceActions: "observed" + enforceActions: 'observed', }); @observer -class ListViewer extends React.Component<{ field: List<Field> }>{ +class ListViewer extends React.Component<{ field: List<Field> }> { @observable expanded = false; @@ -49,14 +47,16 @@ class ListViewer extends React.Component<{ field: List<Field> }>{ onClick = (e: React.MouseEvent) => { this.expanded = !this.expanded; e.stopPropagation(); - } + }; render() { let content; if (this.expanded) { content = ( <div> - {this.props.field.map((field, index) => <DebugViewer field={field} key={index} setValue={value => applyToDoc(this.props.field, index, value)} />)} + {this.props.field.map((field, index) => ( + <DebugViewer field={field} key={index} setValue={value => applyToDoc(this.props.field, index, value)} /> + ))} </div> ); } else { @@ -66,7 +66,7 @@ class ListViewer extends React.Component<{ field: List<Field> }>{ <div> <button onClick={this.onClick}>Toggle</button> {content} - </div > + </div> ); } } @@ -80,7 +80,7 @@ class DocumentViewer extends React.Component<{ field: Doc }> { onClick = (e: React.MouseEvent) => { this.expanded = !this.expanded; e.stopPropagation(); - } + }; render() { let content; @@ -96,10 +96,7 @@ class DocumentViewer extends React.Component<{ field: Doc }> { }); content = ( <div> - Document ({this.props.field[Id]}) - <div style={{ paddingLeft: "25px" }}> - {fields} - </div> + Document ({this.props.field[Id]})<div style={{ paddingLeft: '25px' }}>{fields}</div> </div> ); } else { @@ -109,24 +106,23 @@ class DocumentViewer extends React.Component<{ field: Doc }> { <div> <button onClick={this.onClick}>Toggle</button> {content} - </div > + </div> ); } } @observer -class DebugViewer extends React.Component<{ field: FieldResult, setValue(value: string): boolean }> { - +class DebugViewer extends React.Component<{ field: FieldResult; setValue(value: string): boolean }> { render() { let content; const field = this.props.field; if (field instanceof List) { - content = (<ListViewer field={field} />); + content = <ListViewer field={field} />; } else if (field instanceof Doc) { - content = (<DocumentViewer field={field} />); - } else if (typeof field === "string") { + content = <DocumentViewer field={field} />; + } else if (typeof field === 'string') { content = <p>"{field}"</p>; - } else if (typeof field === "number" || typeof field === "boolean") { + } else if (typeof field === 'number' || typeof field === 'boolean') { content = <p>{field}</p>; } else if (field instanceof RichTextField) { content = <p>RTF: {field.Data}</p>; @@ -153,28 +149,30 @@ class Viewer extends React.Component { @action inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.idToAdd = e.target.value; - } + }; @action onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => { - if (e.key === "Enter") { - DocServer.GetRefField(this.idToAdd).then(action((field: any) => { - if (field !== undefined) { - this.fields.push(field); - } - })); - this.idToAdd = ""; + if (e.key === 'Enter') { + DocServer.GetRefField(this.idToAdd).then( + action((field: any) => { + if (field !== undefined) { + this.fields.push(field); + } + }) + ); + this.idToAdd = ''; } - } + }; render() { return ( <> - <input value={this.idToAdd} - onChange={this.inputOnChange} - onKeyDown={this.onKeyPress} /> + <input value={this.idToAdd} onChange={this.inputOnChange} onKeyDown={this.onKeyPress} /> <div> - {this.fields.map((field, index) => <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>)} + {this.fields.map((field, index) => ( + <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer> + ))} </div> </> ); @@ -182,11 +180,11 @@ class Viewer extends React.Component { } (async function () { - await DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "viewer"); - ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> + await DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, 'viewer'); + ReactDOM.render( + <div style={{ position: 'absolute', width: '100%', height: '100%' }}> <Viewer /> - </div>), + </div>, document.getElementById('root') ); -})();
\ No newline at end of file +})(); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 8879bd194..86b4da792 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -211,6 +211,7 @@ export class Doc extends RefField { } public static set ActivePage(val) { Doc.UserDoc().activePage = val; + DocServer.UPDATE_SERVER_CACHE(); } public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); @@ -1177,7 +1178,7 @@ export namespace Doc { } // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) export function IsBrushedDegreeUnmemoized(doc: Doc) { - if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return DocBrushStatus.unbrushed; + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return DocBrushStatus.unbrushed; const status = brushManager.BrushedDoc.has(doc) ? DocBrushStatus.selfBrushed : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? DocBrushStatus.protoBrushed : DocBrushStatus.unbrushed; if (status === DocBrushStatus.unbrushed) { const lastBrushed = Array.from(brushManager.BrushedDoc.keys()).lastElement(); @@ -1239,7 +1240,7 @@ export namespace Doc { } const highlightManager = new HighlightBrush(); export function IsHighlighted(doc: Doc) { - if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false; + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false; return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc)); } export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) { @@ -1390,6 +1391,20 @@ export namespace Doc { return !curPres ? false : DocListCast(curPres.data).findIndex(val => Doc.AreProtosEqual(val, doc)) !== -1; } + export function styleFromLayoutString(rootDoc: Doc, layoutDoc: Doc, props: any, scale: number) { + const style: { [key: string]: any } = {}; + const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; + const replacer = (match: any, expr: string, offset: any, string: any) => { + // bcz: this executes a script to convert a property expression string: { script } into a value + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: rootDoc, this: layoutDoc, scale }).result?.toString() ?? ''; + }; + divKeys.map((prop: string) => { + const p = props[prop]; + typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); + }); + return style; + } + // prettier-ignore export function toIcon(doc?: Doc, isOpen?: boolean) { switch (StrCast(doc?.type)) { diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 114d5fc2f..a074098c1 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -1,22 +1,21 @@ -import { Bezier } from "bezier-js"; -import { createSimpleSchema, list, object, serializable } from "serializr"; -import { ScriptingGlobals } from "../client/util/ScriptingGlobals"; -import { Deserializable } from "../client/util/SerializationHelper"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { ObjectField } from "./ObjectField"; +import { Bezier } from 'bezier-js'; +import { createSimpleSchema, list, object, serializable } from 'serializr'; +import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { Deserializable } from '../client/util/SerializationHelper'; +import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { ObjectField } from './ObjectField'; // Helps keep track of the current ink tool in use. export enum InkTool { - None = "none", - Pen = "pen", - Highlighter = "highlighter", - Eraser = "eraser", - Stamp = "stamp", - Write = "write", - PresentationPin = 'presentationpin' + None = 'none', + Pen = 'pen', + Highlighter = 'highlighter', + Eraser = 'eraser', + Stamp = 'stamp', + Write = 'write', + PresentationPin = 'presentationpin', } - // Defines a point in an ink as a pair of x- and y-coordinates. export interface PointData { X: number; @@ -54,15 +53,16 @@ export interface HandleLine { } const pointSchema = createSimpleSchema({ - X: true, Y: true + X: true, + Y: true, }); const strokeDataSchema = createSimpleSchema({ pathData: list(object(pointSchema)), - "*": true + '*': true, }); -@Deserializable("ink") +@Deserializable('ink') export class InkField extends ObjectField { @serializable(list(object(strokeDataSchema))) readonly inkData: InkData; @@ -85,11 +85,11 @@ export class InkField extends ObjectField { } [ToScriptString]() { - return "new InkField([" + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}`) + "])"; + return 'new InkField([' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + '])'; } [ToString]() { - return "InkField"; + return 'InkField'; } } -ScriptingGlobals.add("InkField", InkField);
\ No newline at end of file +ScriptingGlobals.add('InkField', InkField); diff --git a/src/fields/List.ts b/src/fields/List.ts index b15548327..5cc4ca543 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -1,25 +1,25 @@ -import { action, observable } from "mobx"; -import { alias, list, serializable } from "serializr"; -import { DocServer } from "../client/DocServer"; -import { ScriptingGlobals } from "../client/util/ScriptingGlobals"; -import { afterDocDeserialize, autoObject, Deserializable } from "../client/util/SerializationHelper"; -import { Field } from "./Doc"; -import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols"; -import { ObjectField } from "./ObjectField"; -import { ProxyField } from "./Proxy"; -import { RefField } from "./RefField"; -import { listSpec } from "./Schema"; -import { Cast } from "./Types"; -import { deleteProperty, getter, setter, updateFunction } from "./util"; +import { action, observable } from 'mobx'; +import { alias, list, serializable } from 'serializr'; +import { DocServer } from '../client/DocServer'; +import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper'; +import { Field } from './Doc'; +import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols'; +import { ObjectField } from './ObjectField'; +import { ProxyField } from './Proxy'; +import { RefField } from './RefField'; +import { listSpec } from './Schema'; +import { Cast } from './Types'; +import { deleteProperty, getter, setter, updateFunction } from './util'; const listHandlers: any = { /// Mutator methods copyWithin() { - throw new Error("copyWithin not supported yet"); + throw new Error('copyWithin not supported yet'); }, fill(value: any, start?: number, end?: number) { if (value instanceof RefField) { - throw new Error("fill with RefFields not supported yet"); + throw new Error('fill with RefFields not supported yet'); } const res = this[Self].__fields.fill(value, start, end); this[Update](); @@ -44,7 +44,7 @@ const listHandlers: any = { } } const res = list.__fields.push(...items); - this[Update]({ op: "$addToSet", items, length: length + items.length }); + this[Update]({ op: '$addToSet', items, length: length + items.length }); return res; }), reverse() { @@ -78,8 +78,13 @@ const listHandlers: any = { } } const res = list.__fields.splice(start, deleteCount, ...items); - this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } : - items.length && !deleteCount && start === list.__fields.length ? { op: "$addToSet", items, length: list.__fields.length } : undefined); + this[Update]( + items.length === 0 && deleteCount + ? { op: '$remFromSet', items: removed, length: list.__fields.length } + : items.length && !deleteCount && start === list.__fields.length + ? { op: '$addToSet', items, length: list.__fields.length } + : undefined + ); return res.map(toRealField); }), unshift(...items: any[]) { @@ -98,7 +103,6 @@ const listHandlers: any = { const res = this[Self].__fields.unshift(...items); this[Update](); return res; - }, /// Accessor methods concat: action(function (this: any, ...items: any[]) { @@ -198,7 +202,7 @@ const listHandlers: any = { }, [Symbol.iterator]() { return this[Self].__realFields().values(); - } + }, }; function toObjectField(field: Field) { @@ -217,14 +221,14 @@ function listGetter(target: any, prop: string | number | symbol, receiver: any): } interface ListSpliceUpdate<T> { - type: "splice"; + type: 'splice'; index: number; added: T[]; removedCount: number; } interface ListIndexUpdate<T> { - type: "update"; + type: 'update'; index: number; newValue: T; } @@ -233,7 +237,7 @@ type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>; type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T; -@Deserializable("list") +@Deserializable('list') class ListImpl<T extends Field> extends ObjectField { constructor(fields?: T[]) { super(); @@ -244,14 +248,16 @@ class ListImpl<T extends Field> extends ObjectField { getOwnPropertyDescriptor: (target, prop) => { if (prop in target.__fields) { return { - configurable: true,//TODO Should configurable be true? + configurable: true, //TODO Should configurable be true? enumerable: true, }; } return Reflect.getOwnPropertyDescriptor(target, prop); }, deleteProperty: deleteProperty, - defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, + defineProperty: () => { + throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); + }, }); this[SelfProxy] = list; if (fields) { @@ -265,7 +271,7 @@ class ListImpl<T extends Field> extends ObjectField { // this requests all ProxyFields at the same time to avoid the overhead // of separate network requests and separate updates to the React dom. private __realFields() { - const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: (f instanceof ProxyField) ? f.promisedValue() : "" })); + const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: f instanceof ProxyField ? f.promisedValue() : '' })); // if we find any ProxyFields that don't have a current value, then // start the server request for all of them if (promised.length) { @@ -282,7 +288,7 @@ class ListImpl<T extends Field> extends ObjectField { return this.__fields.map(toRealField); } - @serializable(alias("fields", list(autoObject(), { afterDeserialize: afterDocDeserialize }))) + @serializable(alias('fields', list(autoObject(), { afterDeserialize: afterDocDeserialize }))) private get __fields() { return this.___fields; } @@ -299,7 +305,7 @@ class ListImpl<T extends Field> extends ObjectField { } [Copy]() { - const copiedData = this[Self].__fields.map(f => f instanceof ObjectField ? f[Copy]() : f); + const copiedData = this[Self].__fields.map(f => (f instanceof ObjectField ? f[Copy]() : f)); const deepCopy = new ListImpl<T>(copiedData as any); return deepCopy; } @@ -313,7 +319,7 @@ class ListImpl<T extends Field> extends ObjectField { const update = this[OnUpdate]; // update && update(diff); update?.(diff); - } + }; private [Self] = this; private [SelfProxy]: any; @@ -328,9 +334,9 @@ class ListImpl<T extends Field> extends ObjectField { export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[]; export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; -ScriptingGlobals.add("List", List); +ScriptingGlobals.add('List', List); ScriptingGlobals.add(function compareLists(l1: any, l2: any) { - const L1 = Cast(l1, listSpec("string"), []); - const L2 = Cast(l2, listSpec("string"), []); + const L1 = Cast(l1, listSpec('string'), []); + const L2 = Cast(l2, listSpec('string'), []); return !L1 && !L2 ? true : L1 && L2 && L1.length === L2.length && L2.reduce((p, v) => p && L1.includes(v), true); -}, "compare two lists");
\ No newline at end of file +}, 'compare two lists'); diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 3b02d0cfe..0b51db70b 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,60 +1,60 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString, OnUpdate } from "./FieldSymbols"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { ColumnType } from "../client/views/collections/collectionSchema/CollectionSchemaView"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, primitive } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; export const PastelSchemaPalette = new Map<string, string>([ // ["pink1", "#FFB4E8"], - ["pink2", "#ff9cee"], - ["pink3", "#ffccf9"], - ["pink4", "#fcc2ff"], - ["pink5", "#f6a6ff"], - ["purple1", "#b28dff"], - ["purple2", "#c5a3ff"], - ["purple3", "#d5aaff"], - ["purple4", "#ecd4ff"], + ['pink2', '#ff9cee'], + ['pink3', '#ffccf9'], + ['pink4', '#fcc2ff'], + ['pink5', '#f6a6ff'], + ['purple1', '#b28dff'], + ['purple2', '#c5a3ff'], + ['purple3', '#d5aaff'], + ['purple4', '#ecd4ff'], // ["purple5", "#fb34ff"], - ["purple6", "#dcd3ff"], - ["purple7", "#a79aff"], - ["purple8", "#b5b9ff"], - ["purple9", "#97a2ff"], - ["bluegreen1", "#afcbff"], - ["bluegreen2", "#aff8db"], - ["bluegreen3", "#c4faf8"], - ["bluegreen4", "#85e3ff"], - ["bluegreen5", "#ace7ff"], + ['purple6', '#dcd3ff'], + ['purple7', '#a79aff'], + ['purple8', '#b5b9ff'], + ['purple9', '#97a2ff'], + ['bluegreen1', '#afcbff'], + ['bluegreen2', '#aff8db'], + ['bluegreen3', '#c4faf8'], + ['bluegreen4', '#85e3ff'], + ['bluegreen5', '#ace7ff'], // ["bluegreen6", "#6eb5ff"], - ["bluegreen7", "#bffcc6"], - ["bluegreen8", "#dbffd6"], - ["yellow1", "#f3ffe3"], - ["yellow2", "#e7ffac"], - ["yellow3", "#ffffd1"], - ["yellow4", "#fff5ba"], + ['bluegreen7', '#bffcc6'], + ['bluegreen8', '#dbffd6'], + ['yellow1', '#f3ffe3'], + ['yellow2', '#e7ffac'], + ['yellow3', '#ffffd1'], + ['yellow4', '#fff5ba'], // ["red1", "#ffc9de"], - ["red2", "#ffabab"], - ["red3", "#ffbebc"], - ["red4", "#ffcbc1"], - ["orange1", "#ffd5b3"], - ["gray", "#f1efeb"] + ['red2', '#ffabab'], + ['red3', '#ffbebc'], + ['red4', '#ffcbc1'], + ['orange1', '#ffd5b3'], + ['gray', '#f1efeb'], ]); export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)]; export const DarkPastelSchemaPalette = new Map<string, string>([ - ["pink2", "#c932b0"], - ["purple4", "#913ad6"], - ["bluegreen1", "#3978ed"], - ["bluegreen7", "#2adb3e"], - ["bluegreen5", "#21b0eb"], - ["yellow4", "#edcc0c"], - ["red2", "#eb3636"], - ["orange1", "#f2740f"], + ['pink2', '#c932b0'], + ['purple4', '#913ad6'], + ['bluegreen1', '#3978ed'], + ['bluegreen7', '#2adb3e'], + ['bluegreen5', '#21b0eb'], + ['yellow4', '#edcc0c'], + ['red2', '#eb3636'], + ['orange1', '#f2740f'], ]); @scriptingGlobal -@Deserializable("schemaheader") +@Deserializable('schemaheader') export class SchemaHeaderField extends ObjectField { @serializable(primitive()) heading: string; @@ -69,7 +69,7 @@ export class SchemaHeaderField extends ObjectField { @serializable(primitive()) desc: boolean | undefined; // boolean determines sort order, undefined when no sort - constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) { + constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) { super(); this.heading = heading; @@ -111,13 +111,13 @@ export class SchemaHeaderField extends ObjectField { } [Copy]() { - return new SchemaHeaderField(this.heading, this.color, this.type); + return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed); } [ToScriptString]() { - return `header(${this.heading},${this.type}})`; + return `header(${this.heading},${this.type},${this.width}})`; } [ToString]() { return `SchemaHeaderField`; } -}
\ No newline at end of file +} diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 40ca0ce22..3cb50a4c0 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,29 +1,32 @@ -import { computedFn } from "mobx-utils"; -import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from "serializr"; -import { CompiledScript, CompileScript } from "../client/util/Scripting"; -import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGlobals"; -import { autoObject, Deserializable } from "../client/util/SerializationHelper"; -import { numberRange } from "../Utils"; -import { Doc, Field, Opt } from "./Doc"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { List } from "./List"; -import { ObjectField } from "./ObjectField"; -import { ProxyField } from "./Proxy"; -import { Cast, NumCast } from "./Types"; -import { Plugins } from "./util"; +import { computedFn } from 'mobx-utils'; +import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr'; +import { DocServer } from '../client/DocServer'; +import { CompiledScript, CompileScript, ScriptOptions } from '../client/util/Scripting'; +import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { autoObject, Deserializable } from '../client/util/SerializationHelper'; +import { numberRange } from '../Utils'; +import { Doc, Field, Opt } from './Doc'; +import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; +import { List } from './List'; +import { ObjectField } from './ObjectField'; +import { Cast, StrCast } from './Types'; +import { Plugins } from './util'; function optional(propSchema: PropSchema) { - return custom(value => { - if (value !== undefined) { - return propSchema.serializer(value); - } - return SKIP; - }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { - if (jsonValue !== undefined) { - return propSchema.deserializer(jsonValue, callback, context, oldValue); + return custom( + value => { + if (value !== undefined) { + return propSchema.serializer(value); + } + return SKIP; + }, + (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { + if (jsonValue !== undefined) { + return propSchema.deserializer(jsonValue, callback, context, oldValue); + } + return SKIP; } - return SKIP; - }); + ); } const optionsSchema = createSimpleSchema({ @@ -32,32 +35,19 @@ const optionsSchema = createSimpleSchema({ typecheck: true, editable: true, readonly: true, - params: optional(map(primitive())) + params: optional(map(primitive())), }); const scriptSchema = createSimpleSchema({ options: object(optionsSchema), - originalScript: true + originalScript: true, }); -async function deserializeScript(script: ScriptField) { - const captures: ProxyField<Doc> = (script as any).captures; - const cache = captures ? undefined : ScriptField.GetScriptFieldCache(script.script.originalScript); - if (cache) return (script as any).script = cache; - if (captures) { - const doc = (await captures.value())!; - const captured: any = {}; - const keys = Object.keys(doc); - const vals = await Promise.all(keys.map(key => doc[key]) as any); - keys.forEach((key, i) => captured[key] = vals[i]); - (script.script.options as any).capturedVariables = captured; - } +function finalizeScript(script: ScriptField) { const comp = CompileScript(script.script.originalScript, script.script.options); if (!comp.compiled) { throw new Error("Couldn't compile loaded script"); } - (script as any).script = comp; - !captures && ScriptField._scriptFieldCache.set(script.script.originalScript, comp); if (script.setterscript) { const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); if (!compset.compiled) { @@ -65,32 +55,56 @@ async function deserializeScript(script: ScriptField) { } (script as any).setterscript = compset; } + return comp; +} +async function deserializeScript(script: ScriptField) { + if (script.captures) { + const captured: any = {}; + (script.script.options as ScriptOptions).capturedVariables = captured; + Promise.all( + script.captures.map(async capture => { + const key = capture.split(':')[0]; + const val = capture.split(':')[1]; + if (val === 'true') captured[key] = true; + else if (val === 'false') captured[key] = false; + else if (val.startsWith('ID->')) captured[key] = await DocServer.GetRefField(val.replace('ID->', '')); + else if (!isNaN(Number(val))) captured[key] = Number(val); + else captured[key] = val; + }) + ).then(() => ((script as any).script = finalizeScript(script))); + } else { + (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script); + } } @scriptingGlobal -@Deserializable("script", deserializeScript) +@Deserializable('script', deserializeScript) export class ScriptField extends ObjectField { + @serializable + readonly rawscript: string | undefined; @serializable(object(scriptSchema)) readonly script: CompiledScript; @serializable(object(scriptSchema)) readonly setterscript: CompiledScript | undefined; @serializable(autoObject()) - private captures?: ProxyField<Doc>; + captures?: List<string>; public static _scriptFieldCache: Map<string, Opt<CompiledScript>> = new Map(); - public static GetScriptFieldCache(field: string) { return this._scriptFieldCache.get(field); } + public static GetScriptFieldCache(field: string) { + return this._scriptFieldCache.get(field); + } - constructor(script: CompiledScript, setterscript?: CompiledScript) { + constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) { super(); - if (script?.options.capturedVariables) { - const doc = Doc.assign(new Doc, script.options.capturedVariables); - doc.system = true; - this.captures = new ProxyField(doc); + const captured = script?.options.capturedVariables; + if (captured) { + this.captures = new List<string>(Object.keys(captured).map(key => key + ':' + (captured[key] instanceof Doc ? 'ID->' + (captured[key] as Doc)[Id] : captured[key].toString()))); } + this.rawscript = rawscript; this.setterscript = setterscript; - this.script = script; + this.script = script ?? (CompileScript('false') as CompiledScript); } // init(callback: (res: Field) => any) { @@ -115,74 +129,92 @@ export class ScriptField extends ObjectField { // } [Copy](): ObjectField { - return new ScriptField(this.script, this.setterscript); + return new ScriptField(this.script, this.setterscript, this.rawscript); } toString() { return `${this.script.originalScript} + ${this.setterscript?.originalScript}`; } [ToScriptString]() { - return "script field"; + return 'script field'; } [ToString]() { return this.script.originalScript; } - public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Field }) { - const compiled = CompileScript(script, { + public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { + return CompileScript(script, { params: { - this: Doc?.name || "Doc", // this is the doc that executes the script - self: Doc?.name || "Doc", // self is the root doc of the doc that executes the script - _last_: "any", // _last_ is the previous value of a computed field when it is being triggered to re-run. - _readOnly_: "boolean", // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) - ...params + this: Doc?.name || 'Doc', // this is the doc that executes the script + self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script + _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run. + _readOnly_: 'boolean', // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) + ...params, }, typecheck: false, editable: true, addReturn: addReturn, - capturedVariables + capturedVariables, }); - return compiled; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } - public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, false, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } } @scriptingGlobal -@Deserializable("computed", deserializeScript) +@Deserializable('computed', deserializeScript) export class ComputedField extends ScriptField { _lastComputedResult: any; //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); - _valueOutsideReaction = (doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result; - + _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); [Copy](): ObjectField { - return new ComputedField(this.script, this.setterscript); + return new ComputedField(this.script, this.setterscript, this.rawscript); } public static MakeScript(script: string, params: object = {}) { const compiled = ScriptField.CompileScript(script, params, false); return compiled.compiled ? new ComputedField(compiled) : undefined; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ComputedField(compiled) : undefined; } - public static MakeInterpolated(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) { if (!doc[`${fieldKey}-indexed`]) { const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]); - flist[curTimecode] = NumCast(doc[fieldKey]); + flist[curTimecode] = Cast(doc[fieldKey], 'number', null); + doc[`${fieldKey}-indexed`] = flist; + } + const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, ${defaultVal})`, {}, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); + return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + } + public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + if (!doc[`${fieldKey}-indexed`]) { + const flist = new List<string>(numberRange(curTimecode + 1).map(i => undefined) as any as string[]); + flist[curTimecode] = StrCast(doc[fieldKey]); + doc[`${fieldKey}-indexed`] = flist; + } + const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); + return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + } + public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + if (!doc[`${fieldKey}-indexed`]) { + const flist = new List<Field>(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]); + flist[curTimecode] = Field.Copy(doc[fieldKey]); doc[`${fieldKey}-indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); - const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: "any" }, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } } @@ -196,7 +228,7 @@ export namespace ComputedField { useComputed = true; } - export const undefined = "__undefined"; + export const undefined = '__undefined'; export function WithoutComputed<T>(fn: () => T) { DisableComputedFields(); @@ -216,15 +248,27 @@ export namespace ComputedField { } } -ScriptingGlobals.add(function setIndexVal(list: any[], index: number, value: any) { - while (list.length <= index) list.push(undefined); - list[index] = value; -}, "sets the value at a given index of a list", "(list: any[], index: number, value: any)"); +ScriptingGlobals.add( + function setIndexVal(list: any[], index: number, value: any) { + while (list.length <= index) list.push(undefined); + list[index] = value; + }, + 'sets the value at a given index of a list', + '(list: any[], index: number, value: any)' +); -ScriptingGlobals.add(function getIndexVal(list: any[], index: number) { - return list?.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any); -}, "returns the value at a given index of a list", "(list: any[], index: number)"); +ScriptingGlobals.add( + function getIndexVal(list: any[], index: number, defaultVal: Opt<number> = undefined) { + return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), defaultVal); + }, + 'returns the value at a given index of a list', + '(list: any[], index: number)' +); -ScriptingGlobals.add(function makeScript(script: string) { - return ScriptField.MakeScript(script); -}, "returns the value at a given index of a list", "(list: any[], index: number)"); +ScriptingGlobals.add( + function makeScript(script: string) { + return ScriptField.MakeScript(script); + }, + 'returns the value at a given index of a list', + '(list: any[], index: number)' +); diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 36dd56a1a..00c78e231 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -1,16 +1,14 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, custom } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { ToScriptString, ToString, Copy } from "./FieldSymbols"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { Utils } from "../Utils"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, custom } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { ToScriptString, ToString, Copy } from './FieldSymbols'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { Utils } from '../Utils'; function url() { return custom( function (value: URL) { - return value.origin === window.location.origin ? - value.pathname : - value.href; + return value?.origin === window.location.origin ? value.pathname : value?.href; }, function (jsonValue: string) { return new URL(jsonValue, window.location.origin); @@ -26,23 +24,23 @@ export abstract class URLField extends ObjectField { constructor(url: URL); constructor(url: URL | string) { super(); - if (typeof url === "string") { - url = url.startsWith("http") ? new URL(url) : new URL(url, window.location.origin); + if (typeof url === 'string') { + url = url.startsWith('http') ? new URL(url) : new URL(url, window.location.origin); } this.url = url; } [ToScriptString]() { - if (Utils.prepend(this.url.pathname) === this.url.href) { + if (Utils.prepend(this.url?.pathname) === this.url?.href) { return `new ${this.constructor.name}("${this.url.pathname}")`; } return `new ${this.constructor.name}("${this.url.href}")`; } [ToString]() { - if (Utils.prepend(this.url.pathname) === this.url.href) { + if (Utils.prepend(this.url?.pathname) === this.url?.href) { return this.url.pathname; } - return this.url.href; + return this.url?.href; } [Copy](): this { @@ -50,16 +48,35 @@ export abstract class URLField extends ObjectField { } } -export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg"; - -@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { } -@scriptingGlobal @Deserializable("recording") export class RecordingField extends URLField { } -@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { } -@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { } -@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { } -@scriptingGlobal @Deserializable("web") export class WebField extends URLField { } -@scriptingGlobal @Deserializable("map") export class MapField extends URLField { } -@scriptingGlobal @Deserializable("csv") export class CsvField extends URLField { } -@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { } -@scriptingGlobal @Deserializable("webcam") export class WebCamField extends URLField { } +export const nullAudio = 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg'; +@scriptingGlobal +@Deserializable('audio') +export class AudioField extends URLField {} +@scriptingGlobal +@Deserializable('recording') +export class RecordingField extends URLField {} +@scriptingGlobal +@Deserializable('image') +export class ImageField extends URLField {} +@scriptingGlobal +@Deserializable('video') +export class VideoField extends URLField {} +@scriptingGlobal +@Deserializable('pdf') +export class PdfField extends URLField {} +@scriptingGlobal +@Deserializable('web') +export class WebField extends URLField {} +@scriptingGlobal +@Deserializable('map') +export class MapField extends URLField {} +@scriptingGlobal +@Deserializable('csv') +export class CsvField extends URLField {} +@scriptingGlobal +@Deserializable('youtube') +export class YoutubeField extends URLField {} +@scriptingGlobal +@Deserializable('webcam') +export class WebCamField extends URLField {} diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index be39e0709..24b5a359d 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -1,116 +1,114 @@ -import { makeInterface, createSchema, listSpec } from "./Schema"; -import { ScriptField } from "./ScriptField"; -import { Doc } from "./Doc"; -import { DateField } from "./DateField"; -import { SchemaHeaderField } from "./SchemaHeaderField"; +import { makeInterface, createSchema, listSpec } from './Schema'; +import { ScriptField } from './ScriptField'; +import { Doc } from './Doc'; +import { DateField } from './DateField'; +import { SchemaHeaderField } from './SchemaHeaderField'; export const documentSchema = createSchema({ // content properties - type: "string", // enumerated type of document -- should be template-specific (ie, start with an '_') - title: "string", // document title (can be on either data document or layout) - isTemplateForField: "string",// if specified, it indicates the document is a template that renders the specified field - creationDate: DateField, // when the document was created - links: listSpec(Doc), // computed (readonly) list of links associated with this document + type: 'string', // enumerated type of document -- should be template-specific (ie, start with an '_') + title: 'string', // document title (can be on either data document or layout) + isTemplateForField: 'string', // if specified, it indicates the document is a template that renders the specified field + creationDate: DateField, // when the document was created + links: listSpec(Doc), // computed (readonly) list of links associated with this document // "Location" properties in a very general sense - _curPage: "number", // current page of a page based document - _currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide) - lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide) - activeFrame: "number", // the active frame of a frame based animated document - _currentTimecode: "number", // current play back time of a temporal document (video / audio) - _timecodeToShow: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) - _timecodeToHIde: "number", // the time that a document should be hidden - markers: listSpec(Doc), // list of markers for audio / video - x: "number", // x coordinate when in a freeform view - y: "number", // y coordinate when in a freeform view - z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview - zIndex: "number", // zIndex of a document in a freeform view - _scrollTop: "number", // scroll position of a scrollable document (pdf, text, web) - lat: "number", - lng: "number", + _curPage: 'number', // current page of a page based document + _currentFrame: 'number', // current frame of a frame based collection (e.g., a progressive slide) + lastFrame: 'number', // last frame of a frame based collection (e.g., a progressive slide) + activeFrame: 'number', // the active frame of a frame based animated document + _currentTimecode: 'number', // current play back time of a temporal document (video / audio) + _timecodeToShow: 'number', // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) + _timecodeToHIde: 'number', // the time that a document should be hidden + markers: listSpec(Doc), // list of markers for audio / video + x: 'number', // x coordinate when in a freeform view + y: 'number', // y coordinate when in a freeform view + z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview + zIndex: 'number', // zIndex of a document in a freeform view + _scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web) + lat: 'number', + lng: 'number', // appearance properties on the layout - "_backgroundGrid-spacing": "number", // the size of the grid for collection views - _autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents - _nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set - _nativeHeight: "number", // " - _width: "number", // width of document in its container's coordinate system - _height: "number", // " - _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set - _yPadding: "number", // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set - _xMargin: "number", // margin added on left/right of most documents to add separation from their container - _yMargin: "number", // margin added on top/bottom of most documents to add separation from their container - _overflow: "string", // sets overflow behvavior for CollectionFreeForm views - _showCaption: "string", // whether editable caption text is overlayed at the bottom of the document - _showTitle: "string", // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover - _showAudio: "boolean", // whether to show the audio record icon on documents - _pivotField: "string", // specifies which field key should be used as the timeline/pivot axis - _columnsFill: "boolean", // whether documents in a stacking view column should be sized to fill the column - _columnsSort: "string", // how a document should be sorted "ascending", "descending", undefined (none) - _columnsHideIfEmpty: "boolean", // whether empty stacking view column headings should be hidden + '_backgroundGrid-spacing': 'number', // the size of the grid for collection views + _autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents + _nativeWidth: 'number', // native width of document which determines how much document contents are scaled when the document's width is set + _nativeHeight: 'number', // " + _width: 'number', // width of document in its container's coordinate system + _height: 'number', // " + _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set + _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set + _xMargin: 'number', // margin added on left/right of most documents to add separation from their container + _yMargin: 'number', // margin added on top/bottom of most documents to add separation from their container + _overflow: 'string', // sets overflow behvavior for CollectionFreeForm views + _showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document + _showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover + _pivotField: 'string', // specifies which field key should be used as the timeline/pivot axis + _columnsFill: 'boolean', // whether documents in a stacking view column should be sized to fill the column + _columnsSort: 'string', // how a document should be sorted "ascending", "descending", undefined (none) + _columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views - _fontSize: "string", - _fontFamily: "string", - _sidebarWidthPercent: "string", // percent of text window width taken up by sidebar + _fontSize: 'string', + _fontFamily: 'string', + _sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar // appearance properties on the data document - backgroundColor: "string", // background color of document - borderRounding: "string", // border radius rounding of document - boxShadow: "string", // the amount of shadow around the perimeter of a document - color: "string", // foreground color of document - fitContentsToBox: "boolean",// whether freeform view contents should be zoomed/panned to fill the area of the document view box - fontSize: "string", - hidden: "boolean", // whether a document should not be displayed - isInkMask: "boolean", // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through) - layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below - layoutKey: "string", // holds the field key for the field that actually holds the current lyoat - letterSpacing: "string", - opacity: "number", // opacity of document - strokeWidth: "number", - strokeBezier: "number", - strokeStartMarker: "string", - strokeEndMarker: "string", - strokeDash: "string", - textTransform: "string", - treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden - treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree - treeViewExpandedViewLock: "boolean", // whether the expanded view can be changed - treeViewOpenIsTransient: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) - treeViewType: "string", // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off + backgroundColor: 'string', // background color of document + borderRounding: 'string', // border radius rounding of document + boxShadow: 'string', // the amount of shadow around the perimeter of a document + color: 'string', // foreground color of document + fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box + fontSize: 'string', + hidden: 'boolean', // whether a document should not be displayed + isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through) + layout: 'string', // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below + layoutKey: 'string', // holds the field key for the field that actually holds the current lyoat + letterSpacing: 'string', + opacity: 'number', // opacity of document + strokeWidth: 'number', + strokeBezier: 'number', + strokeStartMarker: 'string', + strokeEndMarker: 'string', + strokeDash: 'string', + textTransform: 'string', + treeViewOpen: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden + treeViewExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree + treeViewExpandedViewLock: 'boolean', // whether the expanded view can be changed + treeViewOpenIsTransient: 'boolean', // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) + treeViewType: 'string', // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off // interaction and linking properties - ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) - onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) + ignoreClick: 'boolean', // whether documents ignores input clicks (but does not ignore manipulation and other events) + onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) - onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) - onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. - followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, ) - hideLinkButton: "boolean", // whether the blue link counter button should be hidden - hideAllLinks: "boolean", // whether all individual blue anchor dots should be hidden - linkDisplay: "boolean", // whether a link connection should be shown between link anchor endpoints. - isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations - isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked - layers: listSpec("string"), // which layers the document is part of - _lockedPosition: "boolean", // whether the document can be moved (dragged) - _lockedTransform: "boolean",// whether a freeformview can pan/zoom - displayArrow: "boolean", // toggles directed arrows + onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) + onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. + followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, ) + hideLinkButton: 'boolean', // whether the blue link counter button should be hidden + hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden + linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints. + isInPlaceContainer: 'boolean', // whether the marked object will display addDocTab() calls that target "inPlace" destinations + isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked + layers: listSpec('string'), // which layers the document is part of + _lockedPosition: 'boolean', // whether the document can be moved (dragged) + _lockedTransform: 'boolean', // whether a freeformview can pan/zoom + displayArrow: 'boolean', // toggles directed arrows // drag drop properties - _stayInCollection: "boolean",// whether document can be dropped into a different collection - dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. - dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias", "copy", "move") - targetDropAction: "string", // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move' - childDropAction: "string", // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy") - removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped + _stayInCollection: 'boolean', // whether document can be dropped into a different collection + dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. + dropAction: 'string', // override specifying what should happen when this document is dropped (can be "alias", "copy", "move") + targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move' + childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy") + removeDropProperties: listSpec('string'), // properties that should be removed from the alias/copy/etc of this document when it is dropped }); - export const collectionSchema = createSchema({ childLayoutTemplate: Doc, // layout template to use to render children of a collecion - childLayoutString: "string", //layout string to use to render children of a collection + childLayoutString: 'string', //layout string to use to render children of a collection childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template) - childDontRegisterViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links + childDontRegisterViews: 'boolean', // whether views made of this document are registered so that they can be found when drawing links onChildClick: ScriptField, // script to run for each child when its clicked onChildDoubleClick: ScriptField, // script to run for each child when its clicked onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view diff --git a/src/fields/util.ts b/src/fields/util.ts index cbb560114..b3cbbe241 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,41 +1,41 @@ +import { action, observable, runInAction, trace } from 'mobx'; +import { computedFn } from 'mobx-utils'; +import { DocServer } from '../client/DocServer'; +import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; +import { returnZero } from '../Utils'; +import CursorField from './CursorField'; import { - Doc, - FieldResult, - UpdatingFromServer, - LayoutSym, - AclPrivate, + AclAdmin, + AclAugment, AclEdit, + AclPrivate, AclReadonly, - AclAugment, + AclSelfEdit, AclSym, + AclUnset, DataSym, + Doc, DocListCast, - AclAdmin, - HeightSym, - WidthSym, - updateCachedAcls, - AclUnset, DocListCastAsync, + FieldResult, ForceServerWrite, + HeightSym, Initializing, - AclSelfEdit, + LayoutSym, + updateCachedAcls, + UpdatingFromServer, + WidthSym, } from './Doc'; -import { SerializationHelper } from '../client/util/SerializationHelper'; -import { ProxyField, PrefetchProxy } from './Proxy'; -import { RefField } from './RefField'; +import { Id, OnUpdate, Parent, Self, SelfProxy, Update } from './FieldSymbols'; +import { List } from './List'; import { ObjectField } from './ObjectField'; -import { action, observable, runInAction, trace } from 'mobx'; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from './FieldSymbols'; -import { DocServer } from '../client/DocServer'; +import { PrefetchProxy, ProxyField } from './Proxy'; +import { RefField } from './RefField'; +import { RichTextField } from './RichTextField'; +import { SchemaHeaderField } from './SchemaHeaderField'; import { ComputedField } from './ScriptField'; import { ScriptCast, StrCast } from './Types'; -import { returnZero } from '../Utils'; -import CursorField from './CursorField'; -import { List } from './List'; -import { SnappingManager } from '../client/util/SnappingManager'; -import { computedFn } from 'mobx-utils'; -import { RichTextField } from './RichTextField'; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -445,8 +445,13 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any undo: action(() => { // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].splice(ind, 1); + if (item instanceof SchemaHeaderField) { + const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && receiver[prop].splice(ind, 1); + } else { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].splice(ind, 1); + } }); lastValue = ObjectField.MakeCopy(receiver[prop]); }), @@ -456,7 +461,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any ? { redo: action(() => { diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); + const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ? item.value() : item); ind !== -1 && receiver[prop].splice(ind, 1); }); lastValue = ObjectField.MakeCopy(receiver[prop]); @@ -464,8 +469,13 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any undo: () => { // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo diff.items.forEach((item: any) => { - const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); + if (item instanceof SchemaHeaderField) { + const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item); + } else { + const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); + } }); lastValue = ObjectField.MakeCopy(receiver[prop]); }, diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx index d668d134e..6415099fd 100644 --- a/src/mobile/MobileInkOverlay.tsx +++ b/src/mobile/MobileInkOverlay.tsx @@ -1,13 +1,12 @@ import React = require('react'); -import { observer } from "mobx-react"; -import { MobileInkOverlayContent, GestureContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "../server/Message"; -import { observable, action } from "mobx"; -import { GestureUtils } from "../pen-gestures/GestureUtils"; -import "./MobileInkOverlay.scss"; -import { DragManager } from "../client/util/DragManager"; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; import { DocServer } from '../client/DocServer'; +import { DragManager } from '../client/util/DragManager'; import { Doc } from '../fields/Doc'; - +import { GestureUtils } from '../pen-gestures/GestureUtils'; +import { GestureContent, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent } from '../server/Message'; +import './MobileInkOverlay.scss'; @observer export default class MobileInkOverlay extends React.Component { @@ -18,7 +17,7 @@ export default class MobileInkOverlay extends React.Component { @observable private _height: number = 0; @observable private _x: number = -300; @observable private _y: number = -300; - @observable private _text: string = ""; + @observable private _text: string = ''; @observable private _offsetX: number = 0; @observable private _offsetY: number = 0; @@ -49,7 +48,7 @@ export default class MobileInkOverlay extends React.Component { this._scale = scaledSize.scale; this._x = 300; // TODO: center on screen this._y = 25; // TODO: center on screen - this._text = text ? text : ""; + this._text = text ? text : ''; } @action @@ -64,31 +63,29 @@ export default class MobileInkOverlay extends React.Component { // TODO: figure out why strokes drawn in corner of mobile interface dont get inserted const { points, bounds } = content; - console.log("received points", points, bounds); + console.log('received points', points, bounds); const B = { - right: (bounds.right * this._scale) + this._x, - left: (bounds.left * this._scale) + this._x, // TODO: scale - bottom: (bounds.bottom * this._scale) + this._y, - top: (bounds.top * this._scale) + this._y, // TODO: scale + right: bounds.right * this._scale + this._x, + left: bounds.left * this._scale + this._x, // TODO: scale + bottom: bounds.bottom * this._scale + this._y, + top: bounds.top * this._scale + this._y, // TODO: scale width: bounds.width * this._scale, height: bounds.height * this._scale, }; const target = document.elementFromPoint(this._x + 10, this._y + 10); target?.dispatchEvent( - new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", - { - bubbles: true, - detail: { - points: points, - gesture: GestureUtils.Gestures.Stroke, - bounds: B - } - } - ) + new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', { + bubbles: true, + detail: { + points: points, + gesture: GestureUtils.Gestures.Stroke, + bounds: B, + }, + }) ); - } + }; uploadDocument = async (content: MobileDocumentUploadContent) => { const { docId } = content; @@ -100,36 +97,34 @@ export default class MobileInkOverlay extends React.Component { const complete = new DragManager.DragCompleteEvent(false, dragData); if (target) { - console.log("dispatching upload doc!!!!", target, doc); + console.log('dispatching upload doc!!!!', target, doc); target.dispatchEvent( - new CustomEvent<DragManager.DropEvent>("dashOnDrop", - { - bubbles: true, - detail: { - x: this._x, - y: this._y, - complete: complete, - altKey: false, - metaKey: false, - ctrlKey: false, - shiftKey: false, - embedKey: false - } - } - ) + new CustomEvent<DragManager.DropEvent>('dashOnDrop', { + bubbles: true, + detail: { + x: this._x, + y: this._y, + complete: complete, + altKey: false, + metaKey: false, + ctrlKey: false, + shiftKey: false, + embedKey: false, + }, + }) ); } else { - alert("TARGET IS UNDEFINED"); + alert('TARGET IS UNDEFINED'); } } - } + }; @action dragStart = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.removeEventListener("pointerup", this.dragEnd); - document.addEventListener("pointermove", this.dragging); - document.addEventListener("pointerup", this.dragEnd); + document.removeEventListener('pointermove', this.dragging); + document.removeEventListener('pointerup', this.dragEnd); + document.addEventListener('pointermove', this.dragging); + document.addEventListener('pointerup', this.dragEnd); this._isDragging = true; this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left; @@ -137,7 +132,7 @@ export default class MobileInkOverlay extends React.Component { e.preventDefault(); e.stopPropagation(); - } + }; @action dragging = (e: PointerEvent) => { @@ -150,41 +145,39 @@ export default class MobileInkOverlay extends React.Component { e.preventDefault(); e.stopPropagation(); - } + }; @action dragEnd = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.removeEventListener("pointerup", this.dragEnd); + document.removeEventListener('pointermove', this.dragging); + document.removeEventListener('pointerup', this.dragEnd); this._isDragging = false; e.preventDefault(); e.stopPropagation(); - } + }; render() { - return ( - <div className="mobileInkOverlay" + <div + className="mobileInkOverlay" style={{ width: this._width, height: this._height, - position: "absolute", + position: 'absolute', transform: `translate(${this._x}px, ${this._y}px)`, zIndex: 30000, - pointerEvents: "none", - borderStyle: this._isDragging ? "solid" : "dashed", - } - } - ref={this._mainCont} - > + pointerEvents: 'none', + borderStyle: this._isDragging ? 'solid' : 'dashed', + }} + ref={this._mainCont}> <p>{this._text}</p> <div className="mobileInkOverlay-border top" onPointerDown={this.dragStart}></div> <div className="mobileInkOverlay-border bottom" onPointerDown={this.dragStart}></div> <div className="mobileInkOverlay-border left" onPointerDown={this.dragStart}></div> <div className="mobileInkOverlay-border right" onPointerDown={this.dragStart}></div> - </div > + </div> ); } -}
\ No newline at end of file +} diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx index f85f05f53..6cbf86f77 100644 --- a/src/mobile/MobileMain.tsx +++ b/src/mobile/MobileMain.tsx @@ -12,10 +12,7 @@ AssignAllExtensions(); const info = await CurrentUserUtils.loadCurrentUser(); DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + ' (mobile)'); await Docs.Prototypes.initialize(); - if (info.id !== '__guest__') { - // a guest will not have an id registered - await CurrentUserUtils.loadUserDocument(info.id); - } + await CurrentUserUtils.loadUserDocument(info.id); document.getElementById('root')!.addEventListener( 'wheel', event => { diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 332ba3d35..0b6e18743 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -1,291 +1,308 @@ -import ApiManager, { Registration } from "./ApiManager"; -import { Method, _success } from "../RouteManager"; +import ApiManager, { Registration } from './ApiManager'; +import { Method, _success } from '../RouteManager'; import * as formidable from 'formidable'; import v4 = require('uuid/v4'); const AdmZip = require('adm-zip'); -import { extname, basename, dirname, } from 'path'; -import { createReadStream, createWriteStream, unlink, writeFile } from "fs"; -import { publicDirectory, filesDirectory } from ".."; -import { Database } from "../database"; -import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils"; +import { extname, basename, dirname } from 'path'; +import { createReadStream, createWriteStream, unlink, writeFile } from 'fs'; +import { publicDirectory, filesDirectory } from '..'; +import { Database } from '../database'; +import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils'; import * as sharp from 'sharp'; -import { AcceptableMedia, Upload } from "../SharedMediaTypes"; -import { normalize } from "path"; -import RouteSubscriber from "../RouteSubscriber"; +import { AcceptableMedia, Upload } from '../SharedMediaTypes'; +import { normalize } from 'path'; +import RouteSubscriber from '../RouteSubscriber'; const imageDataUri = require('image-data-uri'); -import { SolrManager } from "./SearchManager"; +import { SolrManager } from './SearchManager'; const fs = require('fs'); export enum Directory { - parsed_files = "parsed_files", - images = "images", - videos = "videos", - pdfs = "pdfs", - text = "text", - pdf_thumbnails = "pdf_thumbnails", - audio = "audio", - csv = "csv", + parsed_files = 'parsed_files', + images = 'images', + videos = 'videos', + pdfs = 'pdfs', + text = 'text', + pdf_thumbnails = 'pdf_thumbnails', + audio = 'audio', + csv = 'csv', } export function serverPathToFile(directory: Directory, filename: string) { - return normalize(`${filesDirectory}/${directory}/${filename}`); + return normalize(`${filesDirectory}/${directory}/${filename}`); } export function pathToDirectory(directory: Directory) { - return normalize(`${filesDirectory}/${directory}`); + return normalize(`${filesDirectory}/${directory}`); } export function clientPathToFile(directory: Directory, filename: string) { - return `/files/${directory}/${filename}`; + return `/files/${directory}/${filename}`; } export default class UploadManager extends ApiManager { - protected initialize(register: Registration): void { - - register({ - method: Method.POST, - subscription: "/concatVideos", - secureHandler: async ({ req, res }) => { - // req.body contains the array of server paths to the videos - _success(res, await DashUploadUtils.concatVideos(req.body)); - } - }); - - register({ - method: Method.POST, - subscription: "/uploadFormData", - secureHandler: async ({ req, res }) => { - const form = new formidable.IncomingForm(); - form.keepExtensions = true; - form.uploadDir = pathToDirectory(Directory.parsed_files); - return new Promise<void>(resolve => { - form.parse(req, async (_err, _fields, files) => { - const results: Upload.FileResponse[] = []; - for (const key in files) { - const f = files[key]; - if (!Array.isArray(f)) { - const result = await DashUploadUtils.upload(f); - result && !(result.result instanceof Error) && results.push(result); - } - } - _success(res, results); - resolve(); - }); - }); - } - }); - - register({ - method: Method.POST, - subscription: "/uploadYoutubeVideo", - secureHandler: async ({ req, res }) => { - //req.readableBuffer.head.data - return new Promise<void>(async resolve => { - req.addListener("data", async (args) => { - console.log(args); - const payload = String.fromCharCode.apply(String, args); - const videoId = JSON.parse(payload).videoId; - const results: Upload.FileResponse[] = []; - const result = await DashUploadUtils.uploadYoutube(videoId); - result && !(result.result instanceof Error) && results.push(result); - _success(res, results); - resolve(); - }); - }); - } - }); + register({ + method: Method.POST, + subscription: '/concatVideos', + secureHandler: async ({ req, res }) => { + // req.body contains the array of server paths to the videos + _success(res, await DashUploadUtils.concatVideos(req.body)); + }, + }); - register({ - method: Method.POST, - subscription: new RouteSubscriber("youtubeScreenshot"), - secureHandler: async ({ req, res }) => { - const { id, timecode } = req.body; - const convert = (raw: string) => { - const number = Math.floor(Number(raw)); - const seconds = number % 60; - const minutes = (number - seconds) / 60; - return `${minutes}m${seconds}s`; - }; - const suffix = timecode ? `&t=${convert(timecode)}` : ``; - const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`; - const buffer = await captureYoutubeScreenshot(targetUrl); - if (!buffer) { - return res.send(); + register({ + method: Method.POST, + subscription: '/uploadFormData', + secureHandler: async ({ req, res }) => { + const form = new formidable.IncomingForm(); + form.keepExtensions = true; + form.uploadDir = pathToDirectory(Directory.parsed_files); + return new Promise<void>(resolve => { + form.parse(req, async (_err, _fields, files) => { + const results: Upload.FileResponse[] = []; + for (const key in files) { + const f = files[key]; + if (!Array.isArray(f)) { + const result = await DashUploadUtils.upload(f); + result && !(result.result instanceof Error) && results.push(result); + } } - const resolvedName = `youtube_capture_${id}_${suffix}.png`; - const resolvedPath = serverPathToFile(Directory.images, resolvedName); - return new Promise<void>(resolve => { - writeFile(resolvedPath, buffer, async error => { - if (error) { - return res.send(); - } - await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images)); - res.send({ - accessPaths: { - agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName) - } - } as Upload.FileInformation); - resolve(); - }); - }); - } - }); + _success(res, results); + resolve(); + }); + }); + }, + }); - register({ - method: Method.POST, - subscription: "/uploadRemoteImage", - secureHandler: async ({ req, res }) => { + register({ + method: Method.POST, + subscription: '/uploadYoutubeVideo', + secureHandler: async ({ req, res }) => { + //req.readableBuffer.head.data + return new Promise<void>(async resolve => { + req.addListener('data', async args => { + console.log(args); + const payload = String.fromCharCode.apply(String, args); + const videoId = JSON.parse(payload).videoId; + const results: Upload.FileResponse[] = []; + const result = await DashUploadUtils.uploadYoutube(videoId); + result && results.push(result); + _success(res, results); + resolve(); + }); + }); + }, + }); - const { sources } = req.body; - if (Array.isArray(sources)) { - const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source))); - return res.send(results); + register({ + method: Method.POST, + subscription: new RouteSubscriber('youtubeScreenshot'), + secureHandler: async ({ req, res }) => { + const { id, timecode } = req.body; + const convert = (raw: string) => { + const number = Math.floor(Number(raw)); + const seconds = number % 60; + const minutes = (number - seconds) / 60; + return `${minutes}m${seconds}s`; + }; + const suffix = timecode ? `&t=${convert(timecode)}` : ``; + const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`; + const buffer = await captureYoutubeScreenshot(targetUrl); + if (!buffer) { + return res.send(); + } + const resolvedName = `youtube_capture_${id}_${suffix}.png`; + const resolvedPath = serverPathToFile(Directory.images, resolvedName); + return new Promise<void>(resolve => { + writeFile(resolvedPath, buffer, async error => { + if (error) { + return res.send(); } - res.send(); - } - }); + await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images)); + res.send({ + accessPaths: { + agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName), + }, + } as Upload.FileInformation); + resolve(); + }); + }); + }, + }); - register({ - method: Method.POST, - subscription: "/uploadDoc", - secureHandler: ({ req, res }) => { + register({ + method: Method.POST, + subscription: '/uploadRemoteImage', + secureHandler: async ({ req, res }) => { + const { sources } = req.body; + if (Array.isArray(sources)) { + const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source))); + return res.send(results); + } + res.send(); + }, + }); - const form = new formidable.IncomingForm(); - form.keepExtensions = true; - // let path = req.body.path; - const ids: { [id: string]: string } = {}; - let remap = true; - const getId = (id: string): string => { - if (!remap) return id; - if (id.endsWith("Proto")) return id; - if (id in ids) { - return ids[id]; - } else { - return ids[id] = v4(); - } - }; - const mapFn = (doc: any) => { - if (doc.id) { - doc.id = getId(doc.id); - } - for (const key in doc.fields) { - if (!doc.fields.hasOwnProperty(key)) { continue; } - const field = doc.fields[key]; - if (field === undefined || field === null) { continue; } + register({ + method: Method.POST, + subscription: '/uploadDoc', + secureHandler: ({ req, res }) => { + const form = new formidable.IncomingForm(); + form.keepExtensions = true; + // let path = req.body.path; + const ids: { [id: string]: string } = {}; + let remap = true; + const getId = (id: string): string => { + if (!remap) return id; + if (id.endsWith('Proto')) return id; + if (id in ids) { + return ids[id]; + } else { + return (ids[id] = v4()); + } + }; + const mapFn = (doc: any) => { + if (doc.id) { + doc.id = getId(doc.id); + } + for (const key in doc.fields) { + if (!doc.fields.hasOwnProperty(key)) { + continue; + } + const field = doc.fields[key]; + if (field === undefined || field === null) { + continue; + } - if (field.__type === "Doc") { - mapFn(field); - } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") { - field.fieldId = getId(field.fieldId); - } else if (field.__type === "script" || field.__type === "computed") { - if (field.captures) { - field.captures.fieldId = getId(field.captures.fieldId); - } - } else if (field.__type === "list") { - mapFn(field); - } else if (typeof field === "string") { - const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g; - doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => { - return `${p1}${getId(p2)}"`; - }); - } else if (field.__type === "RichTextField") { - const re = /("href"\s*:\s*")(.*?)"/g; - field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => { - return `${p1}${getId(p2)}"`; - }); - } - } - }; - return new Promise<void>(resolve => { - form.parse(req, async (_err, fields, files) => { - remap = fields.remap !== "false"; - let id: string = ""; + if (field.__type === 'Doc') { + mapFn(field); + } else if (field.__type === 'proxy' || field.__type === 'prefetch_proxy') { + field.fieldId = getId(field.fieldId); + } else if (field.__type === 'script' || field.__type === 'computed') { + if (field.captures) { + field.captures.fieldId = getId(field.captures.fieldId); + } + } else if (field.__type === 'list') { + mapFn(field); + } else if (typeof field === 'string') { + const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g; + doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => { + return `${p1}${getId(p2)}"`; + }); + } else if (field.__type === 'RichTextField') { + const re = /("href"\s*:\s*")(.*?)"/g; + field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => { + return `${p1}${getId(p2)}"`; + }); + } + } + }; + return new Promise<void>(resolve => { + form.parse(req, async (_err, fields, files) => { + remap = fields.remap !== 'false'; + let id: string = ''; + try { + for (const name in files) { + const f = files[name]; + const path_2 = Array.isArray(f) ? '' : f.path; + const zip = new AdmZip(path_2); + zip.getEntries().forEach((entry: any) => { + if (!entry.entryName.startsWith('files/')) return; + let directory = dirname(entry.entryName) + '/'; + const extension = extname(entry.entryName); + const base = basename(entry.entryName).split('.')[0]; try { - for (const name in files) { - const f = files[name]; - const path_2 = Array.isArray(f) ? "" : f.path; - const zip = new AdmZip(path_2); - zip.getEntries().forEach((entry: any) => { - if (!entry.entryName.startsWith("files/")) return; - let directory = dirname(entry.entryName) + "/"; - const extension = extname(entry.entryName); - const base = basename(entry.entryName).split(".")[0]; - try { - zip.extractEntryTo(entry.entryName, publicDirectory, true, false); - directory = "/" + directory; - - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension)); - } catch (e) { - console.log(e); - } - }); - const json = zip.getEntry("doc.json"); - try { - const data = JSON.parse(json.getData().toString("utf8")); - const datadocs = data.docs; - id = getId(data.id); - const docs = Object.keys(datadocs).map(key => datadocs[key]); - docs.forEach(mapFn); - await Promise.all(docs.map((doc: any) => new Promise<void>(res => { - Database.Instance.replace(doc.id, doc, (err, r) => { - err && console.log(err); - res(); - }, true); - }))); - } catch (e) { console.log(e); } - unlink(path_2, () => { }); - } - SolrManager.update(); - res.send(JSON.stringify(id || "error")); - } catch (e) { console.log(e); } - resolve(); - }); - }); - } - }); - - register({ - method: Method.POST, - subscription: "/inspectImage", - secureHandler: async ({ req, res }) => { + zip.extractEntryTo(entry.entryName, publicDirectory, true, false); + directory = '/' + directory; - const { source } = req.body; - if (typeof source === "string") { - return res.send(await DashUploadUtils.InspectImage(source)); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_o' + extension)); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_s' + extension)); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_m' + extension)); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_l' + extension)); + } catch (e) { + console.log(e); + } + }); + const json = zip.getEntry('doc.json'); + try { + const data = JSON.parse(json.getData().toString('utf8')); + const datadocs = data.docs; + id = getId(data.id); + const docs = Object.keys(datadocs).map(key => datadocs[key]); + docs.forEach(mapFn); + await Promise.all( + docs.map( + (doc: any) => + new Promise<void>(res => { + Database.Instance.replace( + doc.id, + doc, + (err, r) => { + err && console.log(err); + res(); + }, + true + ); + }) + ) + ); + } catch (e) { + console.log(e); + } + unlink(path_2, () => {}); + } + SolrManager.update(); + res.send(JSON.stringify(id || 'error')); + } catch (e) { + console.log(e); } - res.send({}); - } - }); + resolve(); + }); + }); + }, + }); + + register({ + method: Method.POST, + subscription: '/inspectImage', + secureHandler: async ({ req, res }) => { + const { source } = req.body; + if (typeof source === 'string') { + return res.send(await DashUploadUtils.InspectImage(source)); + } + res.send({}); + }, + }); register({ method: Method.POST, - subscription: "/uploadURI", + subscription: '/uploadURI', secureHandler: ({ req, res }) => { const uri = req.body.uri; const filename = req.body.name; const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original; const deleteFiles = req.body.replaceRootFilename; if (!uri || !filename) { - res.status(401).send("incorrect parameters specified"); + res.status(401).send('incorrect parameters specified'); return; } if (deleteFiles) { - const path = serverPathToFile(Directory.images, ""); + const path = serverPathToFile(Directory.images, ''); const regex = new RegExp(`${deleteFiles}.*`); - fs.readdirSync(path).filter((f: any) => regex.test(f)).map((f: any) => fs.unlinkSync(path + f)); + fs.readdirSync(path) + .filter((f: any) => regex.test(f)) + .map((f: any) => fs.unlinkSync(path + f)); } return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => { const ext = extname(savedName).toLowerCase(); const { pngs, jpgs } = AcceptableMedia; - const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium }] : [ - { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Small }, - { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium }, - { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Large }, - ]; + const resizers = !origSuffix + ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium }] + : [ + { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Small }, + { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium }, + { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Large }, + ]; let isImage = false; if (pngs.includes(ext)) { resizers.forEach(element => { @@ -301,49 +318,48 @@ export default class UploadManager extends ApiManager { if (isImage) { resizers.forEach(resizer => { const path = serverPathToFile(Directory.images, InjectSize(filename, resizer.suffix) + ext); - createReadStream(savedName).on("error", e => console.log("Resizing read:" + e)) + createReadStream(savedName) + .on('error', e => console.log('Resizing read:' + e)) .pipe(resizer.resizer) - .pipe(createWriteStream(path).on("error", e => console.log("Resizing write: " + e))); + .on('error', e => console.log('Resizing write: ' + e)) + .pipe(createWriteStream(path).on('error', e => console.log('Resizing write: ' + e))); }); - - } - res.send(clientPathToFile(Directory.images, filename + ext)); - }); - } - }); - - } - + } + res.send(clientPathToFile(Directory.images, filename + ext)); + }); + }, + }); + } } function delay(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => setTimeout(resolve, ms)); } /** * On success, returns a buffer containing the bytes of a screenshot * of the video (optionally, at a timecode) specified by @param targetUrl. - * + * * On failure, returns undefined. */ async function captureYoutubeScreenshot(targetUrl: string) { - // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); - // const page = await browser.newPage(); - // // await page.setViewport({ width: 1920, height: 1080 }); + // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); + // const page = await browser.newPage(); + // // await page.setViewport({ width: 1920, height: 1080 }); - // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any }); + // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any }); - // const videoPlayer = await page.$('.html5-video-player'); - // videoPlayer && await page.focus("video"); - // await delay(7000); - // const ad = await page.$('.ytp-ad-skip-button-text'); - // await ad?.click(); - // await videoPlayer?.click(); - // await delay(1000); - // // hide youtube player controls. - // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none'); + // const videoPlayer = await page.$('.html5-video-player'); + // videoPlayer && await page.focus("video"); + // await delay(7000); + // const ad = await page.$('.ytp-ad-skip-button-text'); + // await ad?.click(); + // await videoPlayer?.click(); + // await delay(1000); + // // hide youtube player controls. + // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none'); - // const buffer = await videoPlayer?.screenshot({ encoding: "binary" }); - // await browser.close(); + // const buffer = await videoPlayer?.screenshot({ encoding: "binary" }); + // await browser.close(); - // return buffer; - return null; -}
\ No newline at end of file + // return buffer; + return null; +} diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index 7be8a1e9f..53e55c1c3 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -1,10 +1,10 @@ -import ApiManager, { Registration } from "./ApiManager"; -import { Method } from "../RouteManager"; -import { Database } from "../database"; -import { msToTime } from "../ActionUtilities"; -import * as bcrypt from "bcrypt-nodejs"; -import { Opt } from "../../fields/Doc"; -import { WebSocket } from "../websocket"; +import ApiManager, { Registration } from './ApiManager'; +import { Method } from '../RouteManager'; +import { Database } from '../database'; +import { msToTime } from '../ActionUtilities'; +import * as bcrypt from 'bcrypt-nodejs'; +import { Opt } from '../../fields/Doc'; +import { WebSocket } from '../websocket'; export const timeMap: { [id: string]: number } = {}; interface ActivityUnit { @@ -13,28 +13,26 @@ interface ActivityUnit { } export default class UserManager extends ApiManager { - protected initialize(register: Registration): void { - register({ method: Method.GET, - subscription: "/getUsers", + subscription: '/getUsers', secureHandler: async ({ res }) => { - const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, "users"); + const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, 'users'); const results = await cursor.toArray(); res.send(results.map((user: any) => ({ email: user.email, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }))); - } + }, }); register({ method: Method.POST, - subscription: "/setCacheDocumentIds", + subscription: '/setCacheDocumentIds', secureHandler: async ({ user, req, res }) => { const result: any = {}; user.cacheDocumentIds = req.body.cacheDocumentIds; user.save(err => { if (err) { - result.error = [{ msg: "Error while caching documents" }]; + result.error = [{ msg: 'Error while caching documents' }]; } }); @@ -42,32 +40,35 @@ export default class UserManager extends ApiManager { // console.log(e); // }); res.send(result); - } + }, }); register({ method: Method.GET, - subscription: "/getUserDocumentIds", - secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }) + subscription: '/getUserDocumentIds', + secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }), + publicHandler: ({ res }) => res.send({ userDocumentId: '__guest__', linkDatabaseId: 3, sharingDocumentId: 2 }), }); register({ method: Method.GET, - subscription: "/getSharingDocumentId", - secureHandler: ({ res, user }) => res.send(user.sharingDocumentId) + subscription: '/getSharingDocumentId', + secureHandler: ({ res, user }) => res.send(user.sharingDocumentId), + publicHandler: ({ res }) => res.send(2), }); register({ method: Method.GET, - subscription: "/getLinkDatabaseId", - secureHandler: ({ res, user }) => res.send(user.linkDatabaseId) + subscription: '/getLinkDatabaseId', + secureHandler: ({ res, user }) => res.send(user.linkDatabaseId), + publicHandler: ({ res }) => res.send(3), }); register({ method: Method.GET, - subscription: "/getCurrentUser", + subscription: '/getCurrentUser', secureHandler: ({ res, user: { _id, email, cacheDocumentIds } }) => res.send(JSON.stringify({ id: _id, email, cacheDocumentIds })), - publicHandler: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" })) + publicHandler: ({ res }) => res.send(JSON.stringify({ id: '__guest__', email: 'guest' })), }); register({ @@ -80,7 +81,7 @@ export default class UserManager extends ApiManager { const validated = await new Promise<Opt<boolean>>(resolve => { bcrypt.compare(curr_pass, user.password, (err, passwords_match) => { if (err || !passwords_match) { - result.error = [{ msg: "Incorrect current password" }]; + result.error = [{ msg: 'Incorrect current password' }]; res.send(result); resolve(undefined); } else { @@ -93,10 +94,10 @@ export default class UserManager extends ApiManager { return; } - req.assert("new_pass", "Password must be at least 4 characters long").len({ min: 4 }); - req.assert("new_confirm", "Passwords do not match").equals(new_pass); + req.assert('new_pass', 'Password must be at least 4 characters long').len({ min: 4 }); + req.assert('new_confirm', 'Passwords do not match').equals(new_pass); if (curr_pass === new_pass) { - result.error = [{ msg: "Current and new password are the same" }]; + result.error = [{ msg: 'Current and new password are the same' }]; } // was there error in validating new passwords? if (req.validationErrors()) { @@ -113,17 +114,17 @@ export default class UserManager extends ApiManager { user.save(err => { if (err) { - result.error = [{ msg: "Error while saving new password" }]; + result.error = [{ msg: 'Error while saving new password' }]; } }); res.send(result); - } + }, }); register({ method: Method.GET, - subscription: "/activity", + subscription: '/activity', secureHandler: ({ res }) => { const now = Date.now(); @@ -135,25 +136,23 @@ export default class UserManager extends ApiManager { const socketPair = Array.from(WebSocket.socketMap).find(pair => pair[1] === user); if (socketPair && !socketPair[0].disconnected) { const duration = now - time; - const target = (duration / 1000) < (60 * 5) ? activeTimes : inactiveTimes; + const target = duration / 1000 < 60 * 5 ? activeTimes : inactiveTimes; target.push({ user, duration }); } } - const process = (target: { user: string, duration: number }[]) => { + const process = (target: { user: string; duration: number }[]) => { const comparator = (first: ActivityUnit, second: ActivityUnit) => first.duration - second.duration; const sorted = target.sort(comparator); return sorted.map(({ user, duration }) => `${user} (${msToTime(duration)})`); }; - res.render("user_activity.pug", { - title: "User Activity", + res.render('user_activity.pug', { + title: 'User Activity', active: process(activeTimes), - inactive: process(inactiveTimes) + inactive: process(inactiveTimes), }); - } + }, }); - } - -}
\ No newline at end of file +} diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index cae35da60..8cf657da4 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -4,33 +4,31 @@ import * as exifr from 'exifr'; import { File } from 'formidable'; import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs'; import * as path from 'path'; -import { basename } from "path"; +import { basename } from 'path'; import * as sharp from 'sharp'; import { Stream } from 'stream'; import { filesDirectory, publicDirectory } from '.'; import { Opt } from '../fields/Doc'; -import { ParsedPDF } from "../server/PdfTypes"; +import { ParsedPDF } from '../server/PdfTypes'; import { Utils } from '../Utils'; import { createIfNotExists } from './ActionUtilities'; import { clientPathToFile, Directory, pathToDirectory, serverPathToFile } from './ApiManagers/UploadManager'; -import { resolvedServerUrl } from "./server_Initialization"; +import { resolvedServerUrl } from './server_Initialization'; import { AcceptableMedia, Upload } from './SharedMediaTypes'; import request = require('request-promise'); import formidable = require('formidable'); -import { file } from 'jszip'; -import { csvParser } from './DataVizUtils'; -const { exec } = require("child_process"); +const { exec } = require('child_process'); const parse = require('pdf-parse'); -const ffmpeg = require("fluent-ffmpeg"); -const fs = require("fs"); -const requestImageSize = require("../client/util/request-image-size"); +const ffmpeg = require('fluent-ffmpeg'); +const fs = require('fs'); +const requestImageSize = require('../client/util/request-image-size'); export enum SizeSuffix { - Small = "_s", - Medium = "_m", - Large = "_l", - Original = "_o", - None = "" + Small = '_s', + Medium = '_m', + Large = '_l', + Original = '_o', + None = '', } export function InjectSize(filename: string, size: SizeSuffix) { @@ -43,7 +41,6 @@ function isLocal() { } export namespace DashUploadUtils { - export interface Size { width: number; suffix: SizeSuffix; @@ -59,19 +56,19 @@ export namespace DashUploadUtils { return AcceptableMedia.imageFormats.includes(path.extname(url).toLowerCase()); } - const size = "content-length"; - const type = "content-type"; + const size = 'content-length'; + const type = 'content-type'; + + const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr - const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr - export async function concatVideos(filePaths: string[]): Promise<Upload.AccessPathInfo> { // make a list of paths to create the ordered text file for ffmpeg const inputListName = 'concat.txt'; const textFilePath = path.join(filesDirectory, inputListName); // make a list of paths to create the ordered text file for ffmpeg - const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n'); + const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n'); // write the text file to the file system - writeFile(textFilePath, filePathsText, (err) => console.log(err)); + writeFile(textFilePath, filePathsText, err => console.log(err)); // make output file name based on timestamp const outputFileName = `output-${Utils.GenerateGuid()}.mp4`; @@ -81,87 +78,126 @@ export namespace DashUploadUtils { // concatenate the videos await new Promise((resolve, reject) => { var merge = ffmpeg(); - merge.input(textFilePath) - .inputOptions(['-f concat', '-safe 0']) + merge + .input(textFilePath) + .inputOptions(['-f concat', '-safe 0']) .outputOptions('-c copy') //.videoCodec("copy") .save(outputFilePath) - .on("error", reject) - .on("end", resolve); - }) - - // delete concat.txt from the file system - unlinkSync(textFilePath); - // delete the old segment videos from the server - filePaths.forEach(filePath => unlinkSync(filePath)); - - // return the path(s) to the output file - return { - accessPaths: getAccessPaths(Directory.videos, outputFileName) - } + .on('error', reject) + .on('end', resolve); + }); + + // delete concat.txt from the file system + unlinkSync(textFilePath); + // delete the old segment videos from the server + filePaths.forEach(filePath => unlinkSync(filePath)); + + // return the path(s) to the output file + return { + accessPaths: getAccessPaths(Directory.videos, outputFileName), + }; } export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> { - console.log("UPLOAD " + videoId); return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => { - exec('youtube-dl -o ' + (videoId + ".mp4") + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"', - (error: any, stdout: any, stderr: any) => { - if (error) console.log(`error: ${error.message}`); - else if (stderr) console.log(`stderr: ${stderr}`); - else { - console.log(`stdout: ${stdout}`); - const data = { size: 0, path: videoId + ".mp4", name: videoId, type: "video/mp4" }; - const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any }) }; + console.log('Uploading YouTube video: ' + videoId); + exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' -f "mp4[filesize<5M]/bestvideo[filesize<5M]+bestaudio/bestvideo+bestaudio"', (error: any, stdout: any, stderr: any) => { + if (error) { + console.log(`error: Error: ${error.message}`); + res({ + source: { + size: 0, + path: videoId, + name: videoId, + type: '', + toJSON: () => ({ name: videoId, path: videoId }), + }, + result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}). Error: ${error.message}` }, + }); + } else { + exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' --get-duration', (error: any, stdout: any, stderr: any) => { + const time = Array.from(stdout.trim().split(':')).reverse(); + const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0); + const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' }; + const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: duration.toString(), mime: '', toJson: () => undefined as any }) }; res(MoveParsedFile(file, Directory.videos)); - } - }); + }); + } + }); }); } export async function upload(file: File): Promise<Upload.FileResponse> { const { type, path, name } = file; - const types = type?.split("/") ?? []; + const types = type?.split('/') ?? []; const category = types[0]; let format = `.${types[1]}`; console.log(green(`Processing upload of file (${name}) and format (${format}) with upload type (${type}) in category (${category}).`)); - + switch (category) { - case "image": + case 'image': if (imageFormats.includes(format)) { const result = await UploadImage(path, basename(path)); return { source: file, result }; } - case "video": - if (format.includes("x-matroska")) { - console.log("case video"); - await new Promise(res => ffmpeg(file.path) - .videoCodec("copy") // this will copy the data instead of reencode it - .save(file.path.replace(".mkv", ".mp4")) - .on('end', res)); - file.path = file.path.replace(".mkv", ".mp4"); - format = ".mp4"; + return { source: file, result: { name: 'Unsupported image format', message: `Could not upload unsupported file (${name}). Please convert to an .jpg` } }; + case 'video': + if (format.includes('x-matroska')) { + console.log('case video'); + await new Promise(res => + ffmpeg(file.path) + .videoCodec('copy') // this will copy the data instead of reencode it + .save(file.path.replace('.mkv', '.mp4')) + .on('end', res) + ); + file.path = file.path.replace('.mkv', '.mp4'); + format = '.mp4'; + } + if (format.includes('quicktime')) { + let abort = false; + await new Promise<void>(res => + ffmpeg.ffprobe(file.path, (err: any, metadata: any) => { + if (metadata.streams.some((stream: any) => stream.codec_name === 'hevc')) { + abort = true; + } + res(); + }) + ); + if (abort) return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } }; + // bcz: instead of aborting, we could convert the file using the code below to an mp4. Problem is that this takes a long time and will clog up the server. + // await new Promise(res => + // ffmpeg(file.path) + // .videoCodec('libx264') // this will copy the data instead of reencode it + // .audioCodec('mp2') + // .save(file.path.replace('.MOV', '.mp4').replace('.mov', '.mp4')) + // .on('end', res) + // ); + // file.path = file.path.replace('.mov', '.mp4').replace('.MOV', '.mp4'); + // format = '.mp4'; } if (videoFormats.includes(format)) { return MoveParsedFile(file, Directory.videos); } - case "application": + return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } }; + case 'application': if (applicationFormats.includes(format)) { return UploadPdf(file); } - case "audio": - const components = format.split(";"); + case 'audio': + const components = format.split(';'); if (components.length > 1) { format = components[0]; } if (audioFormats.includes(format)) { return UploadAudio(file, format); } - case "text": - if (types[1] == "csv") { + return { source: file, result: { name: 'Unsupported audio format', message: `Could not upload unsupported file (${name}). Please convert to an .mp3` } }; + case 'text': + if (types[1] == 'csv') { return UploadCsv(file); } - } console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`)); @@ -176,22 +212,21 @@ export namespace DashUploadUtils { const name = path.basename(sourcePath); const textFilename = `${name.substring(0, name.length - 4)}.txt`; const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename)); - writeStream.write(result.text, error => error ? reject(error) : resolve()); + writeStream.write(result.text, error => (error ? reject(error) : resolve())); }); return MoveParsedFile(file, Directory.pdfs, undefined, result.text); } async function UploadCsv(file: File) { - const { path: sourcePath } = file; - // read the file as a string + const { path: sourcePath } = file; + // read the file as a string const data = readFileSync(sourcePath, 'utf8'); // split the string into an array of lines return MoveParsedFile(file, Directory.csv, undefined, data); // console.log(csvParser(data)); - } - const manualSuffixes = [".webm"]; + const manualSuffixes = ['.webm']; async function UploadAudio(file: File, format: string) { const suffix = manualSuffixes.includes(format) ? format : undefined; @@ -200,22 +235,22 @@ export namespace DashUploadUtils { /** * Uploads an image specified by the @param source to Dash's /public/files/ - * directory, and returns information generated during that upload - * + * directory, and returns information generated during that upload + * * @param {string} source is either the absolute path of an already uploaded image or * the url of a remote image * @param {string} filename dictates what to call the image. If not specified, * the name {@param prefix}_upload_{GUID} * @param {string} prefix is a string prepended to the generated image name in the * event that @param filename is not specified - * + * * @returns {ImageUploadInformation | Error} This method returns * 1) the paths to the uploaded images (plural due to resizing) * 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed * 3) the size of the image, in bytes (4432130) * 4) the content type of the image, i.e. image/(jpeg | png | ...) */ - export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<Upload.ImageInformation | Error> => { + export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => { const metadata = await InspectImage(source); if (metadata instanceof Error) { return metadata; @@ -225,12 +260,12 @@ export namespace DashUploadUtils { export async function buildFileDirectories() { if (!existsSync(publicDirectory)) { - console.error("\nPlease ensure that the following directory exists...\n"); + console.error('\nPlease ensure that the following directory exists...\n'); console.log(publicDirectory); process.exit(0); } if (!existsSync(filesDirectory)) { - console.error("\nPlease ensure that the following directory exists...\n"); + console.error('\nPlease ensure that the following directory exists...\n'); console.log(filesDirectory); process.exit(0); } @@ -252,7 +287,7 @@ export namespace DashUploadUtils { /** * Based on the url's classification as local or remote, gleans * as much information as possible about the specified image - * + * * @param source is the path or url to the image in question */ export const InspectImage = async (source: string): Promise<Upload.InspectionResults | Error> => { @@ -265,9 +300,9 @@ export namespace DashUploadUtils { */ if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) { const [ext, data] = rawMatches.slice(1, 3); - const resolved = filename = `upload_${Utils.GenerateGuid()}.${ext}`; + const resolved = (filename = `upload_${Utils.GenerateGuid()}.${ext}`); const error = await new Promise<Error | null>(resolve => { - writeFile(serverPathToFile(Directory.images, resolved), data, "base64", resolve); + writeFile(serverPathToFile(Directory.images, resolved), data, 'base64', resolve); }); if (error !== null) { return error; @@ -276,12 +311,12 @@ export namespace DashUploadUtils { } let resolvedUrl: string; /** - * + * * At this point, we want to take whatever url we have and make sure it's requestable. * Anything that's hosted by some other website already is, but if the url is a local file url * (locates the file on this server machine), we have to resolve the client side url by cutting out the * basename subtree (i.e. /images/<some_guid>.<ext>) and put it on the end of the server's url. - * + * * This can always be localhost, regardless of whether this is on the server or not, since we (the server, not the client) * will be the ones making the request, and from the perspective of dash-release or dash-web, localhost:<port> refers to the same thing * as the full dash-release.eastus.cloudapp.azure.com:<port>. @@ -290,18 +325,18 @@ export namespace DashUploadUtils { if (matches === null) { resolvedUrl = source; } else { - resolvedUrl = `${resolvedServerUrl}/${matches[1].split("\\").join("/")}`; + resolvedUrl = `${resolvedServerUrl}/${matches[1].split('\\').join('/')}`; } // See header comments: not all image files have exif data (I believe only JPG is the only format that can have it) const exifData = await parseExifData(resolvedUrl); const results = { exifData, - requestable: resolvedUrl + requestable: resolvedUrl, }; // Use the request library to parse out file level image information in the headers - const { headers } = (await new Promise<any>((resolve, reject) => { - request.head(resolvedUrl, (error, res) => error ? reject(error) : resolve(res)); - }).catch(console.error)); + const { headers } = await new Promise<any>((resolve, reject) => { + request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res))); + }).catch(console.error); try { // Compute the native width and height ofthe image with an npm module const { width: nativeWidth, height: nativeHeight } = await requestImageSize(resolvedUrl); @@ -313,7 +348,7 @@ export namespace DashUploadUtils { nativeWidth, nativeHeight, filename, - ...results + ...results, }; } catch (e: any) { console.log(e); @@ -331,7 +366,7 @@ export namespace DashUploadUtils { * @param suffix If the file doesn't have a suffix and you want to provide it one * to appear in the new location */ - export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string): Promise<Upload.FileResponse> { + export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string, duration?: number): Promise<Upload.FileResponse> { const { path: sourcePath } = file; let name = path.basename(sourcePath); suffix && (name += suffix); @@ -340,12 +375,15 @@ export namespace DashUploadUtils { rename(sourcePath, destinationPath, error => { resolve({ source: file, - result: error ? error : { - accessPaths: { - agnostic: getAccessPaths(destination, name) - }, - rawText: text - } + result: error + ? error + : { + accessPaths: { + agnostic: getAccessPaths(destination, name), + }, + rawText: text, + duration, + }, }); }); }); @@ -354,19 +392,19 @@ export namespace DashUploadUtils { export function getAccessPaths(directory: Directory, fileName: string) { return { client: clientPathToFile(directory, fileName), - server: serverPathToFile(directory, fileName) + server: serverPathToFile(directory, fileName), }; } - export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<Upload.ImageInformation> => { + export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => { const { requestable, source, ...remaining } = metadata; - const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split("/")[1].toLowerCase()}`; + const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`; const { images } = Directory; const information: Upload.ImageInformation = { accessPaths: { - agnostic: getAccessPaths(images, resolved) + agnostic: getAccessPaths(images, resolved), }, - ...metadata + ...metadata, }; const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images)); for (const suffix of Object.keys(writtenFiles)) { @@ -383,9 +421,9 @@ export namespace DashUploadUtils { const val: any = layer[key]; if (val instanceof Buffer) { layer[key] = val.toString(); - } else if (Array.isArray(val) && typeof val[0] === "number") { + } else if (Array.isArray(val) && typeof val[0] === 'number') { layer[key] = Buffer.from(val).toString(); - } else if (typeof val === "object") { + } else if (typeof val === 'object') { bufferConverterRec(val); } } @@ -410,20 +448,20 @@ export namespace DashUploadUtils { const pngOptions = { compressionLevel: 9, adaptiveFiltering: true, - force: true + force: true, }; export async function outputResizedImages(streamProvider: () => Stream | Promise<Stream>, outputFileName: string, outputDirectory: string) { const writtenFiles: { [suffix: string]: string } = {}; for (const { resizer, suffix } of resizers(path.extname(outputFileName))) { - const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix)); + const outputPath = path.resolve(outputDirectory, (writtenFiles[suffix] = InjectSize(outputFileName, suffix))); await new Promise<void>(async (resolve, reject) => { const source = streamProvider(); let readStream: Stream = source instanceof Promise ? await source : source; if (resizer) { readStream = readStream.pipe(resizer.withMetadata()); } - readStream.pipe(createWriteStream(outputPath)).on("close", resolve).on("error", reject); + readStream.pipe(createWriteStream(outputPath)).on('close', resolve).on('error', reject); }); } return writtenFiles; @@ -442,15 +480,14 @@ export namespace DashUploadUtils { initial = initial.webp(); } else if (tiffs.includes(ext)) { initial = initial.tiff(); - } else if (ext === ".gif") { + } else if (ext === '.gif') { initial = undefined; } return { resizer: initial, - suffix + suffix, }; - }) + }), ]; } - -}
\ No newline at end of file +} diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index aa9bfcfa7..5683cd539 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -1,12 +1,12 @@ import { cyan, green, red } from 'colors'; import { Express, Request, Response } from 'express'; -import { AdminPriviliges } from "."; -import { DashUserModel } from "./authentication/DashUserModel"; -import RouteSubscriber from "./RouteSubscriber"; +import { AdminPriviliges } from '.'; +import { DashUserModel } from './authentication/DashUserModel'; +import RouteSubscriber from './RouteSubscriber'; export enum Method { GET, - POST + POST, } export interface CoreArguments { @@ -33,13 +33,13 @@ const registered = new Map<string, Set<Method>>(); enum RegistrationError { Malformed, - Duplicate + Duplicate, } export default class RouteManager { private server: Express; private _isRelease: boolean; - private failedRegistrations: { route: string, reason: RegistrationError }[] = []; + private failedRegistrations: { route: string; reason: RegistrationError }[] = []; public get isRelease() { return this._isRelease; @@ -74,39 +74,42 @@ export default class RouteManager { } process.exit(1); } else { - console.log(green("all server routes have been successfully registered:")); - Array.from(registered.keys()).sort().forEach(route => console.log(cyan(route))); + console.log(green('all server routes have been successfully registered:')); + Array.from(registered.keys()) + .sort() + .forEach(route => console.log(cyan(route))); console.log(); } - } + }; static routes: string[] = []; /** - * - * @param initializer + * + * @param initializer */ addSupervisedRoute = (initializer: RouteInitializer): void => { const { method, subscription, secureHandler, publicHandler, errorHandler, requireAdminInRelease: requireAdmin } = initializer; - typeof (initializer.subscription) === "string" && RouteManager.routes.push(initializer.subscription); + typeof initializer.subscription === 'string' && RouteManager.routes.push(initializer.subscription); initializer.subscription instanceof RouteSubscriber && RouteManager.routes.push(initializer.subscription.root); - initializer.subscription instanceof Array && initializer.subscription.map(sub => { - typeof (sub) === "string" && RouteManager.routes.push(sub); - sub instanceof RouteSubscriber && RouteManager.routes.push(sub.root); - }); + initializer.subscription instanceof Array && + initializer.subscription.map(sub => { + typeof sub === 'string' && RouteManager.routes.push(sub); + sub instanceof RouteSubscriber && RouteManager.routes.push(sub.root); + }); const isRelease = this._isRelease; const supervised = async (req: Request, res: Response) => { let user = req.user as Partial<DashUserModel> | undefined; const { originalUrl: target } = req; - if (process.env.DB === "MEM" && !user) { - user = { id: "guest", email: "", userDocumentId: "guestDocId" }; + if (process.env.DB === 'MEM' && !user) { + user = { id: 'guest', email: 'guest', userDocumentId: '__guest__' }; } const core = { req, res, isRelease }; const tryExecute = async (toExecute: (args: any) => any | Promise<any>, args: any) => { try { await toExecute(args); } catch (e) { - console.log(red(target), user && ("email" in user) ? "<user logged out>" : undefined); + console.log(red(target), user && 'email' in user ? '<user logged out>' : undefined); if (errorHandler) { errorHandler({ ...core, error: e }); } else { @@ -119,7 +122,7 @@ export default class RouteManager { if (AdminPriviliges.get(user.id)) { AdminPriviliges.delete(user.id); } else { - return res.redirect(`/admin/${req.originalUrl.substring(1).replace("/", ":")}`); + return res.redirect(`/admin/${req.originalUrl.substring(1).replace('/', ':')}`); } } await tryExecute(secureHandler, { ...core, user }); @@ -128,10 +131,10 @@ export default class RouteManager { if (publicHandler) { await tryExecute(publicHandler, core); if (!res.headersSent) { - res.redirect("/login"); + // res.redirect("/login"); } } else { - res.redirect("/login"); + res.redirect('/login'); } } setTimeout(() => { @@ -144,7 +147,7 @@ export default class RouteManager { }; const subscribe = (subscriber: RouteSubscriber | string) => { let route: string; - if (typeof subscriber === "string") { + if (typeof subscriber === 'string') { route = subscriber; } else { route = subscriber.build; @@ -152,7 +155,7 @@ export default class RouteManager { if (!/^\/$|^\/[A-Za-z\*]+(\/\:[A-Za-z?_\*]+)*$/g.test(route)) { this.failedRegistrations.push({ reason: RegistrationError.Malformed, - route + route, }); } else { const existing = registered.get(route); @@ -160,7 +163,7 @@ export default class RouteManager { if (existing.has(method)) { this.failedRegistrations.push({ reason: RegistrationError.Duplicate, - route + route, }); return; } @@ -184,15 +187,14 @@ export default class RouteManager { } else { subscribe(subscription); } - } - + }; } export const STATUS = { OK: 200, BAD_REQUEST: 400, EXECUTION_ERROR: 500, - PERMISSION_DENIED: 403 + PERMISSION_DENIED: 403, }; export function _error(res: Response, message: string, error?: any) { diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index cde95526f..7db1c2dae 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -2,36 +2,45 @@ import { ExifData } from 'exif'; import { File } from 'formidable'; export namespace AcceptableMedia { - export const gifs = [".gif"]; - export const pngs = [".png"]; - export const jpgs = [".jpg", ".jpeg"]; - export const webps = [".webp"]; - export const tiffs = [".tiff"]; + export const gifs = ['.gif']; + export const pngs = ['.png']; + export const jpgs = ['.jpg', '.jpeg']; + export const webps = ['.webp']; + export const tiffs = ['.tiff']; export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs]; - export const videoFormats = [".mov", ".mp4", ".quicktime", ".mkv", ".x-matroska;codecs=avc1"]; - export const applicationFormats = [".pdf"]; - export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"]; + export const videoFormats = ['.mov', '.mp4', '.quicktime', '.mkv', '.x-matroska;codecs=avc1']; + export const applicationFormats = ['.pdf']; + export const audioFormats = ['.wav', '.mp3', '.mpeg', '.flac', '.au', '.aiff', '.m4a', '.webm']; } export namespace Upload { - export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation { - return "nativeWidth" in uploadResponse; + return 'nativeWidth' in uploadResponse; + } + + export function isVideoInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.VideoInformation { + return 'duration' in uploadResponse; } export interface FileInformation { accessPaths: AccessPathInfo; rawText?: string; + duration?: number; } - export type FileResponse<T extends FileInformation = FileInformation> = { source: File, result: T | Error }; + export type FileResponse<T extends FileInformation = FileInformation> = { source: File; result: T | Error }; export type ImageInformation = FileInformation & InspectionResults; + export type VideoInformation = FileInformation & VideoResults; + export interface AccessPathInfo { - [suffix: string]: { client: string, server: string }; + [suffix: string]: { client: string; server: string }; } + export interface VideoResults { + duration: number; + } export interface InspectionResults { source: string; requestable: string; @@ -44,8 +53,7 @@ export namespace Upload { } export interface EnrichedExifData { - data: ExifData & ExifData["gps"]; + data: ExifData & ExifData['gps']; error?: string; } - -}
\ No newline at end of file +} diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts index 3622be4c5..52d876e95 100644 --- a/src/server/authentication/AuthenticationManager.ts +++ b/src/server/authentication/AuthenticationManager.ts @@ -1,4 +1,4 @@ -import { default as User, DashUserModel } from './DashUserModel'; +import { default as User, DashUserModel, initializeGuest } from './DashUserModel'; import { Request, Response, NextFunction } from 'express'; import * as passport from 'passport'; import { IVerifyOptions } from 'passport-local'; @@ -30,6 +30,7 @@ export let getSignup = (req: Request, res: Response) => { * Create a new local account. */ export let postSignup = (req: Request, res: Response, next: NextFunction) => { + const email = req.body.email as String; req.assert('email', 'Email is not valid').isEmail(); req.assert('password', 'Password must be at least 4 characters long').len({ min: 4 }); req.assert('confirmPassword', 'Passwords do not match').equals(req.body.password); @@ -41,15 +42,14 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { return res.redirect('/signup'); } - const email = req.body.email as String; const password = req.body.password; const model = { email, password, - userDocumentId: Utils.GenerateGuid(), - sharingDocumentId: Utils.GenerateGuid(), - linkDatabaseId: Utils.GenerateGuid(), + userDocumentId: email === 'guest' ? '__guest__' : Utils.GenerateGuid(), + sharingDocumentId: email === 'guest' ? 2 : Utils.GenerateGuid(), + linkDatabaseId: email === 'guest' ? 3 : Utils.GenerateGuid(), cacheDocumentIds: '', } as Partial<DashUserModel>; @@ -106,18 +106,22 @@ export let getLogin = (req: Request, res: Response) => { * On failure, redirect to signup page */ export let postLogin = (req: Request, res: Response, next: NextFunction) => { - req.assert('email', 'Email is not valid').isEmail(); - req.assert('password', 'Password cannot be blank').notEmpty(); - req.sanitize('email').normalizeEmail({ gmail_remove_dots: false }); - - const errors = req.validationErrors(); + if (req.body.email === '') { + User.findOne({ email: 'guest' }, (err: any, user: DashUserModel) => !user && initializeGuest()); + req.body.email = 'guest'; + req.body.password = 'guest'; + } else { + req.assert('email', 'Email is not valid').isEmail(); + req.assert('password', 'Password cannot be blank').notEmpty(); + req.sanitize('email').normalizeEmail({ gmail_remove_dots: false }); + } - if (errors) { + if (req.validationErrors()) { req.flash('errors', 'Unable to login at this time. Please try again.'); return res.redirect('/signup'); } - passport.authenticate('local', (err: Error, user: DashUserModel, _info: IVerifyOptions) => { + const callback = (err: Error, user: DashUserModel, _info: IVerifyOptions) => { if (err) { next(err); return; @@ -132,7 +136,8 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => { } tryRedirectToTarget(req, res); }); - })(req, res, next); + }; + setTimeout(() => passport.authenticate('local', callback)(req, res, next), 500); }; /** diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index bee28b96d..a1883beab 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -1,13 +1,13 @@ //@ts-ignore -import * as bcrypt from "bcrypt-nodejs"; +import * as bcrypt from 'bcrypt-nodejs'; //@ts-ignore import * as mongoose from 'mongoose'; export type DashUserModel = mongoose.Document & { - email: String, - password: string, - passwordResetToken?: string, - passwordResetExpires?: Date, + email: String; + password: string; + passwordResetToken?: string; + passwordResetExpires?: Date; userDocumentId: string; sharingDocumentId: string; @@ -15,66 +15,74 @@ export type DashUserModel = mongoose.Document & { cacheDocumentIds: string; profile: { - name: string, - gender: string, - location: string, - website: string, - picture: string - }, + name: string; + gender: string; + location: string; + website: string; + picture: string; + }; - comparePassword: comparePasswordFunction, + comparePassword: comparePasswordFunction; }; type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => {}) => void; export type AuthToken = { - accessToken: string, - kind: string + accessToken: string; + kind: string; }; -const userSchema = new mongoose.Schema({ - email: String, - password: String, - passwordResetToken: String, - passwordResetExpires: Date, +const userSchema = new mongoose.Schema( + { + email: String, + password: String, + passwordResetToken: String, + passwordResetExpires: Date, - userDocumentId: String, // id that identifies a document which hosts all of a user's account data - sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users - linkDatabaseId: String, - cacheDocumentIds: String, // set of document ids to retreive on startup + userDocumentId: String, // id that identifies a document which hosts all of a user's account data + sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users + linkDatabaseId: String, + cacheDocumentIds: String, // set of document ids to retreive on startup - facebook: String, - twitter: String, - google: String, + facebook: String, + twitter: String, + google: String, - profile: { - name: String, - gender: String, - location: String, - website: String, - picture: String - } -}, { timestamps: true }); + profile: { + name: String, + gender: String, + location: String, + website: String, + picture: String, + }, + }, + { timestamps: true } +); /** * Password hash middleware. */ -userSchema.pre("save", function save(next) { +userSchema.pre('save', function save(next) { const user = this as DashUserModel; - if (!user.isModified("password")) { + if (!user.isModified('password')) { return next(); } bcrypt.genSalt(10, (err: any, salt: string) => { if (err) { return next(err); } - bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash: string) => { - if (err) { - return next(err); + bcrypt.hash( + user.password, + salt, + () => void {}, + (err: mongoose.Error, hash: string) => { + if (err) { + return next(err); + } + user.password = hash; + next(); } - user.password = hash; - next(); - }); + ); }); }); @@ -88,5 +96,15 @@ const comparePassword: comparePasswordFunction = function (this: DashUserModel, userSchema.methods.comparePassword = comparePassword; -const User = mongoose.model("User", userSchema); -export default User;
\ No newline at end of file +const User = mongoose.model('User', userSchema); +export function initializeGuest() { + new User({ + email: 'guest', + password: 'guest', + userDocumentId: '__guest__', + sharingDocumentId: '2', + linkDatabaseId: '3', + cacheDocumentIds: '', + }).save(); +} +export default User; diff --git a/src/server/index.ts b/src/server/index.ts index f8c32103b..6e6bde3cb 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,35 +1,35 @@ require('dotenv').config(); -import { yellow } from "colors"; +import { yellow } from 'colors'; import * as mobileDetect from 'mobile-detect'; import * as path from 'path'; import * as qs from 'query-string'; -import { log_execution } from "./ActionUtilities"; -import DeleteManager from "./ApiManagers/DeleteManager"; +import { log_execution } from './ActionUtilities'; +import DeleteManager from './ApiManagers/DeleteManager'; import DownloadManager from './ApiManagers/DownloadManager'; -import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; -import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; -import PDFManager from "./ApiManagers/PDFManager"; +import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager'; +import GooglePhotosManager from './ApiManagers/GooglePhotosManager'; +import PDFManager from './ApiManagers/PDFManager'; import { SearchManager } from './ApiManagers/SearchManager'; -import SessionManager from "./ApiManagers/SessionManager"; -import UploadManager from "./ApiManagers/UploadManager"; +import SessionManager from './ApiManagers/SessionManager'; +import UploadManager from './ApiManagers/UploadManager'; import UserManager from './ApiManagers/UserManager'; import UtilManager from './ApiManagers/UtilManager'; import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader'; -import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; -import { DashSessionAgent } from "./DashSession/DashSessionAgent"; -import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; +import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; +import { DashSessionAgent } from './DashSession/DashSessionAgent'; +import { AppliedSessionAgent } from './DashSession/Session/agents/applied_session_agent'; import { DashUploadUtils } from './DashUploadUtils'; import { Database } from './database'; -import { Logger } from "./ProcessFactory"; +import { Logger } from './ProcessFactory'; import RouteManager, { Method, PublicHandler } from './RouteManager'; import RouteSubscriber from './RouteSubscriber'; import initializeServer, { resolvedPorts } from './server_Initialization'; export const AdminPriviliges: Map<string, boolean> = new Map(); -export const onWindows = process.platform === "win32"; +export const onWindows = process.platform === 'win32'; export let sessionAgent: AppliedSessionAgent; -export const publicDirectory = path.resolve(__dirname, "public"); -export const filesDirectory = path.resolve(publicDirectory, "files"); +export const publicDirectory = path.resolve(__dirname, 'public'); +export const filesDirectory = path.resolve(publicDirectory, 'files'); /** * These are the functions run before the server starts @@ -43,11 +43,11 @@ async function preliminaryFunctions() { await GoogleCredentialsLoader.loadCredentials(); SSL.loadCredentials(); GoogleApiServerUtils.processProjectCredentials(); - if (process.env.DB !== "MEM") { + if (process.env.DB !== 'MEM') { await log_execution({ - startMessage: "attempting to initialize mongodb connection", - endMessage: "connection outcome determined", - action: Database.tryInitializeConnection + startMessage: 'attempting to initialize mongodb connection', + endMessage: 'connection outcome determined', + action: Database.tryInitializeConnection, }); } } @@ -56,27 +56,16 @@ async function preliminaryFunctions() { * Either clustered together as an API manager * or individually referenced below, by the completion * of this function's execution, all routes will - * be registered on the server + * be registered on the server * @param router the instance of the route manager * that will manage the registration of new routes * with the server */ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) { - const managers = [ - new SessionManager(), - new UserManager(), - new UploadManager(), - new DownloadManager(), - new SearchManager(), - new PDFManager(), - new DeleteManager(), - new UtilManager(), - new GeneralGoogleManager(), - new GooglePhotosManager(), - ]; + const managers = [new SessionManager(), new UserManager(), new UploadManager(), new DownloadManager(), new SearchManager(), new PDFManager(), new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), new GooglePhotosManager()]; // initialize API Managers - console.log(yellow("\nregistering server routes...")); + console.log(yellow('\nregistering server routes...')); managers.forEach(manager => manager.register(addSupervisedRoute)); /** @@ -84,88 +73,87 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: */ addSupervisedRoute({ method: Method.GET, - subscription: "/", - secureHandler: ({ res }) => res.redirect("/home") + subscription: '/', + secureHandler: ({ res }) => res.redirect('/home'), }); - addSupervisedRoute({ method: Method.GET, - subscription: "/serverHeartbeat", - secureHandler: ({ res }) => res.send(true) + subscription: '/serverHeartbeat', + secureHandler: ({ res }) => res.send(true), }); addSupervisedRoute({ method: Method.GET, - subscription: "/resolvedPorts", - secureHandler: ({ res }) => res.send(resolvedPorts) + subscription: '/resolvedPorts', + secureHandler: ({ res }) => res.send(resolvedPorts), + publicHandler: ({ res }) => res.send(resolvedPorts), }); const serve: PublicHandler = ({ req, res }) => { - const detector = new mobileDetect(req.headers['user-agent'] || ""); + const detector = new mobileDetect(req.headers['user-agent'] || ''); const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; res.sendFile(path.join(__dirname, '../../deploy/' + filename)); }; /** - * Serves a simple password input box for any + * Serves a simple password input box for any */ addSupervisedRoute({ method: Method.GET, - subscription: new RouteSubscriber("admin").add("previous_target"), + subscription: new RouteSubscriber('admin').add('previous_target'), secureHandler: ({ res, isRelease }) => { const { PASSWORD } = process.env; if (!(isRelease && PASSWORD)) { - return res.redirect("/home"); + return res.redirect('/home'); } - res.render("admin.pug", { title: "Enter Administrator Password" }); - } + res.render('admin.pug', { title: 'Enter Administrator Password' }); + }, }); addSupervisedRoute({ method: Method.POST, - subscription: new RouteSubscriber("admin").add("previous_target"), + subscription: new RouteSubscriber('admin').add('previous_target'), secureHandler: async ({ req, res, isRelease, user: { id } }) => { const { PASSWORD } = process.env; if (!(isRelease && PASSWORD)) { - return res.redirect("/home"); + return res.redirect('/home'); } const { password } = req.body; const { previous_target } = req.params; let redirect: string; if (password === PASSWORD) { AdminPriviliges.set(id, true); - redirect = `/${previous_target.replace(":", "/")}`; + redirect = `/${previous_target.replace(':', '/')}`; } else { redirect = `/admin/${previous_target}`; } res.redirect(redirect); - } + }, }); addSupervisedRoute({ method: Method.GET, - subscription: ["/home", new RouteSubscriber("doc").add("docId")], + subscription: ['/home', new RouteSubscriber('doc').add('docId')], secureHandler: serve, publicHandler: ({ req, res, ...remaining }) => { const { originalUrl: target } = req; - const sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true"; - const docAccess = target.startsWith("/doc/"); + const sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === 'true'; + const docAccess = target.startsWith('/doc/'); // since this is the public handler, there's no meaning of '/home' to speak of // since there's no user logged in, so the only viable operation // for a guest is to look at a shared document - if (sharing && docAccess) { + if (docAccess) { serve({ req, res, ...remaining }); } else { - res.redirect("/login"); + res.redirect('/login'); } - } + }, }); logRegistrationOutcome(); } - /** * This function can be used in two different ways. If not in release mode, * this is simply the logic that is invoked to start the server. In release mode, @@ -174,9 +162,9 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: */ export async function launchServer() { await log_execution({ - startMessage: "\nstarting execution of preliminary functions", - endMessage: "completed preliminary functions\n", - action: preliminaryFunctions + startMessage: '\nstarting execution of preliminary functions', + endMessage: 'completed preliminary functions\n', + action: preliminaryFunctions, }); await initializeServer(routeSetter); } diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 1b7f5919f..9b91a35a6 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -1,24 +1,24 @@ -import * as express from "express"; -import { blue, green } from "colors"; -import { createServer, Server } from "https"; -import { networkInterfaces } from "os"; +import { blue } from 'colors'; +import * as express from 'express'; +import { createServer, Server } from 'https'; +import { networkInterfaces } from 'os'; import * as sio from 'socket.io'; -import { Socket } from "socket.io"; -import { Utils } from "../Utils"; +import { Socket } from 'socket.io'; +import { Opt } from '../fields/Doc'; +import { Utils } from '../Utils'; import { logPort } from './ActionUtilities'; -import { timeMap } from "./ApiManagers/UserManager"; -import { GoogleCredentialsLoader, SSL } from "./apis/google/CredentialsLoader"; -import YoutubeApi from "./apis/youtube/youtubeApiSample"; -import { Client } from "./Client"; -import { Database } from "./database"; -import { DocumentsCollection } from "./IDatabase"; -import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from "./Message"; -import { Search } from "./Search"; +import { timeMap } from './ApiManagers/UserManager'; +import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader'; +import YoutubeApi from './apis/youtube/youtubeApiSample'; +import { initializeGuest } from './authentication/DashUserModel'; +import { Client } from './Client'; +import { Database } from './database'; +import { DocumentsCollection } from './IDatabase'; +import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from './Message'; +import { Search } from './Search'; import { resolvedPorts } from './server_Initialization'; -import { Opt } from "../fields/Doc"; export namespace WebSocket { - export let _socket: Socket; const clients: { [key: string]: Client } = {}; export const socketMap = new Map<SocketIO.Socket, string>(); @@ -32,14 +32,14 @@ export namespace WebSocket { resolvedPorts.socket = Number(socketPort); } let socketEndpoint: Opt<Server>; - await new Promise<void>(resolve => socketEndpoint = createServer(SSL.Credentials, app).listen(resolvedPorts.socket, resolve)); + await new Promise<void>(resolve => (socketEndpoint = createServer(SSL.Credentials, app).listen(resolvedPorts.socket, resolve))); io = sio(socketEndpoint!, SSL.Credentials as any); } else { io = sio().listen(resolvedPorts.socket); } - logPort("websocket", resolvedPorts.socket); + logPort('websocket', resolvedPorts.socket); - io.on("connection", function (socket: Socket) { + io.on('connection', function (socket: Socket) { _socket = socket; socket.use((_packet, next) => { const userEmail = socketMap.get(socket); @@ -70,14 +70,14 @@ export namespace WebSocket { socket.join(room); console.log('Client ID ' + socket.id + ' created room ' + room); socket.emit('created', room, socket.id); - } else if (numClients === 1) { console.log('Client ID ' + socket.id + ' joined room ' + room); socket.in(room).emit('join', room); socket.join(room); socket.emit('joined', room, socket.id); socket.in(room).emit('ready'); - } else { // max two clients + } else { + // max two clients socket.emit('full', room); } }); @@ -97,10 +97,10 @@ export namespace WebSocket { console.log('received bye'); }); - Utils.Emit(socket, MessageStore.Foo, "handshooken"); + Utils.Emit(socket, MessageStore.Foo, 'handshooken'); Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid)); - Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)); + Utils.AddServerHandler(socket, MessageStore.SetField, args => setField(socket, args)); Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); if (isRelease) { @@ -126,26 +126,26 @@ export namespace WebSocket { */ disconnect = () => { - socket.broadcast.emit("connection_terminated", Date.now()); + socket.broadcast.emit('connection_terminated', Date.now()); socket.disconnect(true); }; }); } function processGesturePoints(socket: Socket, content: GestureContent) { - socket.broadcast.emit("receiveGesturePoints", content); + socket.broadcast.emit('receiveGesturePoints', content); } function processOverlayTrigger(socket: Socket, content: MobileInkOverlayContent) { - socket.broadcast.emit("receiveOverlayTrigger", content); + socket.broadcast.emit('receiveOverlayTrigger', content); } function processUpdateOverlayPosition(socket: Socket, content: UpdateMobileInkOverlayPositionContent) { - socket.broadcast.emit("receiveUpdateOverlayPosition", content); + socket.broadcast.emit('receiveUpdateOverlayPosition', content); } function processMobileDocumentUpload(socket: Socket, content: MobileDocumentUploadContent) { - socket.broadcast.emit("receiveMobileDocumentUpload", content); + socket.broadcast.emit('receiveMobileDocumentUpload', content); } function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) { @@ -165,27 +165,22 @@ export namespace WebSocket { const target: string[] = []; onlyFields && target.push(DocumentsCollection); await Database.Instance.dropSchema(...target); - if (process.env.DISABLE_SEARCH !== "true") { + if (process.env.DISABLE_SEARCH !== 'true') { await Search.clear(); } + initializeGuest(); } function barReceived(socket: SocketIO.Socket, userEmail: string) { clients[userEmail] = new Client(userEmail.toString()); const currentdate = new Date(); - const datetime = currentdate.getDate() + "/" - + (currentdate.getMonth() + 1) + "/" - + currentdate.getFullYear() + " @ " - + currentdate.getHours() + ":" - + currentdate.getMinutes() + ":" - + currentdate.getSeconds(); + const datetime = currentdate.getDate() + '/' + (currentdate.getMonth() + 1) + '/' + currentdate.getFullYear() + ' @ ' + currentdate.getHours() + ':' + currentdate.getMinutes() + ':' + currentdate.getSeconds(); console.log(blue(`user ${userEmail} has connected to the web socket at: ${datetime}`)); - socketMap.set(socket, userEmail + " at " + datetime); + socketMap.set(socket, userEmail + ' at ' + datetime); } function getField([id, callback]: [string, (result?: Transferable) => void]) { - Database.Instance.getDocument(id, (result?: Transferable) => - callback(result ? result : undefined)); + Database.Instance.getDocument(id, (result?: Transferable) => callback(result ? result : undefined)); } function getFields([ids, callback]: [string[], (result: Transferable[]) => void]) { @@ -193,9 +188,9 @@ export namespace WebSocket { } function setField(socket: Socket, newValue: Transferable) { - Database.Instance.update(newValue.id, newValue, () => - socket.broadcast.emit(MessageStore.SetField.Message, newValue)); // broadcast set value to all other clients - if (newValue.type === Types.Text) { // if the newValue has sring type, then it's suitable for searching -- pass it to SOLR + Database.Instance.update(newValue.id, newValue, () => socket.broadcast.emit(MessageStore.SetField.Message, newValue)); // broadcast set value to all other clients + if (newValue.type === Types.Text) { + // if the newValue has sring type, then it's suitable for searching -- pass it to SOLR Search.updateDocument({ id: newValue.id, data: { set: (newValue as any).data } }); } } @@ -213,34 +208,36 @@ export namespace WebSocket { Database.Instance.getDocuments(ids, callback); } - const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { - "number": "_n", - "string": "_t", - "boolean": "_b", - "image": ["_t", "url"], - "video": ["_t", "url"], - "pdf": ["_t", "url"], - "audio": ["_t", "url"], - "web": ["_t", "url"], - "map": ["_t", "url"], - "script": ["_t", value => value.script.originalScript], - "RichTextField": ["_t", value => value.Text], - "date": ["_d", value => new Date(value.date).toISOString()], - "proxy": ["_i", "fieldId"], - "list": ["_l", list => { - const results = []; - for (const value of list.fields) { - const term = ToSearchTerm(value); - if (term) { - results.push(term.value); + const suffixMap: { [type: string]: string | [string, string | ((json: any) => any)] } = { + number: '_n', + string: '_t', + boolean: '_b', + image: ['_t', 'url'], + video: ['_t', 'url'], + pdf: ['_t', 'url'], + audio: ['_t', 'url'], + web: ['_t', 'url'], + map: ['_t', 'url'], + script: ['_t', value => value.script.originalScript], + RichTextField: ['_t', value => value.Text], + date: ['_d', value => new Date(value.date).toISOString()], + proxy: ['_i', 'fieldId'], + list: [ + '_l', + list => { + const results = []; + for (const value of list.fields) { + const term = ToSearchTerm(value); + if (term) { + results.push(term.value); + } } - } - return results.length ? results : null; - }] + return results.length ? results : null; + }, + ], }; - function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { - + function ToSearchTerm(val: any): { suffix: string; value: any } | undefined { if (val === null || val === undefined) { return; } @@ -252,69 +249,79 @@ export namespace WebSocket { } if (Array.isArray(suffix)) { const accessor = suffix[1]; - if (typeof accessor === "function") { + if (typeof accessor === 'function') { val = accessor(val); } else { val = val[accessor]; - } suffix = suffix[0]; - } return { suffix, value: val }; } function getSuffix(value: string | [string, any]): string { - return typeof value === "string" ? value : value[0]; + return typeof value === 'string' ? value : value[0]; } function addToListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { - diff.diff.$set = diff.diff.$addToSet; delete diff.diff.$addToSet;// convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones + diff.diff.$set = diff.diff.$addToSet; + delete diff.diff.$addToSet; // convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const newListItems = diff.diff.$set[updatefield]?.fields; if (!newListItems) { - console.log("Error: addToListField - no new list items"); + console.log('Error: addToListField - no new list items'); return; } - const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((item: any) => item !== undefined) || []; - diff.diff.$set[updatefield].fields = [...curList, ...newListItems];//, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; + const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((item: any) => item !== undefined) || []; + diff.diff.$set[updatefield].fields = [...curList, ...newListItems]; //, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; - Database.Instance.update(diff.id, diff.diff, + Database.Instance.update( + diff.id, + diff.diff, () => { if (sendBack) { - console.log("Warning: list modified during update. Composite list is being returned."); + console.log('Warning: list modified during update. Composite list is being returned.'); const id = socket.id; - socket.id = ""; + socket.id = ''; socket.broadcast.emit(MessageStore.UpdateField.Message, diff); socket.id = id; } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); dispatchNextOp(diff.id); - }, false); + }, + false + ); } function remFromListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { - diff.diff.$set = diff.diff.$remFromSet; delete diff.diff.$remFromSet; + diff.diff.$set = diff.diff.$remFromSet; + delete diff.diff.$remFromSet; const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const remListItems = diff.diff.$set[updatefield].fields; - const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((f: any) => f !== null) || []; - diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem)); + const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((f: any) => f !== null) || []; + diff.diff.$set[updatefield].fields = curList?.filter( + (curItem: any) => !remListItems.some((remItem: any) => (remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem)) + ); const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; - Database.Instance.update(diff.id, diff.diff, + Database.Instance.update( + diff.id, + diff.diff, () => { if (sendBack) { - console.log("SEND BACK"); + console.log('SEND BACK'); const id = socket.id; - socket.id = ""; + socket.id = ''; socket.broadcast.emit(MessageStore.UpdateField.Message, diff); socket.id = id; } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); dispatchNextOp(diff.id); - }, false); + }, + false + ); } - const pendingOps = new Map<string, { diff: Diff, socket: Socket }[]>(); + const pendingOps = new Map<string, { diff: Diff; socket: Socket }[]>(); function dispatchNextOp(id: string) { const next = pendingOps.get(id)!.shift(); @@ -341,7 +348,7 @@ export namespace WebSocket { function UpdateField(socket: Socket, diff: Diff) { if (CurUser !== socketMap.get(socket)) { CurUser = socketMap.get(socket); - console.log("Switch User: " + CurUser); + console.log('Switch User: ' + CurUser); } if (pendingOps.has(diff.id)) { pendingOps.get(diff.id)!.push({ diff, socket }); @@ -357,24 +364,25 @@ export namespace WebSocket { return GetRefFieldLocal([diff.id, (result?: Transferable) => SetField(socket, diff, result)]); } function SetField(socket: Socket, diff: Diff, curListItems?: Transferable) { - Database.Instance.update(diff.id, diff.diff, - () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false); + Database.Instance.update(diff.id, diff.diff, () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false); const docfield = diff.diff.$set || diff.diff.$unset; if (docfield) { const update: any = { id: diff.id }; let dynfield = false; for (let key in docfield) { - if (!key.startsWith("fields.")) continue; + if (!key.startsWith('fields.')) continue; dynfield = true; const val = docfield[key]; key = key.substring(7); - Object.values(suffixMap).forEach(suf => { update[key + getSuffix(suf)] = { set: null }; }); + Object.values(suffixMap).forEach(suf => { + update[key + getSuffix(suf)] = { set: null }; + }); const term = ToSearchTerm(val); if (term !== undefined) { const { suffix, value } = term; update[key + suffix] = { set: value }; if (key.endsWith('lastModified')) { - update["lastModified" + suffix] = value; + update['lastModified' + suffix] = value; } } } @@ -403,6 +411,4 @@ export namespace WebSocket { function CreateField(newValue: any) { Database.Instance.insert(newValue); } - } - diff --git a/views/login.pug b/views/login.pug index 98816e9c8..71c6933be 100644 --- a/views/login.pug +++ b/views/login.pug @@ -15,10 +15,10 @@ block content h3.auth_header Log In .form-group .col-sm-7 - input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus, required) + input.form-control(type='email', name='email', id='email', placeholder='Email (or blank for "guest")', autofocus) .form-group .col-sm-7 - input.form-control(type='password', name='password', id='password', placeholder='Password', required) + input.form-control(type='password', name='password', id='password', placeholder='Password') .form-group .col-sm-offset-3.col-sm-7 button.btn.btn-success(id='submit', type='submit') diff --git a/webpack.config.js b/webpack.config.js index 5a954db19..01625988c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,38 +1,38 @@ -var path = require('path'); -var webpack = require('webpack'); -const CopyWebpackPlugin = require("copy-webpack-plugin"); +const path = require('path'); +const webpack = require('webpack'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const plugins = [ - new CopyWebpackPlugin([{ - from: "deploy", - to: path.join(__dirname, "build") - }]), + new CopyWebpackPlugin([ + { + from: 'deploy', + to: path.join(__dirname, 'build'), + }, + ]), new HtmlWebpackPlugin({ title: 'Caching', }), new ForkTsCheckerWebpackPlugin({ tslint: true, // memoryLimit: 4096, - useTypescriptIncrementalApi: true + useTypescriptIncrementalApi: true, }), - new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], }), - new webpack.ProvidePlugin({ process: 'process/browser', }), + new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'] }), + new webpack.ProvidePlugin({ process: 'process/browser' }), new webpack.HotModuleReplacementPlugin(), ]; function transferEnvironmentVariables() { - const prefix = "_CLIENT_"; - const { - parsed - } = require('dotenv').config(); + const prefix = '_CLIENT_'; + const { parsed } = require('dotenv').config(); if (!parsed) { return; } const resolvedClientSide = Object.keys(parsed).reduce((mapping, envKey) => { if (envKey.startsWith(prefix)) { - mapping[`process.env.${envKey.replace(prefix, "")}`] = JSON.stringify(parsed[envKey]); + mapping[`process.env.${envKey.replace(prefix, '')}`] = JSON.stringify(parsed[envKey]); } return mapping; }, {}); @@ -44,18 +44,18 @@ transferEnvironmentVariables(); module.exports = { mode: 'development', entry: { - bundle: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'], - viewer: ["./src/debug/Viewer.tsx", 'webpack-hot-middleware/client?reload=true'], - repl: ["./src/debug/Repl.tsx", 'webpack-hot-middleware/client?reload=true'], - test: ["./src/debug/Test.tsx", 'webpack-hot-middleware/client?reload=true'], - inkControls: ["./src/mobile/InkControls.tsx", 'webpack-hot-middleware/client?reload=true'], - mobileInterface: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'], + bundle: ['./src/client/views/Main.tsx', 'webpack-hot-middleware/client?reload=true'], + viewer: ['./src/debug/Viewer.tsx', 'webpack-hot-middleware/client?reload=true'], + repl: ['./src/debug/Repl.tsx', 'webpack-hot-middleware/client?reload=true'], + test: ['./src/debug/Test.tsx', 'webpack-hot-middleware/client?reload=true'], + inkControls: ['./src/mobile/InkControls.tsx', 'webpack-hot-middleware/client?reload=true'], + mobileInterface: ['./src/client/views/Main.tsx', 'webpack-hot-middleware/client?reload=true'], }, - devtool: "source-map", + devtool: 'source-map', output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - publicPath: "/", + filename: '[name].js', + path: path.resolve(__dirname, 'build'), + publicPath: '/', }, resolve: { extensions: ['.js', '.ts', '.tsx'], @@ -68,67 +68,73 @@ module.exports = { crypto: false, assert: false, os: false, - path: require.resolve("path-browserify"), - http: require.resolve("http-browserify"), - https: require.resolve("https-browserify"), - stream: require.resolve("stream-browserify"), - buffer: require.resolve("buffer") - } + path: require.resolve('path-browserify'), + http: require.resolve('http-browserify'), + https: require.resolve('https-browserify'), + stream: require.resolve('stream-browserify'), + buffer: require.resolve('buffer'), + }, }, module: { - rules: [{ - test: [/\.tsx?$/], - use: [{ - loader: 'ts-loader', - options: { - transpileOnly: true - } - }] - }, - { - test: /\.m?js/, - resolve: { - fullySpecified: false - } - }, - { - test: /\.(woff|woff2|ttf|eot|otf|svg)$/, - use: 'file-loader?name=fonts/[name].[ext]!static' - }, - { - test: /\.scss|css$/, - use: [{ - loader: "style-loader" + rules: [ + { + test: [/\.tsx?$/], + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + }, + ], }, { - loader: "css-loader" + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, }, { - loader: "sass-loader" + test: /\.(woff|woff2|ttf|eot|otf|svg)$/, + use: 'file-loader?name=fonts/[name].[ext]!static', + }, + { + test: /\.scss|css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + }, + { + loader: 'sass-loader', + }, + ], + }, + { + test: /\.(jpg|png|pdf)$/, + use: [ + { + loader: 'file-loader', + }, + ], + }, + { + test: /\.(png|jpg|gif)$/i, + use: [ + { + loader: 'url-loader', + options: { + limit: 8192, + }, + }, + ], }, - ] - }, - { - test: /\.(jpg|png|pdf)$/, - use: [{ - loader: 'file-loader' - }] - }, - { - test: /\.(png|jpg|gif)$/i, - use: [{ - loader: 'url-loader', - options: { - limit: 8192 - } - }] - } ], noParse: [require.resolve('typescript/lib/typescript.js')], }, plugins, - externals: [ - 'child_process' - ], -};
\ No newline at end of file + externals: ['child_process'], +}; |