diff options
38 files changed, 866 insertions, 429 deletions
diff --git a/package-lock.json b/package-lock.json index 2e70478d3..adf71ee58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -412,6 +412,15 @@ "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", "dev": true }, + "@types/chrome": { + "version": "0.0.114", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.114.tgz", + "integrity": "sha512-i7qRr74IrxHtbnrZSKUuP5Uvd5EOKwlwJq/yp7+yTPihOXnPhNQO4Z5bqb1XTnrjdbUKEJicaVVbhcgtRijmLA==", + "requires": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "@types/color": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.1.tgz", @@ -547,6 +556,19 @@ "express-validator": "*" } }, + "@types/filesystem": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.29.tgz", + "integrity": "sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==", + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.28.tgz", + "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=" + }, "@types/formidable": { "version": "1.0.31", "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-1.0.31.tgz", @@ -584,6 +606,11 @@ "integrity": "sha512-L8O9HAVFZj0TuiS8h5ORthiMsrrhjxTC8XUusp5k47oXCst4VTm+qWKvrAvmYMybZVokbp4Udco1mNwJrTNZPQ==", "dev": true }, + "@types/har-format": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.4.tgz", + "integrity": "sha512-iUxzm1meBm3stxUMzRqgOVHjj4Kgpgu5w9fm4X7kPRfSgVRzythsucEN7/jtOo8SQzm+HfcxWWzJS0mJDH/3DQ==" + }, "@types/jquery": { "version": "3.3.36", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.36.tgz", @@ -2809,8 +2836,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -2828,13 +2854,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2847,18 +2871,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -2961,8 +2982,7 @@ }, "inherits": { "version": "2.0.4", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -2972,7 +2992,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2985,20 +3004,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.5", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.9.0", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3015,7 +3031,6 @@ "mkdirp": { "version": "0.5.3", "bundled": true, - "optional": true, "requires": { "minimist": "^1.2.5" } @@ -3071,8 +3086,7 @@ }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "npm-packlist": { "version": "1.4.8", @@ -3097,8 +3111,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -3108,7 +3121,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3177,8 +3189,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -3208,7 +3219,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3226,7 +3236,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3265,13 +3274,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.1.1", - "bundled": true, - "optional": true + "bundled": true } } } @@ -3282,6 +3289,15 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "chrome": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chrome/-/chrome-0.1.0.tgz", + "integrity": "sha1-9h2beS/v6MGUxwVt3BAscmqGQyk=", + "requires": { + "exeq": "^2.2.0", + "plist": "^1.1.0" + } + }, "chrome-trace-event": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", @@ -5319,6 +5335,15 @@ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" }, + "exeq": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/exeq/-/exeq-2.4.0.tgz", + "integrity": "sha1-Td8qaEZIxCeteZNJzzO9dTWPiEo=", + "requires": { + "bluebird": "^3.0.3", + "native-or-bluebird": "^1.2.0" + } + }, "exif": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/exif/-/exif-0.6.0.tgz", @@ -8729,6 +8754,11 @@ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, + "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=" + }, "needle": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz", @@ -9470,7 +9500,7 @@ }, "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": { @@ -9776,7 +9806,7 @@ }, "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": { @@ -10275,7 +10305,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", @@ -10363,7 +10393,7 @@ }, "hosted-git-info": { "version": "2.8.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, "http-cache-semantics": { @@ -10499,7 +10529,7 @@ }, "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" @@ -10575,7 +10605,7 @@ }, "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": { @@ -11084,7 +11114,7 @@ }, "mkdirp": { "version": "0.5.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", "requires": { "minimist": "^1.2.5" @@ -11092,7 +11122,7 @@ "dependencies": { "minimist": { "version": "1.2.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } @@ -11144,7 +11174,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", @@ -11258,7 +11288,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", @@ -11278,7 +11308,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", @@ -11288,7 +11318,7 @@ }, "npm-registry-fetch": { "version": "4.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.3.tgz", "integrity": "sha512-WGvUx0lkKFhu9MbiGFuT9nG2NpfQ+4dCJwRwwtK2HK5izJEvwDxMeUyqbuMS7N/OkpVCqDorV6rO5E4V9F8lJw==", "requires": { "JSONStream": "^1.3.4", @@ -11723,7 +11753,7 @@ }, "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", @@ -11734,7 +11764,7 @@ "dependencies": { "minimist": { "version": "1.2.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } @@ -11793,7 +11823,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", @@ -11814,7 +11844,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", @@ -11878,7 +11908,7 @@ }, "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" @@ -12177,7 +12207,7 @@ }, "string_decoder": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { "safe-buffer": "~5.2.0" @@ -12185,7 +12215,7 @@ "dependencies": { "safe-buffer": { "version": "5.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" } } @@ -12497,7 +12527,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" @@ -13294,6 +13324,24 @@ "semver-compare": "^1.0.0" } }, + "plist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz", + "integrity": "sha1-CEtQk93JJQbiWfh0uNmxr7jHlZM=", + "requires": { + "base64-js": "0.0.8", + "util-deprecate": "1.0.2", + "xmlbuilder": "4.0.0", + "xmldom": "0.1.x" + }, + "dependencies": { + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" + } + } + }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -17880,12 +17928,32 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xmlbuilder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz", + "integrity": "sha1-mLj2UcowqmJANvEn0RzGbce5B6M=", + "requires": { + "lodash": "^3.5.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==" + }, "xmlhttprequest-ssl": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", diff --git a/package.json b/package.json index f314423ff..74b7bf9d1 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "@hig/flyout": "^1.2.0", "@hig/theme-context": "^2.1.3", "@hig/theme-data": "^2.13.0", + "@types/chrome": "0.0.114", "adm-zip": "^0.4.13", "archiver": "^3.1.1", "array-batcher": "^1.2.3", @@ -129,6 +130,7 @@ "bootstrap": "^4.4.1", "canvas": "^2.5.0", "child_process": "^1.0.2", + "chrome": "^0.1.0", "class-transformer": "^0.2.0", "color": "^3.1.2", "colors": "^1.4.0", diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 517ccdce9..b816d1617 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -45,7 +45,11 @@ export enum Confidence { export namespace CognitiveServices { const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => { - const apiKey = process.env[service.toUpperCase()]; + let apiKey = process.env[service.toUpperCase()]; + // A HACK FOR A DEMO VIDEO - syip2 + if (service === "handwriting") { + apiKey = "61088486d76c4b12ba578775a5f55422"; + } if (!apiKey) { console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`); return undefined; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7f5b62f22..f7e19eecd 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -432,17 +432,28 @@ export namespace Docs { parentProto.data = new List<Doc>(); } if (device) { - const { __images } = device; + const { title, __images, additionalMedia } = device; delete device.__images; + delete device.additionalMedia; const { ImageDocument, StackingDocument } = Docs.Create; const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight })); - const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => ImageDocument(url, { - title: `image${i}.${extname(url)}`, - _nativeWidth: nativeWidth, - _nativeHeight: nativeHeight - })); + const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => { + const imageDoc = ImageDocument(url, { + title: `image${i}.${extname(url)}`, + _nativeWidth: nativeWidth, + _nativeHeight: nativeHeight + }); + const media = additionalMedia[i]; + if (media) { + for (const key of Object.keys(media)) { + imageDoc[`additionalMedia_${key}`] = Utils.prepend(`/files/${key}/buxton/${media[key]}`); + } + } + return imageDoc; + }); // the main document we create - const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true, hero: new ImageField(constructed[0].url) }); + const doc = StackingDocument(deviceImages, { title, _LODdisable: true, hero: new ImageField(constructed[0].url) }); + doc.nameAliases = new List<string>([title.toLowerCase()]); // add the parsed attributes to this main document Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } }); Doc.AddDocToList(parentProto, "data", doc); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index ab087335e..67f2f244c 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -126,13 +126,13 @@ export class DocumentManager { finished?.(); } public jumpToDocument = async ( - targetDoc: Doc, - willZoom: boolean, - createViewFunc = DocumentManager.addRightSplit, - docContext?: Doc, - linkId?: string, - closeContextIfNotFound: boolean = false, - originatingDoc: Opt<Doc> = undefined, + targetDoc: Doc, // document to display + willZoom: boolean, // whether to zoom doc to take up most of screen + createViewFunc = DocumentManager.addRightSplit, // how to create a view of the doc if it doesn't exist + docContext?: Doc, // context to load that should contain the target + linkId?: string, // link that's being followed + closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there + originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc finished?: () => void ): Promise<void> => { const getFirstDocView = DocumentManager.Instance.getFirstDocumentView; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 358de2333..a1d1b0ece 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -51,6 +51,8 @@ import { PreviewCursor } from './PreviewCursor'; import { ScriptField } from '../../fields/ScriptField'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { SnappingManager } from '../util/SnappingManager'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { DocumentManager } from '../util/DocumentManager'; @observer export class MainView extends React.Component { @@ -83,6 +85,14 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("paste", KeyManager.Instance.paste as any); + document.addEventListener("dash", (e: any) => { // event used by chrome plugin to tell Dash which document to focus on + const id = FormattedTextBox.GetDocFromUrl(e.detail); + DocServer.GetRefField(id).then(doc => { + if (doc instanceof Doc) { + DocumentManager.Instance.jumpToDocument(doc, false, undefined); + } + }); + }); } componentWillUnMount() { @@ -534,7 +544,7 @@ export class MainView extends React.Component { } @computed get snapLines() { - return <div className="mainView-snapLines"> + return !Doc.UserDoc().showSnapLines ? (null) : <div className="mainView-snapLines"> <svg style={{ width: "100%", height: "100%" }}> {SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} {SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index 92b0f05b3..b562bd957 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -12,11 +12,9 @@ import { Transform } from "../../util/Transform"; import { TimelineMenu } from "./TimelineMenu"; import { Docs } from "../../documents/Documents"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; import { emptyPath } from "../../../Utils"; - /** * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also */ @@ -100,16 +98,11 @@ export namespace KeyframeFunc { export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => { const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement; switch (unit) { - case "mili": - return time; - case "sec": - return dir === "pixel" ? time / 1000 : time * 1000; - case "min": - return dir === "pixel" ? time / 60000 : time * 60000; - case "hr": - return dir === "pixel" ? time / 3600000 : time * 3600000; - default: - return time; + case "mili": return time; + case "sec": return dir === "pixel" ? time / 1000 : time * 1000; + case "min": return dir === "pixel" ? time / 60000 : time * 60000; + case "hr": return dir === "pixel" ? time / 3600000 : time * 3600000; + default: return time; } }; } @@ -187,10 +180,10 @@ export class Keyframe extends React.Component<IProps> { const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade); const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade); const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end); - (fadeIn.key as Doc).opacity = 1; - (fadeOut.key as Doc).opacity = 1; - (start.key as Doc).opacity = 0.1; - (finish.key as Doc).opacity = 0.1; + (fadeIn as Doc).opacity = 1; + (fadeOut as Doc).opacity = 1; + (start as Doc).opacity = 0.1; + (finish as Doc).opacity = 0.1; this.forceUpdate(); //not needed, if setTimeout is gone... }, 1000); } @@ -333,31 +326,28 @@ export class Keyframe extends React.Component<IProps> { */ @action makeKeyframeMenu = (kf: Doc, e: MouseEvent) => { - TimelineMenu.Instance.addItem("button", "Show Data", () => { - runInAction(() => { + TimelineMenu.Instance.addItem("button", "Toggle Fade Only", () => { + kf.type = kf.type === KeyframeFunc.KeyframeType.fade ? KeyframeFunc.KeyframeType.default : KeyframeFunc.KeyframeType.fade; + }), + TimelineMenu.Instance.addItem("button", "Show Data", action(() => { const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 }); CollectionDockingView.AddRightSplit(kvp, emptyPath); - }); - }), - TimelineMenu.Instance.addItem("button", "Delete", () => { - runInAction(() => { - (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); - this.forceUpdate(); - }); - }), - TimelineMenu.Instance.addItem("input", "Move", (val) => { - runInAction(() => { - let cannotMove: boolean = false; - const kfIndex: number = this.keyframes.indexOf(kf); - if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) { - cannotMove = true; - } - if (!cannotMove) { - this.keyframes[kfIndex].time = parseInt(val, 10); - this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; - } - }); - }); + })), + TimelineMenu.Instance.addItem("button", "Delete", action(() => { + (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); + this.forceUpdate(); + })), + TimelineMenu.Instance.addItem("input", "Move", action((val) => { + let cannotMove: boolean = false; + const kfIndex: number = this.keyframes.indexOf(kf); + if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) { + cannotMove = true; + } + if (!cannotMove) { + this.keyframes[kfIndex].time = parseInt(val, 10); + this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + } + })); TimelineMenu.Instance.addMenu("Keyframe"); TimelineMenu.Instance.openMenu(e.clientX, e.clientY); } diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 020a87b2e..fc48e0327 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -23,7 +23,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { @observable _childClickedScript: Opt<ScriptField>; componentDidMount() { if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") { - this.Document._layoutEngine = "pass"; + this.Document._pileLayoutEngine = "pass"; } this._originalChrome = StrCast(this.layoutDoc._chromeStatus); this.layoutDoc._chromeStatus = "disabled"; @@ -34,7 +34,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { this.layoutDoc._chromeStatus = this._originalChrome; } - layoutEngine = () => StrCast(this.Document._layoutEngine); + layoutEngine = () => StrCast(this.Document._pileLayoutEngine); @computed get contents() { return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} > @@ -53,7 +53,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { Doc.pileup(this.childDocs); this.layoutDoc._panX = 0; this.layoutDoc._panY = -10; - this.props.Document._layoutEngine = 'pass'; + this.props.Document._pileLayoutEngine = 'pass'; } else { const defaultSize = 25; this.layoutDoc._overflow = 'visible'; @@ -67,7 +67,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } this.layoutDoc._panX = this.layoutDoc._panY = 0; this.layoutDoc._width = this.layoutDoc._height = defaultSize; - this.props.Document._layoutEngine = 'starburst'; + this.props.Document._pileLayoutEngine = 'starburst'; } }); diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 5eaf29316..203c51163 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -7,11 +7,13 @@ .collectionStackingView { display: flex; } + .collectionStackingMasonry-cont { - position:relative; - height:100%; - width:100%; + position: relative; + height: 100%; + width: 100%; } + .collectionStackingView, .collectionMasonryView { height: 100%; @@ -22,12 +24,17 @@ overflow-x: hidden; flex-wrap: wrap; transition: top .5s; + >div { position: relative; display: block; } + .collectionStackingViewFieldColumn { - height:max-content; + height: max-content; + } + .collectionStackingViewFieldColumnDragging { + height:100%; } .collectionSchemaView-previewDoc { @@ -129,27 +136,34 @@ background: red; } } + .collectionStackingView-miniHeader { width: 100%; + .editableView-container-editing-oneLine { min-height: 20px; display: flex; align-items: center; flex-direction: row; } - span::before , span::after{ + + span::before, + span::after { content: ""; width: 50%; border-top: dashed gray 1px; position: relative; display: inline-block; } + span::before { margin-right: 10px; } - span::after{ + + span::after { margin-left: 10px; } + span { position: relative; text-align: center; @@ -157,10 +171,11 @@ overflow: visible; width: 100%; display: flex; - color:gray; + color: gray; align-items: center; } } + .collectionStackingView-sectionHeader { text-align: center; margin: auto; @@ -277,7 +292,7 @@ height: 20px; border-radius: 10px; margin: 3px; - width:max-content; + width: max-content; &.active { color: red; @@ -294,15 +309,18 @@ display: none; } } + .collectionStackingView-sectionHeader:hover { .collectionStackingView-sectionColor { - display:unset; + display: unset; } + .collectionStackingView-sectionOptions { - display:unset; + display: unset; } + .collectionStackingView-sectionDelete { - display:unset; + display: unset; } } @@ -403,4 +421,4 @@ .rc-switch-checked .rc-switch-inner { left: 8px; } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6949670d6..7fd19a23c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -193,7 +193,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { const height = () => this.getDocHeight(doc); - const opacity = () => this.Document.currentTimecode === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, this.Document.currentTimecode || 0)?.opacity; + const opacity = () => this.Document.currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document.currentFrame))?.opacity; return <ContentFittingDocumentView Document={doc} DataDoc={dataDoc || (doc[DataSym] !== doc && doc[DataSym])} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 53435ccc9..a269b21f5 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -352,7 +352,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; const chromeStatus = this.props.parent.props.Document._chromeStatus; return ( - <div className="collectionStackingViewFieldColumn" key={heading} + <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 3e99af724..b2e1c0f73 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -717,7 +717,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } ContextMenu.Instance.addItem({ description: "Buxton Layout", icon: "eye", event: () => { - const { ImageDocument } = Docs.Create; + const { ImageDocument, PdfDocument } = Docs.Create; const { Document } = this.props; const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif"; const detailView = Cast(Cast(Doc.UserDoc()["template-button-detail"], Doc, null)?.dragFactory, Doc, null); @@ -726,13 +726,14 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll heroView._showTitle = "title"; heroView._showTitleHover = "titlehover"; - const doubleClickView = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target + const fallback = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target + let pdfContent: string; DocListCast(this.dataDoc[this.props.fieldKey]).map(d => { DocListCast(d.data).map((img, i) => { const caption = (d.captions as any)[i]; if (caption) { Doc.GetProto(img).caption = caption; - Doc.GetProto(img).doubleClickView = doubleClickView; + Doc.GetProto(img).doubleClickView = (pdfContent = StrCast(img.additionalMedia_pdfs)) ? PdfDocument(pdfContent, { title: pdfContent }) : fallback; } }); Doc.GetProto(d).type = "buxton"; @@ -748,7 +749,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll Document.childLayoutTemplate = heroView; Document.childClickedOpenTemplateView = new PrefetchProxy(detailView); Document._viewType = CollectionViewType.Time; - Document._forceActive = true; + Document.forceActive = true; Document._pivotField = "company"; Document.childDropAction = "alias"; } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index d1fbb445d..bcb190de4 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../fields/DateField'; -import { DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, Field, Opt, AclSym, AclAddonly, AclReadonly } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; @@ -92,7 +92,7 @@ export interface CollectionRenderProps { export class CollectionView extends Touchable<FieldViewProps & CollectionViewCustomProps> { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } - private _isChildActive = false; //TODO should this be observable? + _isChildActive = false; //TODO should this be observable? get _isLightboxOpen() { return BoolCast(this.props.Document.isLightboxOpen); } set _isLightboxOpen(value) { this.props.Document.isLightboxOpen = value; } @observable private _curLightboxImg = 0; @@ -128,10 +128,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const docList = DocListCast(targetDataDoc[this.props.fieldKey]); const added = docs.filter(d => !docList.includes(d)); if (added.length) { - added.map(doc => doc.context = this.props.Document); - added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); - targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); - targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + if (this.dataDoc[AclSym] === AclReadonly) { + return false; + } else if (this.dataDoc[AclSym] === AclAddonly) { + added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); + } else { + added.map(doc => doc.context = this.props.Document); + added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); + targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); + targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + } } return true; } diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 9795a3a22..42ec35853 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -8,10 +8,12 @@ z-index: 9001; transition: top .5s; background: lightgrey; + transform-origin: top left; .collectionViewChrome { display: flex; - padding-bottom: 10px; + padding-bottom: 1px; + height: 32px; border-bottom: .5px solid rgb(180, 180, 180); overflow: hidden; @@ -21,12 +23,12 @@ .collectionViewBaseChrome-viewPicker { font-size: 75%; //text-transform: uppercase; - letter-spacing: 2px; + //letter-spacing: 2px; background: rgb(238, 238, 238); color: grey; outline-color: black; border: none; - padding: 12px 10px 11px 10px; + //padding: 12px 10px 11px 10px; } .collectionViewBaseChrome-viewPicker:active { @@ -48,6 +50,7 @@ .collectionViewBaseChrome-cmdPicker { margin-left: 3px; margin-right: 0px; + font-size: 75%; background: rgb(238, 238, 238); border: none; color: grey; @@ -58,6 +61,7 @@ background-color: gray; display: flex; flex-direction: row; + height: 30px; .commandEntry-drop { color: white; @@ -100,8 +104,8 @@ margin: auto; background: gray; color: white; - width: 40px; - height: 40px; + width: 30px; + height: 30px; align-items: center; justify-content: center; } @@ -164,13 +168,31 @@ } } - .collectionStackingViewChrome-cont, .collectionTreeViewChrome-cont { display: flex; justify-content: space-between; } + .collectionGridViewChrome-cont { + display: flex; + margin-left: 10; + + .grid-control { + align-self: center; + width: 30%; + + .grid-icon { + margin-right: 5px; + } + } + + .collectionGridViewChrome-entryBox { + width: 50%; + } + } + + .collectionStackingViewChrome-sort, .collectionTreeViewChrome-sort { display: flex; @@ -200,8 +222,7 @@ .collectionTreeViewChrome-pivotField-label { vertical-align: center; padding-left: 10px; - padding-top: 10px; - padding-bottom: 10px; + margin: auto; } .collectionStackingViewChrome-pivotField, @@ -209,14 +230,15 @@ color: white; width: 100%; min-width: 100px; - text-align: center; + display: flex; + align-items: center; background: rgb(238, 238, 238); .editable-view-input, input, .editableView-container-editing-oneLine, .editableView-container-editing { - padding: 12px 10px 11px 10px; + margin: auto; border: 0px; color: grey; text-align: center; @@ -238,8 +260,51 @@ cursor: text; } - .collectionGridViewChrome-entryBox { - width: 50%; + } +} + +.collectionFreeFormViewChrome-cont { + width: 60px; + display: flex; + position: relative; + align-items: center; + + .fwdKeyframe, + .numKeyframe, + .backKeyframe { + cursor: pointer; + position: absolute; + width: 20; + height: 30; + bottom: 0; + background: gray; + display: flex; + align-items: center; + color: white; + } + + .backKeyframe { + left: 0; + + svg { + display: block; + margin: auto; + } + } + + .numKeyframe { + left: 20; + display: flex; + flex-direction: column; + padding: 5px; + } + + .fwdKeyframe { + left: 40; + + svg { + display: block; + margin: auto; } } } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index def20ae9b..9b8a24d4a 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -15,8 +15,7 @@ import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; import { CollectionViewType } from "./CollectionView"; import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; -import { CollectionGridView } from "./collectionGrid/CollectionGridView"; -import HeightLabel from "./collectionMulticolumn/MultirowHeightLabel"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { @@ -46,7 +45,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro initialize: emptyFunction, }; _narrativeCommand = { - params: ["target", "source"], title: "=> click clicked open view", + params: ["target", "source"], title: "=> child click view", script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])", immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]), initialize: emptyFunction, @@ -85,22 +84,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro private _viewRef = React.createRef<HTMLInputElement>(); @observable private _currentKey: string = ""; - componentDidMount = () => { - runInAction(() => { - // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - const chromeStatus = this.props.CollectionView.props.Document._chromeStatus; - if (chromeStatus) { - if (chromeStatus === "disabled") { - throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); - } - else if (chromeStatus === "collapsed") { - if (this.props.collapse) { - this.props.collapse(true); - } - } - } - }); - } + componentDidMount = action(() => { + // chrome status is one of disabled, collapsed, or visible. this determines initial state from document + switch (this.props.CollectionView.props.Document._chromeStatus) { + case "disabled": + throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); + case "collapsed": + this.props.collapse?.(true); + break; + } + }) @undoBatch viewChanged = (e: React.ChangeEvent) => { @@ -199,10 +192,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } } - subChrome = () => { + @computed get subChrome() { const collapsed = this.document._chromeStatus !== "enabled"; if (collapsed) return null; switch (this.props.type) { + case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); @@ -239,7 +233,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro const vtype = this.props.CollectionView.collectionViewType; const c = { params: ["target"], title: vtype, - script: `this.target._viewType = ${StrCast(this.props.CollectionView.props.Document._viewType)}`, + script: `this.target._viewType = '${StrCast(this.props.CollectionView.props.Document._viewType)}'`, immediate: (source: Doc[]) => this.props.CollectionView.props.Document._viewType = Doc.getDocTemplate(source?.[0]), initialize: emptyFunction, }; @@ -257,14 +251,61 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro }, emptyFunction, emptyFunction); } + @computed get templateChrome() { + const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; + return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}> + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> + <div className="commandEntry-drop"> + <FontAwesomeIcon icon="bullseye" size="2x" /> + </div> + <select + className="collectionViewBaseChrome-cmdPicker" + onPointerDown={stopPropagation} + onChange={this.commandChanged} + value={this._currentKey}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> + {this._buttonizableCommands.map(cmd => + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> + )} + </select> + </div> + </div>; + } + + @computed get viewModes() { + const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; + return <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}> + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> + <div className="commandEntry-drop"> + <FontAwesomeIcon icon="bullseye" size="2x" /> + </div> + <select + className="collectionViewBaseChrome-viewPicker" + onPointerDown={stopPropagation} + onChange={this.viewChanged} + value={StrCast(this.props.CollectionView.props.Document._viewType)}> + {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : ( + <option + key={Utils.GenerateGuid()} + className="collectionViewBaseChrome-viewOption" + onPointerDown={stopPropagation} + value={type}> + {type[0].toUpperCase() + type.substring(1)} + </option> + ))} + </select> + </div> + </div>; + } + render() { const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; + const scale = Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale); return ( <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined, - transform: collapsed ? "" : `scale(${Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)})`, - transformOrigin: "top left", - width: `${this.props.PanelWidth() / Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)}px` + transform: collapsed ? "" : `scale(${scale})`, + width: `${this.props.PanelWidth() / scale}px` }}> <div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}> <div className="collectionViewBaseChrome"> @@ -279,52 +320,15 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro title="Collapse collection chrome" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> </button> - <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}> - <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon> - </div> - <select - className="collectionViewBaseChrome-viewPicker" - onPointerDown={stopPropagation} - onChange={this.viewChanged} - value={StrCast(this.props.CollectionView.props.Document._viewType)}> - {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : ( - <option - key={Utils.GenerateGuid()} - className="collectionViewBaseChrome-viewOption" - onPointerDown={stopPropagation} - value={type}> - {type[0].toUpperCase() + type.substring(1)} - </option> - ))} - </select> - </div> - </div> + {this.viewModes} <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.toggleViewSpecs} > <FontAwesomeIcon icon="filter" size="2x" /> </div> </div> - <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}> - <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x" /> - </div> - <select - className="collectionViewBaseChrome-cmdPicker" - onPointerDown={stopPropagation} - onChange={this.commandChanged} - value={this._currentKey}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""}>{""}</option> - {this._buttonizableCommands.map(cmd => - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> - )} - </select> - </div> - </div> + {this.templateChrome} </div> - {this.subChrome()} + {this.subChrome} </div> </div> ); @@ -332,6 +336,56 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @observer +export class CollectionFreeFormViewChrome extends React.Component<CollectionViewChromeProps> { + + get Document() { return this.props.CollectionView.props.Document; } + @computed get dataField() { + return this.props.CollectionView.props.Document[Doc.LayoutFieldKey(this.props.CollectionView.props.Document)]; + } + @computed get childDocs() { + return DocListCast(this.dataField); + } + @undoBatch + @action + nextKeyframe = (): void => { + const currentFrame = NumCast(this.Document.currentFrame); + if (currentFrame === undefined) { + this.Document.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); + } + CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); + this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1); + this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame)); + } + @undoBatch + @action + prevKeyframe = (): void => { + const currentFrame = NumCast(this.Document.currentFrame); + if (currentFrame === undefined) { + this.Document.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); + } + CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); + this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1); + } + render() { + return this.Document.isAnnotationOverlay ? (null) : + <div className="collectionFreeFormViewChrome-cont"> + <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}> + <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> + </div> + <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} + onClick={action(() => this.Document.editing = !this.Document.editing)} > + {NumCast(this.Document.currentFrame)} + </div> + <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}> + <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> + </div> + </div>; + } +} + +@observer export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> { @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; @@ -374,6 +428,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView this.suggestions = []; } + @action setValue = (value: string) => { this.props.CollectionView.props.Document._pivotField = value; return true; @@ -476,19 +531,20 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh @observer export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> { - get dataExtension() { - return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "_ext"] as Doc; + get sortAscending() { + return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"]; + } + set sortAscending(value) { + this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"] = value; } - @computed private get descending() { - return this.dataExtension && Cast(this.dataExtension.sortAscending, "boolean", null); + @computed private get ascending() { + return Cast(this.sortAscending, "boolean", null); } @action toggleSort = () => { - if (this.dataExtension) { - if (this.dataExtension.sortAscending) this.dataExtension.sortAscending = undefined; - else if (this.dataExtension.sortAscending === undefined) this.dataExtension.sortAscending = false; - else this.dataExtension.sortAscending = true; - } + if (this.sortAscending) this.sortAscending = undefined; + else if (this.sortAscending === undefined) this.sortAscending = false; + else this.sortAscending = true; } render() { @@ -498,7 +554,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro <div className="collectionTreeViewChrome-sortLabel"> Sort </div> - <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending === undefined ? "90" : this.descending ? "180" : "0"}deg)` }}> + <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}> <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> </div> </button> @@ -516,10 +572,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewChro /** * Sets the value of `numCols` on the grid's Document to the value entered. */ - @action + @undoBatch onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter" || e.key === "Tab") { - if (this.props.CollectionView.props.Document.numCols as number !== e.currentTarget.valueAsNumber) { + if (e.currentTarget.valueAsNumber > 0 && this.props.CollectionView.props.Document.numCols as number !== e.currentTarget.valueAsNumber) { this.props.CollectionView.props.Document.numCols = e.currentTarget.valueAsNumber; //this.props.CollectionView.forceUpdate(); // to be used if CollectionGridView is not an observer } @@ -530,30 +586,44 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewChro /** * Sets the value of `rowHeight` on the grid's Document to the value entered. */ - @action + @undoBatch onRowHeightEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter" || e.key === "Tab") { - if (this.props.CollectionView.props.Document.rowHeight as number !== e.currentTarget.valueAsNumber) { + if (e.currentTarget.valueAsNumber > 0 && this.props.CollectionView.props.Document.rowHeight as number !== e.currentTarget.valueAsNumber) { this.props.CollectionView.props.Document.rowHeight = e.currentTarget.valueAsNumber; - //this.props.CollectionView.forceUpdate(); } } } + /** + * Sets whether the grid is flexible or not on the grid's Document. + */ + @undoBatch + toggleFlex = () => { + this.props.CollectionView.props.Document.flexGrid = !this.props.CollectionView.props.Document.flexGrid; + } + render() { return ( - <div className="collectionTreeViewChrome-cont"> - <span className={"search-icon"}> - <span className="icon-background"> + <div className="collectionGridViewChrome-cont"> + <span className={"grid-control"}> + <span className="grid-icon"> <FontAwesomeIcon icon="columns" size="1x" /> </span> - <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.props.CollectionView.props.Document.numCols as string} onKeyDown={this.onNumColsEnter} autoFocus /> + <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.props.CollectionView.props.Document.numCols as string} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => e.currentTarget.focus()} /> </span> - <span className={"search-icon"}> - <span className="icon-background"> + <span className={"grid-control"}> + <span className="grid-icon"> <FontAwesomeIcon icon="text-height" size="1x" /> </span> - <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.props.CollectionView.props.Document.rowHeight as string} onKeyDown={this.onRowHeightEnter} /> + <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.props.CollectionView.props.Document.rowHeight as string} onKeyDown={this.onRowHeightEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => e.currentTarget.focus()} /> + </span> + <span className={"grid-control"}> + <input style={{ marginRight: 5 }} type="checkbox" onClick={this.toggleFlex} defaultChecked={this.props.CollectionView.props.Document.flexGrid as boolean} /> + <span className="grid-icon"> + <FontAwesomeIcon icon="arrow-up" size="1x" /> + </span> + <label>Flexible</label> </span> </div> ); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 5478a1c4a..d9011c9d3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -41,32 +41,6 @@ // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions. touch-action: none; - .fwdKeyframe, .numKeyframe, .backKeyframe{ - cursor: pointer; - position: absolute; - width: 20; - height:20; - bottom:0; - background:gray; - } - .backKeyframe { - right:45; - svg { - display:block; - margin:auto; - } - } - .numKeyframe { - right:25; - text-align:center; - } - .fwdKeyframe { - right:5; - svg { - display:block; - margin:auto; - } - } .collectionfreeformview-placeholder { background: gray; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4840bb7e7..c753a703d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,7 +1,7 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed, trace } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../fields/Doc"; @@ -76,6 +76,7 @@ const PanZoomDocument = makeInterface(panZoomSchema, collectionSchema, documentS export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; + childPointerEvents?: boolean; }; @observer @@ -117,12 +118,28 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : this.Document.scale || 1) - private centeringShiftX = () => !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0; // shift so pan position is at center of window for non-overlay collections - private centeringShiftY = () => !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections - private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); - private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1); - private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); - private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); + @computed get cachedCenteringShiftX(): number { + return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0; // shift so pan position is at center of window for non-overlay collections + } + @computed get cachedCenteringShiftY(): number { + return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections + } + @computed get cachedGetLocalTransform(): Transform { + return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); + } + @computed get cachedGetContainerTransform(): Transform { + return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); + } + @computed get cachedGetTransform(): Transform { + return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); + } + + private centeringShiftX = () => this.cachedCenteringShiftX; + private centeringShiftY = () => this.cachedCenteringShiftY; + private getTransform = () => this.cachedGetTransform.copy(); + private getLocalTransform = () => this.cachedGetLocalTransform.copy(); + private getContainerTransform = () => this.cachedGetContainerTransform.copy(); + private getTransformOverlay = () => this.getContainerTransform().translate(1, 1); private addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); @@ -161,30 +178,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return retVal; }) - @undoBatch - @action - nextKeyframe = (): void => { - const currentFrame = this.Document.currentFrame; - if (currentFrame === undefined) { - this.Document.currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); - } - CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); - this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1); - this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame)); - } - @undoBatch - @action - prevKeyframe = (): void => { - const currentFrame = this.Document.currentFrame; - if (currentFrame === undefined) { - this.Document.currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); - } - CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); - this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1); - } - private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); @@ -501,9 +494,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P console.log("end"); if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); - const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "text" && s.color); + const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color); const sets = setDocs.map((sd) => { - return Cast(sd.data, RichTextField)?.Text as string; + return Cast(sd.text, RichTextField)?.Text as string; }); if (sets.length && sets[0]) { this._wordPalette.clear(); @@ -539,6 +532,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } }); + console.log(this._wordPalette) CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { console.log(results); const wordResults = results.filter((r: any) => r.category === "inkWord"); @@ -1123,10 +1117,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P replica={entry[1].replica} dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} - pointerEvents={ - this.backgroundActive ? - true : - (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined} + pointerEvents={this.backgroundActive || this.props.childPointerEvents ? + true : + (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined} jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))} //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this fitToBox={BoolCast(this.props.freezeChildDimensions)} // bcz: check this @@ -1218,6 +1211,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); + optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); @@ -1349,8 +1343,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} shifted={!this.nativeHeight && !this.isAnnotationOverlay} - easing={this.easing} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + <CollectionFreeFormViewPannableContents + centeringShiftX={this.centeringShiftX} + centeringShiftY={this.centeringShiftY} + shifted={!this.nativeHeight && !this.isAnnotationOverlay} + easing={this.easing} + transition={Cast(this.layoutDoc.transition, "string", null)} + viewDefDivClick={this.props.viewDefDivClick} + zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> {this.children} </CollectionFreeFormViewPannableContents> {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} @@ -1390,18 +1390,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> - {this.isAnnotationOverlay || !this.props.isSelected() || this.props.Document._viewType === CollectionViewType.Pile ? (null) : - <> - <div key="back" className="backKeyframe" onClick={this.prevKeyframe}> - <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> - </div> - <div key="num" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.Document.editing = !this.Document.editing)} > - {NumCast(this.Document.currentFrame)} - </div> - <div key="fwd" className="fwdKeyframe" onClick={this.nextKeyframe}> - <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> - </div> - </>} + <div className={"pullpane-indicator"} style={{ display: this._pullDirection ? "block" : "none", @@ -1444,6 +1433,7 @@ interface CollectionFreeFormViewPannableContentsProps { viewDefDivClick?: ScriptField; children: () => JSX.Element[]; shifted: boolean; + transition?: string; } @observer @@ -1458,7 +1448,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF return <div className={freeformclass} style={{ width: this.props.shifted ? 0 : undefined, height: this.props.shifted ? 0 : undefined, - transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` + transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, + transition: this.props.transition }}> {this.props.children()} </div>; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index ed70ac9e8..cdfeeaa6b 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -357,10 +357,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const selected = this.marqueeSelect(false); if (e instanceof KeyboardEvent ? e.key === "c" : true) { selected.map(action(d => { - //this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + const dx = NumCast(d.x); + const dy = NumCast(d.y); + delete d.x; + delete d.y; + delete d.activeFrame; + delete d.displayTimecode; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + d.x = dx - bounds.left - bounds.width / 2; + d.y = dy - bounds.top - bounds.height / 2; return d; })); this.props.removeDocument(selected); diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss index 8f12c1a24..fb30fe4c1 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.scss +++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss @@ -1,14 +1,39 @@ -.collectionGridView_contents { +.collectionGridView-contents { display: flex; overflow: hidden; width: 100%; height: 100%; flex-direction: column; - .document-wrapper { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - } +} + +// .documentDecorations-container .documentDecorations-resizer { +// pointer-events: none; +// } + +// #documentDecorations-bottomRightResizer, +// #documentDecorations-bottomLeftResizer, +// #documentDecorations-topRightResizer, +// #documentDecorations-topLeftResizer { +// visibility: collapse; +// } + +.collectionGridView-contents .collectionGridView-gridContainer { + height: 100%; + overflow-y: auto; + background-color: white; +} + + + +/* Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type=number] { + -moz-appearance: textfield; }
\ No newline at end of file diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 1775bcb7d..0583caa55 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -1,4 +1,4 @@ -import { computed, observable, Lambda } from 'mobx'; +import { computed, observable, Lambda, action } from 'mobx'; import * as React from "react"; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { documentSchema } from '../../../../fields/documentSchemas'; @@ -15,18 +15,29 @@ import Grid, { Layout } from "./Grid"; import { Id } from '../../../../fields/FieldSymbols'; import { observer } from 'mobx-react'; import "./CollectionGridView.scss"; +import { SnappingManager } from '../../../util/SnappingManager'; + type GridSchema = makeInterface<[typeof documentSchema]>; const GridSchema = makeInterface(documentSchema); @observer export class CollectionGridView extends CollectionSubView(GridSchema) { + private containerRef: React.RefObject<HTMLDivElement>; + @observable private _scroll: number = 0; private changeListenerDisposer: Opt<Lambda>; constructor(props: Readonly<SubCollectionViewProps>) { super(props); + this.props.Document.numCols = NumCast(this.props.Document.numCols, 10); this.props.Document.rowHeight = NumCast(this.props.Document.rowHeight, 100); + this.props.Document.flexGrid = (this.props.Document.flexGrid !== undefined) ? this.props.Document.flexGrid : true; + + this.setLayout = this.setLayout.bind(this); + this.deleteInContext = this.deleteInContext.bind(this); + + this.containerRef = React.createRef(); } componentDidMount() { @@ -89,10 +100,13 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { * documents before the target. */ private lookupIndividualTransform = (doc: Doc) => { - const yTranslation = (this.props.Document.rowHeight as number) * (doc.y as number) + 10 * (doc.y as number); - return this.props.ScreenToLocalTransform().translate(-this.props.PanelWidth() / (this.props.Document.numCols as number) * (doc.x as number), -yTranslation); + const yTranslation = this.rowHeightPlusGap * NumCast(doc.y) + 10 - this._scroll; + const xTranslation = this.colWidthPlusGap * NumCast(doc.x) + 10; + return this.props.ScreenToLocalTransform().translate(-xTranslation, -yTranslation); } + @computed get colWidthPlusGap() { return (this.props.PanelWidth() - 10) / NumCast(this.props.Document.numCols); } + @computed get rowHeightPlusGap() { return NumCast(this.props.Document.rowHeight) + 10; } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } @@ -100,13 +114,13 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { * Sets the width of the decorating box. * @param Doc doc */ - @observable private width = (doc: Doc) => doc.w as number * this.props.PanelWidth() / (this.props.Document.numCols as number); + @observable private width = (doc: Doc) => NumCast(doc.w) * this.colWidthPlusGap - 10; /** * Sets the height of the decorating box. * @param doc `Doc` */ - @observable private height = (doc: Doc) => doc.h as number * (this.props.Document.rowHeight as number); + @observable private height = (doc: Doc) => NumCast(doc.h) * this.rowHeightPlusGap - 10; addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { @@ -117,6 +131,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { } getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + console.log(layout[Id]); return <ContentFittingDocumentView {...this.props} Document={layout} @@ -124,8 +139,6 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { NativeHeight={returnZero} NativeWidth={returnZero} addDocTab={this.addDocTab} - fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} - FreezeDimensions={BoolCast(this.props.Document._freezeChildDimensions)} backgroundColor={this.props.backgroundColor} ContainingCollectionDoc={this.props.Document} PanelWidth={width} @@ -134,10 +147,41 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { onClick={this.onChildClickHandler} renderDepth={this.props.renderDepth + 1} parentActive={this.props.active} - display={"contents"} + display={"contents"} // this causes an issue- this is the reason the decorations box is weird with images and web boxes + removeDocument={this.deleteInContext} />; } + @undoBatch + deleteInContext(doc: Doc | Doc[]): boolean { + + if (!(this.props.Document.flexGrid as boolean)) { + this.props.removeDocument(doc); + } + else { + const docList: Doc[] = DocListCast(this.props.Document.gridLayouts); + const newDocList: Doc[] = []; + if (doc instanceof Doc) { + for (const savedDoc of docList) { + if (savedDoc.i !== doc[Id]) { + console.log("compare"); + console.log(savedDoc.i); + console.log(doc[Id]); + newDocList.push(savedDoc); + } + } + this.props.Document.gridLayouts = new List<Doc>(newDocList); + this.props.removeDocument(doc); + } + // else { + // console.log("doc is list"); + // this.props.removeDocument(doc); + // } + } + console.log("here???? in deletei n conte"); + return true; + } + /** * Saves the layouts received from the Grid to the Document. @@ -160,8 +204,6 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { }); } - // _.reject() on item removal? - /** * @returns a list of `ContentFittingDocumentView`s inside wrapper divs. * The key of the wrapper div must be the same as the `i` value of the corresponding layout. @@ -198,13 +240,26 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { * @param docLayoutList `Doc[]` */ toLayoutList(docLayoutList: Doc[]): Layout[] { - return docLayoutList.map(({ i, x, y, w, h }) => ({ - i: i as string, - x: x as number, - y: y as number, - w: w as number, - h: h as number - })); + + if (this.props.Document.flexGrid) { + return docLayoutList.map(({ i, x, y, w, h }) => ({ + i: i as string, + x: x as number, + y: y as number, + w: w as number, + h: h as number + })); + } + else { + return docLayoutList.map(({ i }, index) => ({ + i: i as string, + x: 2 * (index % Math.floor(this.props.Document.numCols as number / 2)), + y: 2 * Math.floor(index / Math.floor(this.props.Document.numCols as number / 2)), + w: 2, + h: 2, + static: true + })); + } } /** @@ -222,21 +277,40 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { return null; } return ( - <div className="collectionGridView_contents" + <div className="collectionGridView-contents" style={{ marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), - marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) + marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin), + pointerEvents: !this.props.isSelected() && this.props.renderDepth !== 0 && !this.props.ContainingCollectionView?._isChildActive && !SnappingManager.GetIsDragging() ? "none" : undefined }} ref={this.createDashEventsTarget} + onPointerDown={e => { + if (this.props.active(true)) { + if (this.props.isSelected(true)) { + e.stopPropagation(); + } + } + if (this.props.isSelected(true)) { + !((e.target as any)?.className.includes("react-resizable-handle")) && e.preventDefault(); + } + }} // the grid doesn't stopPropagation when its widgets are hit, so we need to otherwise the outer documents will respond > - <Grid - width={this.props.PanelWidth()} - nodeList={childDocumentViews} - layout={this.toLayoutList(layoutDocList)} - numCols={this.props.Document.numCols as number} - rowHeight={this.props.Document.rowHeight as number} - setLayout={(layout: Layout[]) => this.setLayout(layout)} - /> + <div className="collectionGridView-gridContainer" + ref={this.containerRef} + onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} + > + <Grid + width={this.props.PanelWidth()} + nodeList={childDocumentViews} + layout={this.toLayoutList(layoutDocList)} + childrenDraggable={this.props.isSelected() ? true : false} + numCols={this.props.Document.numCols as number} + rowHeight={this.props.Document.rowHeight as number} + setLayout={this.setLayout} + //setLayout={(layout: Layout[]) => this.setLayout(layout)} + transformScale={this.props.ScreenToLocalTransform().Scale} + /> + </div> </div> ); } diff --git a/src/client/views/collections/collectionGrid/Grid.tsx b/src/client/views/collections/collectionGrid/Grid.tsx index a5f5c724a..0e9e8bbca 100644 --- a/src/client/views/collections/collectionGrid/Grid.tsx +++ b/src/client/views/collections/collectionGrid/Grid.tsx @@ -7,7 +7,6 @@ import "../../../../../node_modules/react-resizable/css/styles.css"; import * as GridLayout from 'react-grid-layout'; import { Layout } from 'react-grid-layout'; -import { CollectionGridView } from './CollectionGridView'; export { Layout } from 'react-grid-layout'; @@ -18,6 +17,8 @@ interface GridProps { numCols: number; rowHeight: number; setLayout: Function; + transformScale: number; + childrenDraggable: boolean; } /** @@ -26,17 +27,20 @@ interface GridProps { @observer export default class Grid extends React.Component<GridProps> { + constructor(props: Readonly<GridProps>) { + super(props); + this.onLayoutChange = this.onLayoutChange.bind(this); + } /** * If there has been a change in layout, calls a method in CollectionGridView to set the layouts on the Document. * @param layout `Layout[]` */ onLayoutChange(layout: Layout[]) { - console.log("setting in grid component" + layout[0].w); this.props.setLayout(layout); } render() { - console.log("In grid layout prop received value= " + this.props.layout?.[0]?.w); + console.log(this.props.transformScale); return ( <GridLayout className="layout" layout={this.props.layout} @@ -45,8 +49,12 @@ export default class Grid extends React.Component<GridProps> { width={this.props.width} compactType={null} isDroppable={true} + // isDraggable={this.props.childrenDraggable} + // useCSSTransforms={true} margin={[10, 10]} - onLayoutChange={layout => this.onLayoutChange(layout)} + onLayoutChange={this.onLayoutChange} + preventCollision={false} // change this to true later + // transformScale={2} // 1.2/scale > {this.props.nodeList} </GridLayout > diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 5fb4cf3c6..13b9a2459 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -298,7 +298,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor"> {this.props.hideback ? (null) : <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>} <div className="linkEditor-info"> - <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p> + <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto?.title ?? destination.title ?? "untitled"}</b></p> <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button> </div> {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 682aed8f5..910aa744d 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -96,12 +96,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF opacityindexed?.length <= timecode + 1 && opacityindexed.push(undefined as any as number); doc.transition = "all 1s"; }); - setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010); + setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010); } public static gotoKeyframe(docs: Doc[]) { docs.forEach(doc => doc.transition = "all 1s"); - setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010); + setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010); } public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { @@ -119,6 +119,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); doc.y = ComputedField.MakeInterpolated("y", "activeFrame"); doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame"); + doc.transition = "inherit"; }); } @@ -153,6 +154,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF width: this.width, height: this.height, zIndex: this.ZInd, + mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, display: this.ZInd === -99 ? "none" : undefined, pointerEvents: this.props.Document.isBackground || this.Opacity === 0 ? "none" : this.props.pointerEvents ? "all" : undefined }} > diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 7849c9976..810a824cf 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -1,4 +1,5 @@ -.comparisonBox-interactive, .comparisonBox { +.comparisonBox-interactive, +.comparisonBox { border-radius: inherit; width: 100%; height: 100%; @@ -19,13 +20,13 @@ } } - .slide-bar { + .slide-bar { position: absolute; height: 100%; width: 3px; display: inline-block; background: white; - cursor: ew-resize; + .slide-handle { position: absolute; display: none; @@ -33,7 +34,9 @@ width: 30px; bottom: 0px; left: -10.5px; - .left-handle, .right-handle{ + + .left-handle, + .right-handle { width: 15px; } } @@ -77,10 +80,13 @@ } } } + .comparisonBox-interactive { pointer-events: unset; + cursor: ew-resize; + .slide-bar { - .slide-handle { + .slide-handle { display: flex; } } diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 77e07ec0c..cce60628d 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction, Lambda, IReactionDisposer } from 'mobx'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, Opt } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; import { createSchema, makeInterface } from '../../../fields/Schema'; import { NumCast, Cast, StrCast } from '../../../fields/Types'; @@ -32,13 +32,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); if (ele) { + // create disposers identified by disposerId to remove drag & drop listeners this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc); } } @undoBatch private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { - event.stopPropagation(); + event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; if (droppedDocs?.length) { this.dataDoc[fieldKey] = droppedDocs[0]; @@ -47,6 +48,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { + // on click, animate slider movement to the targetWidth this._animating = "all 1s"; this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth(); setTimeout(action(() => this._animating = ""), 1000); @@ -64,7 +66,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C @undoBatch clearDoc = (e: React.MouseEvent, fieldKey: string) => { - e.stopPropagation; + e.stopPropagation; // prevent click event action (slider movement) in registerSliding delete this.dataDoc[fieldKey]; } @@ -72,7 +74,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%"; const childProps: DocumentViewProps = { ...this.props, pointerEvents: false, parentActive: this.props.active }; const clearButton = (which: string) => { - return <div className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} onClick={e => this.clearDoc(e, `${which}Doc`)}> + return <div className={`clear-button ${which}`} + onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding + onClick={e => this.clearDoc(e, `${which}Doc`)}> <FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" /> </div>; }; @@ -95,13 +99,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C }; return ( - <div className={`comparisonBox${this.active() || SnappingManager.GetIsDragging() ? "-interactive" : ""}`}> + <div className={`comparisonBox${this.active() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}> {displayBox("after", 1, this.props.PanelWidth() - 5)} <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}> {displayBox("before", 0, 5)} </div> - <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)` }}> + <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)` }} + onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> </div >); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index ef56e6fcd..126e9ac14 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,6 +1,6 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, Field } from "../../../fields/Doc"; +import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc"; import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { OmitKeys, Without, emptyPath } from "../../../Utils"; import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; @@ -185,7 +185,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { const bindings = this.CreateBindings(onClick, onInput); // layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns - return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc) ? (null) : + return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || this.layoutDoc[AclSym] === AclPrivate) ? (null) : <ObserverJsxParser key={42} blacklistedAttrs={[]} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b5de0af12..334033874 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclSym, AclReadonly, AclPrivate } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -517,6 +517,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { e.stopPropagation(); + if (SelectionManager.IsSelected(this, true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it // TODO: check here for panning/inking } return; @@ -530,8 +531,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.inOverlay) { - e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); - + e.stopPropagation(); + if (SelectionManager.IsSelected(this, true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -743,13 +744,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const more = cm.findByDescription("More..."); const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; + moreItems.push({ description: "Make Add Only", event: () => this.dataDoc.ACL = this.layoutDoc.ACL = "addOnly", icon: "concierge-bell" }); + moreItems.push({ description: "Make Read Only", event: () => this.dataDoc.ACL = this.layoutDoc.ACL = "readOnly", icon: "concierge-bell" }); + moreItems.push({ description: "Make Private", event: () => this.dataDoc.ACL = this.layoutDoc.ACL = "noAccess", icon: "concierge-bell" }); moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); if (!ClientUtils.RELEASE) { // let copies: ContextMenuProps[] = []; - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { @@ -966,9 +970,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; } childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); - panelWidth = () => this.props.PanelWidth(); - panelHeight = () => this.props.PanelHeight(); - screenToLocalTransform = () => this.props.ScreenToLocalTransform(); @computed get contents() { TraceMobx(); return (<> @@ -989,10 +990,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} - ScreenToLocalTransform={this.screenToLocalTransform} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} ignoreAutoHeight={this.props.ignoreAutoHeight} focus={this.props.focus} parentActive={this.props.parentActive} @@ -1123,6 +1124,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } render() { + if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null); if (!(this.props.Document instanceof Doc)) return (null); const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null))); diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 05306c29f..9f1e99c77 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -62,6 +62,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); const curFrame = NumCast(presTargetDoc.currentFrame); if (lastFrame !== undefined && curFrame < lastFrame) { + presTargetDoc.transition = "all 1s"; + setTimeout(() => presTargetDoc.transition = undefined, 1010); presTargetDoc.currentFrame = curFrame + 1; } else if (this.childDocs[this.itemIndex + 1] !== undefined) { diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 8541a3149..affffc44e 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -29,6 +29,7 @@ .page { position: relative; + border: unset; } .pdfViewer-text-selected { .textLayer{ @@ -57,6 +58,9 @@ display: inline-block; width:100%; } + .pdfViewer-overlay { + pointer-events: none; + } .pdfViewer-annotationLayer { position: absolute; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 810ce5aea..5bad248be 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -31,8 +31,10 @@ import Annotation from "./Annotation"; import PDFMenu from "./PDFMenu"; import "./PDFViewer.scss"; import React = require("react"); +import { SnappingManager } from "../../util/SnappingManager"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); +import { Networking } from "../../Network"; export const pageSchema = createSchema({ curPage: "number", @@ -128,8 +130,17 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // change the address to be the file address of the PNG version of each page // file address of the pdf const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!; - const addr = Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`); - this._coverPath = href.startsWith(window.location.origin) ? JSON.parse(await rp.get(addr)) : { width: 100, height: 100, path: "" }; + const { url: relative } = this.props; + const pathComponents = relative.split("/pdfs/")[1].split("/"); + const coreFilename = pathComponents.pop()!.split(".")[0]; + const params: any = { + coreFilename, + pageNum: this.Document.curPage || 1, + }; + if (pathComponents.length) { + params.subtree = `${pathComponents.join("/")}/`; + } + this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; runInAction(() => this._showWaiting = this._showCover = true); this.props.startupLive && this.setupPdfJsViewer(); this._searchReactionDisposer = reaction(() => this.Document.searchMatch, search => { @@ -641,7 +652,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0); panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0); @computed get overlayLayer() { - return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" + return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}> <CollectionFreeFormView {...this.props} LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath} @@ -661,6 +672,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu ContentScaling={this.contentZoom} bringToFront={emptyFunction} whenActiveChanged={this.whenActiveChanged} + childPointerEvents={true} removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument} diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index eea7b528b..c9d29e485 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -309,9 +309,9 @@ export class SearchBox extends React.Component<SearchProps> { const baseExpr = "NOT baseProto_b:true"; const includeDeleted = this.getDataStatus() ? "" : " NOT deleted_b:true"; const includeIcons = this.getDataStatus() ? "" : " NOT type_t:fonticonbox"; - const typeExpr = !types ? "" : ` (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})`; + // const typeExpr = !types ? "" : ` (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})`; // this line was causing issues for me, check solr logging -syip2 // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello - const query = [baseExpr, includeDeleted, includeIcons, typeExpr].join(" AND ").replace(/AND $/, ""); + const query = [baseExpr, includeDeleted, includeIcons].join(" AND ").replace(/AND $/, ""); return query; } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 1ea686cbb..6891bf652 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -93,13 +93,40 @@ export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); export const DataSym = Symbol("Data"); export const LayoutSym = Symbol("Layout"); +export const AclSym = Symbol("Acl"); +export const AclPrivate = Symbol("AclNoAccess"); +export const AclReadonly = Symbol("AclReadOnly"); +export const AclAddonly = Symbol("AclAddonly"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); const CachedUpdates = Symbol("Cached updates"); function fetchProto(doc: Doc) { + if (doc.author !== Doc.CurrentUserEmail) { + if (doc.ACL === "noAccess") { + doc[AclSym] = AclPrivate; + return undefined; + } else if (doc.ACL === "readOnly") { + doc[AclSym] = AclReadonly; + } else if (doc.ACL === "addOnly") { + doc[AclSym] = AclAddonly; + } + } + const proto = doc.proto; if (proto instanceof Promise) { + proto.then(proto => { + if (proto.author !== Doc.CurrentUserEmail) { + if (proto.ACL === "noAccess") { + proto[AclSym] = doc[AclSym] = AclPrivate; + return undefined; + } else if (proto.ACL === "readOnly") { + proto[AclSym] = doc[AclSym] = AclReadonly; + } else if (proto.ACL === "addOnly") { + proto[AclSym] = doc[AclSym] = AclAddonly; + } + } + }); return proto; } } @@ -113,10 +140,10 @@ export class Doc extends RefField { set: setter, get: getter, // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter - has: (target, key) => key in target.__fields, + has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields, ownKeys: target => { const obj = {} as any; - Object.assign(obj, target.___fields); + (target[AclSym] !== AclPrivate) && Object.assign(obj, target.___fields); runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); return Object.keys(obj); }, @@ -170,8 +197,11 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; + public [AclSym]: any = undefined; public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); + public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; } + public [ToString]() { return `Doc(${this[AclSym] === AclPrivate ? "-inaccessible-" : this.title})`; } public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; } public get [DataSym]() { const self = this[SelfProxy]; @@ -193,8 +223,6 @@ export class Doc extends RefField { return undefined; } - [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; } - [ToString]() { return `Doc(${this.title})`; } private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; public static CurrentUserEmail: string = ""; @@ -794,6 +822,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 || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return 0; return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0; } export function IsBrushedDegree(doc: Doc) { @@ -802,11 +831,13 @@ export namespace Doc { })(doc); } export function BrushDoc(doc: Doc) { + if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc; brushManager.BrushedDoc.set(doc, true); brushManager.BrushedDoc.set(Doc.GetProto(doc), true); return doc; } export function UnBrushDoc(doc: Doc) { + if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc; brushManager.BrushedDoc.delete(doc); brushManager.BrushedDoc.delete(Doc.GetProto(doc)); return doc; @@ -836,6 +867,7 @@ export namespace Doc { } const highlightManager = new HighlightBrush(); export function IsHighlighted(doc: Doc) { + if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return false; return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc)); } export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) { diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 5192af407..fc7f9ca80 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -155,7 +155,7 @@ export class ComputedField extends ScriptField { public static MakeInterpolated(fieldKey: string, interpolatorKey: string) { const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); const setField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}] = value`, { value: "any" }, true, {}); - return getField.compiled && setField.compiled ? new ComputedField(getField, setField) : undefined; + return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 32f1b6e6c..e7031cc39 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -15,7 +15,7 @@ export const documentSchema = createSchema({ 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 - urrentTimecode: "number", // current play back time of a temporal document (video / audio) + currentTimecode: "number", // current play back time of a temporal document (video / audio) displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently x: "number", // x coordinate when in a freeform view diff --git a/src/fields/util.ts b/src/fields/util.ts index 024c0f80e..54e7eca28 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym } from "./Doc"; +import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -106,6 +106,7 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe "LODdisable", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; + if (target[AclSym]) return true; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); @@ -124,6 +125,8 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; + if (in_prop === AclSym) return target[AclSym]; + if (target[AclSym] === AclPrivate) return undefined; if (prop === LayoutSym) { return target.__LAYOUT__; } @@ -148,6 +151,9 @@ export function getter(target: any, in_prop: string | symbol | number, receiver: function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { receiver = receiver || target[SelfProxy]; + if (target === undefined) { + console.log(""); + } let field = target.__fields[prop]; for (const plugin of getterPlugins) { const res = plugin(receiver, prop, field); @@ -160,7 +166,7 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP } if (field === undefined && !ignoreProto && prop !== "proto") { const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters - if (proto instanceof Doc) { + if (proto instanceof Doc && proto[AclSym] !== AclPrivate) { return getFieldImpl(proto[Self], prop, receiver, ignoreProto); } return undefined; diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts index e55850b29..684c00c0d 100644 --- a/src/scraping/buxton/final/BuxtonImporter.ts +++ b/src/scraping/buxton/final/BuxtonImporter.ts @@ -1,4 +1,4 @@ -import { readdirSync, writeFile, mkdirSync } from "fs"; +import { readdirSync, writeFile, mkdirSync, createReadStream, createWriteStream, existsSync, statSync } from "fs"; import * as path from "path"; import { red, cyan, yellow } from "colors"; import { Utils } from "../../../Utils"; @@ -9,6 +9,7 @@ const createImageSizeStream = require("image-size-stream"); import { parseXml } from "libxmljs"; import { strictEqual } from "assert"; import { Readable, PassThrough } from "stream"; +import { Directory, serverPathToFile, pathToDirectory } from "../../../server/ApiManagers/UploadManager"; /** * This is an arbitrary bundle of data that gets populated @@ -18,8 +19,7 @@ interface DocumentContents { body: string; imageData: ImageData[]; hyperlinks: string[]; - captions: string[]; - embeddedFileNames: string[]; + tableData: TableData[]; longDescription: string; } @@ -40,6 +40,7 @@ export interface DeviceDocument { secondaryKey: string; attribute: string; __images: ImageData[]; + additionalMedia: ({ [type: string]: string } | undefined)[]; hyperlinks: string[]; captions: string[]; // from the table column embeddedFileNames: string[]; // from the table column @@ -255,6 +256,8 @@ const FormatMap = new Map<keyof DeviceDocument, ValueFormatDefinition<any>>([ ]); const sourceDir = path.resolve(__dirname, "source"); // where the Word documents are assumed to be stored +const assetDir = path.resolve(__dirname, "assets"); // where any additional media content like pdfs will be stored. Each subdirectory of this +// must follow the enum Directory.<type> naming scheme const outDir = path.resolve(__dirname, "json"); // where the JSON output of these device documents will be written const imageDir = path.resolve(__dirname, "../../../server/public/files/images/buxton"); // where, in the server, these images will be written const successOut = "buxton.json"; // the JSON list representing properly formatted documents @@ -277,12 +280,13 @@ export default async function executeImport(emitter: ResultCallback, terminator: rimraf.sync(dir); mkdirSync(dir); }); + await transferAssets(); return parseFiles(wordDocuments, emitter, terminator); } catch (e) { const message = [ "Unable to find a source directory.", - "Please ensure that the following directory exists and is populated with Word documents:", - `${sourceDir}` + "Please ensure that the following directory exists:", + `${e.message}` ].join('\n'); console.log(red(message)); return { error: message }; @@ -290,6 +294,32 @@ export default async function executeImport(emitter: ResultCallback, terminator: } /** + * Builds a mirrored directory structure of all media / asset files + * within the server's public directory. + */ +async function transferAssets() { + for (const assetType of readdirSync(assetDir)) { + const subroot = path.resolve(assetDir, assetType); + if (!statSync(subroot).isDirectory()) { + continue; + } + const outputSubroot = serverPathToFile(assetType as Directory, "buxton"); + if (existsSync(outputSubroot)) { + continue; + } else { + mkdirSync(outputSubroot); + } + for (const fileName of readdirSync(subroot)) { + const readStream = createReadStream(path.resolve(subroot, fileName)); + const writeStream = createWriteStream(path.resolve(outputSubroot, fileName)); + await new Promise<void>(resolve => { + readStream.pipe(writeStream).on("close", resolve); + }); + } + } +} + +/** * Parse every Word document in the directory, notifying any callers as needed * at each iteration via the emitter. * @param wordDocuments the string list of Word document names to parse @@ -356,6 +386,16 @@ const xPaths = { hyperlinks: '//*[name()="Relationship" and contains(@Type, "hyperlink")]' }; +interface TableData { + fileName: string; + caption: string; + additionalMedia?: { [type: string]: string }; +} + +const SuffixDirectoryMap = new Map<string, Directory>([ + ["p", Directory.pdfs] +]); + /** * The meat of the script, images and text content are extracted here * @param pathToDocument the path to the document relative to the root of the zip @@ -370,8 +410,7 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont // get plain text const body = document.root()?.text() ?? "No body found. Check the import script's XML parser."; const captions: string[] = []; - const embeddedFileNames: string[] = []; - + const tableData: TableData[] = []; // preserve paragraph formatting and line breaks that would otherwise get lost in the plain text parsing // of the XML hierarchy const paragraphs = document.find(xPaths.paragraphs).map(node => Utilities.correctSentences(node.text()).transformed!); @@ -382,7 +421,7 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont // extract captions from the table cells const tableRowsFlattened = document.find(xPaths.tableCells).map(node => node.text().trim()); const { length } = tableRowsFlattened; - const numCols = 3; + const numCols = 4; strictEqual(length > numCols, true, "No captions written."); // first row has the headers, not content strictEqual(length % numCols === 0, true, "Improper caption formatting."); @@ -392,8 +431,14 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont // have been added or reordered since this was written, but follow the same appraoch) for (let i = numCols; i < tableRowsFlattened.length; i += numCols) { const row = tableRowsFlattened.slice(i, i + numCols); - embeddedFileNames.push(row[1]); - captions.push(row[2]); + const entry: TableData = { fileName: row[1], caption: row[2] }; + const key = SuffixDirectoryMap.get(row[3].toLowerCase()); + if (key) { + const media: any = {}; + media[key] = `${entry.fileName.split(".")[0]}.pdf`; + entry.additionalMedia = media; + } + tableData.push(entry); } // extract all hyperlinks embedded in the document @@ -409,7 +454,7 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont // cleanup zip.close(); - return { body, longDescription, imageData, captions, embeddedFileNames, hyperlinks }; + return { body, longDescription, imageData, tableData, hyperlinks }; } // zip relative path from root expression / filter used to isolate only media assets @@ -492,11 +537,12 @@ async function writeImages(zip: any): Promise<ImageData[]> { * @param contents the data already computed / parsed by extractFileContents */ function analyze(fileName: string, contents: DocumentContents): AnalysisResult { - const { body, imageData, captions, hyperlinks, embeddedFileNames, longDescription } = contents; + const { body, imageData, hyperlinks, tableData, longDescription } = contents; const device: any = { hyperlinks, - captions, - embeddedFileNames, + captions: tableData.map(({ caption }) => caption), + embeddedFileNames: tableData.map(({ fileName }) => fileName), + additionalMedia: tableData.map(({ additionalMedia }) => additionalMedia), longDescription, __images: imageData }; diff --git a/src/scraping/buxton/final/assets/pdfs/3DCad_Brochure.pdf b/src/scraping/buxton/final/assets/pdfs/3DCad_Brochure.pdf Binary files differnew file mode 100644 index 000000000..4746d2f41 --- /dev/null +++ b/src/scraping/buxton/final/assets/pdfs/3DCad_Brochure.pdf diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts index 0136b758e..d2a9e9cce 100644 --- a/src/server/ApiManagers/PDFManager.ts +++ b/src/server/ApiManagers/PDFManager.ts @@ -7,54 +7,54 @@ import { createCanvas } from "canvas"; const imageSize = require("probe-image-size"); import * as express from "express"; import * as path from "path"; -import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager"; +import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from "./UploadManager"; import { red } from "colors"; +import { resolve } from "path"; export default class PDFManager extends ApiManager { protected initialize(register: Registration): void { register({ - method: Method.GET, - subscription: new RouteSubscriber("thumbnail").add("filename"), - secureHandler: ({ req, res }) => getOrCreateThumbnail(req.params.filename, res) + method: Method.POST, + subscription: new RouteSubscriber("thumbnail"), + secureHandler: async ({ req, res }) => { + const { coreFilename, pageNum, subtree } = req.body; + return getOrCreateThumbnail(coreFilename, pageNum, res, subtree); + } }); } } -async function getOrCreateThumbnail(thumbnailName: string, res: express.Response): Promise<void> { - const noExtension = thumbnailName.substring(0, thumbnailName.length - ".png".length); - const pageString = noExtension.split('-')[1]; - const pageNumber = parseInt(pageString); +async function getOrCreateThumbnail(coreFilename: string, pageNum: number, res: express.Response, subtree?: string): Promise<void> { + const resolved = `${coreFilename}-${pageNum}.png`; return new Promise<void>(async resolve => { - const path = serverPathToFile(Directory.pdf_thumbnails, thumbnailName); + const path = serverPathToFile(Directory.pdf_thumbnails, resolved); if (existsSync(path)) { const existingThumbnail = createReadStream(path); const { err, viewport } = await new Promise<any>(resolve => { imageSize(existingThumbnail, (err: any, viewport: any) => resolve({ err, viewport })); }); if (err) { - console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`)); + console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${resolved}:`)); console.log(err); return; } - dispatchThumbnail(res, viewport, thumbnailName); + dispatchThumbnail(res, viewport, resolved); } else { - const offset = thumbnailName.length - pageString.length - 5; - const name = thumbnailName.substring(0, offset) + ".pdf"; - const path = serverPathToFile(Directory.pdfs, name); - await CreateThumbnail(path, pageNumber, res); + await CreateThumbnail(coreFilename, pageNum, res, subtree); } resolve(); }); } -async function CreateThumbnail(file: string, pageNumber: number, res: express.Response) { - const documentProxy = await Pdfjs.getDocument(file).promise; +async function CreateThumbnail(coreFilename: string, pageNum: number, res: express.Response, subtree?: string) { + const sourcePath = resolve(pathToDirectory(Directory.pdfs), `${subtree ?? ""}${coreFilename}.pdf`); + const documentProxy = await Pdfjs.getDocument(sourcePath).promise; const factory = new NodeCanvasFactory(); - const page = await documentProxy.getPage(pageNumber); + const page = await documentProxy.getPage(pageNum); const viewport = page.getViewport(1 as any); const { canvas, context } = factory.create(viewport.width, viewport.height); const renderContext = { @@ -64,14 +64,13 @@ async function CreateThumbnail(file: string, pageNumber: number, res: express.Re }; await page.render(renderContext).promise; const pngStream = canvas.createPNGStream(); - const filenames = path.basename(file).split("."); - const thumbnailName = `${filenames[0]}-${pageNumber}.png`; - const pngFile = serverPathToFile(Directory.pdf_thumbnails, thumbnailName); + const resolved = `${coreFilename}-${pageNum}.png`; + const pngFile = serverPathToFile(Directory.pdf_thumbnails, resolved); const out = createWriteStream(pngFile); pngStream.pipe(out); return new Promise<void>((resolve, reject) => { out.on("finish", () => { - dispatchThumbnail(res, viewport, thumbnailName); + dispatchThumbnail(res, viewport, resolved); resolve(); }); out.on("error", error => { |