diff options
author | Melissa Zhang <mzhang19096@gmail.com> | 2020-08-07 09:40:06 -0700 |
---|---|---|
committer | Melissa Zhang <mzhang19096@gmail.com> | 2020-08-07 09:40:06 -0700 |
commit | 9886ed681ff18a33f7acab8f83b475ca9ea60bf7 (patch) | |
tree | 0b69d4b0fbfc52eac07c2e38fdbb34f161a2dc98 | |
parent | c94a61aa594f77db4c9b08a5f91c1a7e57d5ff9d (diff) | |
parent | b02cfed890d9d95a8f45bbc93d688bd3311dd387 (diff) |
merge with master
109 files changed, 7075 insertions, 3217 deletions
diff --git a/deploy/index.html b/deploy/index.html index 82e8c53a3..990ca510d 100644 --- a/deploy/index.html +++ b/deploy/index.html @@ -3,6 +3,7 @@ <head> <title>Dash Web</title> <link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet"> + <link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500' rel='stylesheet'> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script> diff --git a/package-lock.json b/package-lock.json index 51105842a..2cc50060f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,36 @@ } } }, + "@babel/helper-annotate-as-pure": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "requires": { + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" + }, + "@babel/types": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.5.tgz", + "integrity": "sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + } + } + }, "@babel/helper-function-name": { "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", @@ -1072,15 +1102,6 @@ "@types/prosemirror-transform": "*" } }, - "@types/puppeteer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-2.1.1.tgz", - "integrity": "sha512-FqPZvUtnpTGrqbHvPUn76pvVcBPEVEqZftrdOjr6YRkaaxkjKQ8dQLNaQBjER7Lvd1Q6+0R0XR+N3tYGWBSzNw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/qs": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", @@ -1226,6 +1247,11 @@ "@types/request": "*" } }, + "@types/reveal": { + "version": "3.3.33", + "resolved": "https://registry.npmjs.org/@types/reveal/-/reveal-3.3.33.tgz", + "integrity": "sha512-lKbezA9Oa5LfdSRwFDc/FHEGH4+FjiXh/a/PCSZAmN+KCeQJL/3ClOdAQwOxt3zdHc8XyioT+cNvIOletwRI7A==" + }, "@types/rimraf": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-2.0.4.tgz", @@ -2213,6 +2239,37 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -2299,6 +2356,17 @@ "resolve": "^1.12.0" } }, + "babel-plugin-styled-components": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz", + "integrity": "sha512-MBMHGcIA22996n9hZRf/UJLVVgkEOITuR2SvjHLb5dSTUyR4ZRGn+ngITapes36FI3WLxZHfRhkA1ffHxihOrg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-module-imports": "^7.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11" + } + }, "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", @@ -2929,6 +2997,11 @@ } } }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, "canvas": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz", @@ -4398,6 +4471,19 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" + }, "css-in-js-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", @@ -4461,6 +4547,16 @@ } } }, + "css-to-react-native": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-2.3.2.tgz", + "integrity": "sha512-VOFaeZA053BqvvvqIA8c9n0+9vFppVBAHCp6JgFTtTMU3Mzi+XnelJ9XC9ul3BqFzZyQ5N+H0SnwsWT2Ebchxw==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^3.3.0" + } + }, "css-vendor": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", @@ -7867,6 +7963,11 @@ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" }, + "is-what": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.10.0.tgz", + "integrity": "sha512-U4RYCXNOmATQHlOPlOCHCfXyKEFIPqvyaKDqYRuLbD6EYKcTTfc3YXkAYjzOVxO3zt34L+Wh2feIyWrYiZ7kng==" + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -8784,6 +8885,14 @@ "trim-newlines": "^1.0.0" } }, + "merge-anything": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-2.4.4.tgz", + "integrity": "sha512-l5XlriUDJKQT12bH+rVhAHjwIuXWdAIecGwsYjv2LJo+dA1AeRTmeQS+3QBpO6lEthBMDi2IUMpLC1yyRvGlwQ==", + "requires": { + "is-what": "^3.3.1" + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -13550,8 +13659,7 @@ "postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" }, "prebuild-install": { "version": "5.3.3", @@ -14042,6 +14150,11 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, + "raf-schd": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", + "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" + }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -14134,6 +14247,11 @@ "prop-types": "^15.6.2" } }, + "react-audio-waveform": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/react-audio-waveform/-/react-audio-waveform-0.0.5.tgz", + "integrity": "sha512-4dJwhl+LQRuywzmmjuhWT5GueT3Ai2ZhB+loVxg0sRdhplyDp96xAu10/V/+J25Cl7YqNtl2DWSxvSoI/i6l6w==" + }, "react-autosuggest": { "version": "9.4.3", "resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-9.4.3.tgz", @@ -14165,6 +14283,20 @@ "pure-color": "^1.2.0" } }, + "react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==", + "requires": { + "@babel/runtime": "^7.8.4", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.1.1", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-color": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz", @@ -14397,6 +14529,18 @@ } } }, + "react-redux": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz", + "integrity": "sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + } + }, "react-resizable": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-1.10.1.tgz", @@ -14406,6 +14550,19 @@ "react-draggable": "^4.0.3" } }, + "react-resizable-rotatable-draggable": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/react-resizable-rotatable-draggable/-/react-resizable-rotatable-draggable-0.2.0.tgz", + "integrity": "sha512-F8TPx3z7/AcmRViySbYV3LpUWXFpHlGAmKmNcYMgPlS+h1eYFazRG3xYS8Z6e48hWY1EcCny/YNrwRNUrap8CQ==" + }, + "react-reveal": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-reveal/-/react-reveal-1.2.2.tgz", + "integrity": "sha512-JCv3fAoU6Z+Lcd8U48bwzm4pMZ79qsedSXYwpwt6lJNtj/v5nKJYZZbw3yhaQPPgYePo3Y0NOCoYOq/jcsisuw==", + "requires": { + "prop-types": "^15.5.10" + } + }, "react-select": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.0.tgz", @@ -14623,6 +14780,15 @@ "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerator-runtime": { "version": "0.13.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", @@ -14871,6 +15037,11 @@ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", "dev": true }, + "reveal.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/reveal.js/-/reveal.js-4.0.2.tgz", + "integrity": "sha512-LWZSUenufF1gpD7npxJ7KfoQQFKgc1D6XrLTFgKlwWNP1BQ74hT48KONFWMAw+8R/QUeaScCLCLrBVHDfFIEiw==" + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -16214,6 +16385,36 @@ } } }, + "styled-components": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", + "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@emotion/is-prop-valid": "^0.8.1", + "@emotion/unitless": "^0.7.0", + "babel-plugin-styled-components": ">= 1", + "css-to-react-native": "^2.2.2", + "memoize-one": "^5.0.0", + "merge-anything": "^2.2.4", + "prop-types": "^15.5.4", + "react-is": "^16.6.0", + "stylis": "^3.5.0", + "stylis-rule-sheet": "^0.0.10", + "supports-color": "^5.5.0" + } + }, + "stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" + }, + "stylis-rule-sheet": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -16522,6 +16723,11 @@ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -17407,6 +17613,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-memo-one": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", + "integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==" + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index 0a3a63ec3..6736d27f6 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "@types/prosemirror-state": "^1.2.4", "@types/prosemirror-transform": "^1.1.1", "@types/prosemirror-view": "^1.11.2", - "@types/puppeteer": "^2.1.1", "@types/rc-switch": "^1.9.0", "@types/react": "^16.9.41", "@types/react-autosuggest": "^9.3.14", @@ -126,12 +125,14 @@ "@hig/theme-data": "^2.16.1", "@material-ui/core": "^4.11.0", "@types/google-maps": "^3.2.2", + "@types/reveal": "^3.3.33", "@types/webscopeio__react-textarea-autocomplete": "^4.6.1", "@webscopeio/react-textarea-autocomplete": "^4.7.0", "adm-zip": "^0.4.16", "archiver": "^3.1.1", "array-batcher": "^1.2.3", "async": "^2.6.2", + "axios": "^0.19.2", "babel-runtime": "^6.26.0", "bcrypt-nodejs": "0.0.3", "bezier-curve": "^1.0.0", @@ -217,7 +218,9 @@ "raw-loader": "^1.0.0", "rc-switch": "^1.9.0", "react": "^16.12.0", + "react-audio-waveform": "0.0.5", "react-autosuggest": "^9.4.3", + "react-beautiful-dnd": "^13.0.0", "react-color": "^2.18.1", "react-compound-slider": "^2.5.0", "react-datepicker": "^3.0.0", @@ -228,11 +231,14 @@ "react-loading": "^2.0.3", "react-measure": "^2.2.4", "react-resizable": "^1.10.1", + "react-resizable-rotatable-draggable": "^0.2.0", + "react-reveal": "^1.2.2", "react-select": "^3.1.0", "react-table": "^6.11.5", "readline": "^1.3.0", "request": "^2.88.0", "request-promise": "^4.2.5", + "reveal.js": "^4.0.2", "rimraf": "^3.0.0", "serializr": "^1.5.4", "sharp": "^0.23.4", @@ -241,6 +247,7 @@ "socket.io-client": "^2.3.0", "solr-node": "^1.2.1", "standard-http-error": "^2.0.1", + "styled-components": "^4.4.1", "textarea-caret": "^3.1.0", "typescript-collections": "^1.3.3", "typescript-language-server": "^0.4.0", diff --git a/solr-8.3.1/CHANGES.txt b/solr-8.3.1/CHANGES.txt index 219888b49..78ee2efc1 100644 --- a/solr-8.3.1/CHANGES.txt +++ b/solr-8.3.1/CHANGES.txt @@ -66,7 +66,7 @@ Upgrade Notes ---------------------- * Users who have written test cases that extend SolrTestCaseJ4 may see NullPointerExceptions if - their tests directly reference both SolrTestCaseJ4.initCoreDataDir and SolrTestCaseJ4.deleteCore(). + their tests directly reference both SolrTestCaseJ4.initCoreDataDir and TestCaseJ4.deleteCore(). This change in behavior is due to a bug fix in deleteCore() to ensure the dataDir is properly reset in tests that call initCore()/deleteCore() multiple times in a given test (class). initCoreDataDir is now deprecated, and users are encouraged to use SolrTestCaseJ4.initAndGetDataDir() in it's place. diff --git a/solr-8.3.1/bin/solr b/solr-8.3.1/bin/solr index 596242fac..596242fac 100644..100755 --- a/solr-8.3.1/bin/solr +++ b/solr-8.3.1/bin/solr diff --git a/solr-8.3.1/bin/solr-8983.pid b/solr-8.3.1/bin/solr-8983.pid index 779eb1af5..1f77d281d 100644 --- a/solr-8.3.1/bin/solr-8983.pid +++ b/solr-8.3.1/bin/solr-8983.pid @@ -1 +1 @@ -17656 +1731 diff --git a/solr-8.3.1/server/solr/dash/conf/schema.xml b/solr-8.3.1/server/solr/dash/conf/schema.xml index c0a4bab07..36e803d83 100644 --- a/solr-8.3.1/server/solr/dash/conf/schema.xml +++ b/solr-8.3.1/server/solr/dash/conf/schema.xml @@ -44,6 +44,9 @@ <field name="text" type="text" indexed="true" stored="false" uninvertible="false" multiValued="true"/> <field name="id" type="string" indexed="true" stored="true" uninvertible="false" required="true"/> <field name="_version_" type="plong" indexed="true" stored="true"/> + <field name="proto" type="string" indexed="true" stored="true" uninvertible="false" /> + <field name="_height" type="pdouble" indexed="true" stored="true" uninvertible="false" docValues="true"/> + <dynamicField name="*_t" type="text" indexed="true" stored="true" uninvertible="false" docValues="false"/> <dynamicField name="*_n" type="pdouble" indexed="true" stored="true" uninvertible="false" docValues="false"/> diff --git a/src/Utils.ts b/src/Utils.ts index a764ebea0..0be27b25d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -378,6 +378,15 @@ export function timenow() { return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm; } +export function formatTime(time: number) { + time = Math.round(time); + const hours = Math.floor(time / 60 / 60); + const minutes = Math.floor(time / 60) - (hours * 60); + const seconds = time % 60; + + return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); +} + export function aggregateBounds(boundsList: { x: number, y: number, width?: number, height?: number }[], xpad: number, ypad: number) { const bounds = boundsList.map(b => ({ x: b.x, y: b.y, r: b.x + (b.width || 0), b: b.y + (b.height || 0) })).reduce((bounds, b) => ({ x: Math.min(b.x, bounds.x), y: Math.min(b.y, bounds.y), diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 6fa8cf909..2fe3e9778 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -372,6 +372,9 @@ export namespace DocServer { } else if (cached instanceof Promise) { proms.push(cached as any); } + } else if (field) { + proms.push(_cache[field.id] as any); + fieldMap[field.id] = field; } } }); diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 985fcce11..71d6c2ccc 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -13,7 +13,7 @@ export enum DocumentType { INK = "ink", // ink stroke SCREENSHOT = "screenshot", // view of a desktop application FONTICON = "fonticonbox", // font icon - QUERY = "query", // search query + SEARCH = "search", // search query LABEL = "label", // simple text label BUTTON = "button", // onClick button WEBCAM = "webcam", // webcam @@ -31,6 +31,7 @@ export enum DocumentType { COLOR = "color", // color picker (view of a color picker for a color string) YOUTUBE = "youtube", // youtube directory (view of you tube search results) DOCHOLDER = "docholder", // nested document (view of a document) + SEARCHITEM= "searchitem", COMPARISON = "comparison", // before/after view with slider (view of 2 images) GROUP = "group", // group of users diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 725dacb5d..ddc25ec43 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -21,6 +21,8 @@ import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox"; import { LinkManager } from "../util/LinkManager"; import { Scripting } from "../util/Scripting"; import { UndoManager } from "../util/UndoManager"; +import { DocumentType } from "./DocumentTypes"; +import { SearchBox } from "../views/search/SearchBox"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; import { ContextMenu } from "../views/ContextMenu"; @@ -31,6 +33,7 @@ import { ColorBox } from "../views/nodes/ColorBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; +import { MenuIconBox } from "../views/nodes/MenuIconBox"; import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { ImageBox } from "../views/nodes/ImageBox"; import { KeyValueBox } from "../views/nodes/KeyValueBox"; @@ -38,7 +41,6 @@ import { LabelBox } from "../views/nodes/LabelBox"; import { LinkBox } from "../views/nodes/LinkBox"; import { PDFBox } from "../views/nodes/PDFBox"; import { PresBox } from "../views/nodes/PresBox"; -import { QueryBox } from "../views/nodes/QueryBox"; import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; import { ScriptingBox } from "../views/nodes/ScriptingBox"; import { SliderBox } from "../views/nodes/SliderBox"; @@ -46,7 +48,6 @@ import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; -import { DocumentType } from "./DocumentTypes"; import { Networking } from "../Network"; import { Upload } from "../../server/SharedMediaTypes"; const path = require('path'); @@ -128,6 +129,7 @@ export interface DocumentOptions { isLinkButton?: boolean; _columnWidth?: number; _fontSize?: string; + _fontWeight?: number; _fontFamily?: string; curPage?: number; currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds @@ -135,6 +137,12 @@ export interface DocumentOptions { currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection + appearFrame?: number; // the frame in which the document appears + presTransition?: number; //the time taken for the transition TO a document + presDuration?: number; //the duration of the slide in presentation view + presProgressivize?: boolean; + // xArray?: number[]; + // yArray?: number[]; borderRounding?: string; boxShadow?: string; dontRegisterChildViews?: boolean; @@ -189,10 +197,14 @@ export interface DocumentOptions { flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse"; selectedIndex?: number; syntaxColor?: string; // can be applied to text for syntax highlighting all matches in the text - searchText?: string; //for searchbox - searchQuery?: string; // for queryBox - filterQuery?: string; + searchQuery?: string; // for quersyBox linearViewIsExpanded?: boolean; // is linear view expanded + isLabel?: boolean; // whether the document is a label or not (video / audio) + useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox + audioStart?: number; // the time frame where the audio should begin playing + audioEnd?: number; // the time frame where the audio should stop playing + border?: string; //for searchbox + hovercolor?: string; } class EmptyBox { @@ -222,8 +234,8 @@ export namespace Docs { layout: { view: FormattedTextBox, dataField: "text" }, options: { _height: 150, _xMargin: 10, _yMargin: 10 } }], - [DocumentType.QUERY, { - layout: { view: QueryBox, dataField: defaultDataKey }, + [DocumentType.SEARCH, { + layout: { view: SearchBox, dataField: defaultDataKey }, options: { _width: 400 } }], [DocumentType.COLOR, { @@ -557,6 +569,7 @@ export namespace Docs { // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. dataDoc[fieldKey + "-annotations"] = new List<Doc>(); + dataDoc.aliases = new List<Doc>(); proto.links = ComputedField.MakeFunction("links(self)"); @@ -625,13 +638,13 @@ export namespace Docs { } export function AudioDocument(url: string, options: DocumentOptions = {}) { - const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options); + const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), { useLinkSmallAnchor: true, ...options }); // hideLinkButton: false, useLinkSmallAnchor: false, Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'"); return instance; } - export function QueryDocument(options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.QUERY), "", options); + export function SearchDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List<Doc>([]), options); } export function ColorDocument(options: DocumentOptions = {}) { @@ -919,6 +932,8 @@ export namespace DocUtils { if (target.doc === Doc.UserDoc()) return undefined; const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView", description }, id); + Doc.GetProto(linkDoc)["anchor1-useLinkSmallAnchor"] = source.doc.useLinkSmallAnchor; + Doc.GetProto(linkDoc)["anchor2-useLinkSmallAnchor"] = target.doc.useLinkSmallAnchor; linkDoc.linkDisplay = true; linkDoc.hidden = true; linkDoc.layout_linkView = Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 674980b32..49377a728 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -8,7 +8,7 @@ import { Doc, DocListCast, DocListCastAsync, DataSym } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { ScriptField, ComputedField } from "../../fields/ScriptField"; -import { Cast, PromiseValue, StrCast, NumCast } from "../../fields/Types"; +import { Cast, PromiseValue, StrCast, NumCast, BoolCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; import { DragManager } from "./DragManager"; import { Scripting } from "./Scripting"; @@ -45,7 +45,7 @@ export class CurrentUserUtils { if (doc["template-button-query"] === undefined) { const queryTemplate = Docs.Create.MulticolumnDocument( [ - Docs.Create.QueryDocument({ title: "query", _height: 200 }), + Docs.Create.SearchDocument({ _viewType: CollectionViewType.Schema, ignoreClick: true, forceActive: true, lockedPosition: true, title: "query", _height: 200 }), Docs.Create.FreeformDocument([], { title: "data", _height: 100 }) ], { _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true } @@ -248,8 +248,8 @@ export class CurrentUserUtils { if (doc["template-buttons"] === undefined) { doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, { title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", - hidden: ComputedField.MakeFunction("self.target.noviceMode") as any, - target: doc, + hidden: ComputedField.MakeFunction("self.userDoc.noviceMode") as any, + userDoc: doc, _autoHeight: true, _width: 500, _columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), })); @@ -387,7 +387,7 @@ export class CurrentUserUtils { static creatorBtnDescriptors(doc: Doc): { title: string, toolTip: string, icon: string, drag?: string, ignoreClick?: boolean, - click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc + click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc, noviceMode?: boolean }[] { if (doc.emptyPresentation === undefined) { doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), @@ -415,31 +415,28 @@ export class CurrentUserUtils { if (doc.emptyButton === undefined) { doc.emptyButton = Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title: "Button" }); } - if (doc.emptySearch === undefined) { - doc.emptySearch = Docs.Create.QueryDocument({ _width: 200, title: "empty search" }); - } if (doc.emptyDocHolder === undefined) { doc.emptyDocHolder = Docs.Create.DocumentDocument( ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any, { _width: 250, _height: 250, title: "container" }); } if (doc.emptyWebpage === undefined) { - doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 600, UseCors: true }); + doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 400, UseCors: true }); } if (doc.activeMobileMenu === undefined) { this.setupActiveMobileMenu(doc); } return [ - { toolTip: "Drag a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, - { toolTip: "Drag a web page", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc }, + { toolTip: "Drag a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc, noviceMode: true }, + { toolTip: "Drag a web page", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true }, { toolTip: "Drag a cat image", title: "Image", icon: "cat", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyImage as Doc }, - { toolTip: "Drag a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc }, + { toolTip: "Drag a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc, noviceMode: true }, { toolTip: "Drag a screengrabber", title: "Grab", icon: "photo-video", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScreenshot as Doc }, // { title: "Drag a webcam", title: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, - { toolTip: "Drag a audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc }, - { toolTip: "Drag a button", title: "Button", icon: "bolt", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyButton as Doc }, + { toolTip: "Drag a audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc, noviceMode: true }, + { toolTip: "Drag a button", title: "Button", icon: "bolt", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyButton as Doc, noviceMode: true }, - { toolTip: "Drag a presentation view", title: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true)`, dragFactory: doc.emptyPresentation as Doc }, + { toolTip: "Drag a presentation view", title: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, { toolTip: "Drag a search box", title: "Query", icon: "search", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptySearch as Doc }, { toolTip: "Drag a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScript as Doc }, // { title: "Drag an import folder", title: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, @@ -469,7 +466,7 @@ export class CurrentUserUtils { } } const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)); - const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({ + const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory, noviceMode }) => Docs.Create.FontIconDocument({ _nativeWidth: 50, _nativeHeight: 50, _width: 50, _height: 50, icon, title, @@ -483,6 +480,8 @@ export class CurrentUserUtils { backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory, + userDoc: noviceMode ? undefined as any : doc, + hidden: noviceMode ? undefined as any : ComputedField.MakeFunction("self.userDoc.noviceMode") })); if (dragCreatorSet === undefined) { @@ -513,6 +512,14 @@ export class CurrentUserUtils { ]; } + static setupSearchPanel(doc: Doc) { + if (doc["search-panel"] === undefined) { + doc["search-panel"] = new PrefetchProxy(Docs.Create.SearchDocument({ + _width: 500, _height: 400, backgroundColor: "dimGray", ignoreClick: true, + childDropAction: "alias", lockedPosition: true, _viewType: CollectionViewType.Schema, _chromeStatus: "disabled", title: "sidebar search stack", + })) as any as Doc; + } + } static setupMenuPanel(doc: Doc) { if (doc.menuStack === undefined) { const menuBtns = CurrentUserUtils.menuBtnDescriptions().map(({ title, icon, click }) => @@ -528,8 +535,8 @@ export class CurrentUserUtils { onClick: ScriptField.MakeScript(click, { scriptContext: "any" }), })); const userDoc = menuBtns[menuBtns.length - 1]; - userDoc.target = doc; - userDoc.hidden = ComputedField.MakeFunction("self.target.noviceMode"); + userDoc.userDoc = doc; + userDoc.hidden = ComputedField.MakeFunction("self.userDoc.noviceMode"); doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, { title: "menuItemPanel", @@ -547,7 +554,7 @@ export class CurrentUserUtils { btn.color = "white"; btn._backgroundColor = ""; })); - }) + }); }); return doc.menuStack as Doc; } @@ -653,7 +660,7 @@ export class CurrentUserUtils { // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. // when clicked, this panel will be displayed in the target container (ie, sidebarContainer) - static async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) { + static async setupToolsBtnPanel(doc: Doc) { // setup a masonry view of all he creators const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc); const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc); @@ -751,6 +758,8 @@ export class CurrentUserUtils { })) as any as Doc; } } + + static setupUserDoc(doc: Doc) { if (doc["sidebar-userDoc"] === undefined) { doc.treeViewOpen = true; @@ -764,19 +773,19 @@ export class CurrentUserUtils { } static setupSidebarContainer(doc: Doc) { - if (doc["sidebar"] === undefined) { + if (doc.sidebar === undefined) { const sidebarContainer = new Doc(); sidebarContainer._chromeStatus = "disabled"; sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()"); - doc["sidebar"] = new PrefetchProxy(sidebarContainer); + doc.sidebar = new PrefetchProxy(sidebarContainer); } - return doc["sidebar"] as Doc; + return doc.sidebar as Doc; } // setup the list of sidebar mode buttons which determine what is displayed in the sidebar static async setupSidebarButtons(doc: Doc) { - const sidebarContainer = CurrentUserUtils.setupSidebarContainer(doc); - await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer); + CurrentUserUtils.setupSidebarContainer(doc); + await CurrentUserUtils.setupToolsBtnPanel(doc); CurrentUserUtils.setupWorkspaces(doc); CurrentUserUtils.setupCatalog(doc); CurrentUserUtils.setupRecentlyClosed(doc); @@ -814,6 +823,11 @@ export class CurrentUserUtils { // the initial presentation Doc to use static setupDefaultPresentation(doc: Doc) { + if (doc["template-presentation"] === undefined) { + doc["template-presentation"] = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ + title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data" + })); + } if (doc.activePresentation === undefined) { doc.activePresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias", @@ -823,9 +837,9 @@ export class CurrentUserUtils { } // Right sidebar is where mobile uploads are contained - static setupRightSidebar(doc: Doc) { + static setupSharingSidebar(doc: Doc) { if (doc["sidebar-sharing"] === undefined) { - doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Mobile Uploads" })); + doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Shared Documents", childDropAction: "alias" })); } } @@ -891,14 +905,17 @@ export class CurrentUserUtils { doc.fontFamily = StrCast(doc.fontFamily, "Arial"); doc.fontColor = StrCast(doc.fontColor, "black"); doc.fontHighlight = StrCast(doc.fontHighlight, ""); + doc.defaultColor = StrCast(doc.defaultColor, "white"); + doc.noviceMode = BoolCast(doc.noviceMode, true); doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon this.setupDocTemplates(doc); // sets up the template menu of templates - this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing + this.setupSharingSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile this.setupMenuPanel(doc); + this.setupSearchPanel(doc); this.setupOverlays(doc); // documents in overlay layer this.setupDockedButtons(doc); // the bottom bar of font icons this.setupDefaultPresentation(doc); // presentation that's initially triggered diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss index 51e4fa9e2..9438bdd72 100644 --- a/src/client/util/GroupManager.scss +++ b/src/client/util/GroupManager.scss @@ -92,6 +92,7 @@ .sort-groups { text-align: left; margin-left: 5; + width: 50px; cursor: pointer; } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 229a846a9..277e96a89 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -18,7 +18,7 @@ import { setGroups } from "../../fields/util"; import { DocServer } from "../DocServer"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; -library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); +library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown); /** * Interface for options for the react-select component @@ -42,6 +42,7 @@ export default class GroupManager extends React.Component<{}> { private currentUserGroups: string[] = []; // the list of groups the current user is a member of @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; + private populating: boolean = false; @@ -62,21 +63,24 @@ export default class GroupManager extends React.Component<{}> { * Fetches the list of users stored on the database. */ populateUsers = async () => { - runInAction(() => this.users = []); - const userList = await RequestPromise.get(Utils.prepend("/getUsers")); - const raw = JSON.parse(userList) as User[]; - const evaluating = raw.map(async user => { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push(user.email); - } - }); - } - }); - return Promise.all(evaluating); + if (!this.populating) { + this.populating = true; + runInAction(() => this.users = []); + const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const raw = JSON.parse(userList) as User[]; + const evaluating = raw.map(async user => { + const userDocument = await DocServer.GetRefField(user.userDocumentId); + if (userDocument instanceof Doc) { + const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); + runInAction(() => { + if (notificationDoc instanceof Doc) { + this.users.push(user.email); + } + }); + } + }); + return Promise.all(evaluating).then(() => this.populating = false); + } } /** @@ -88,7 +92,6 @@ export default class GroupManager extends React.Component<{}> { const members: string[] = JSON.parse(StrCast(group.members)); if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); }); - setGroups(this.currentUserGroups); }); } @@ -120,6 +123,7 @@ export default class GroupManager extends React.Component<{}> { this.currentGroup = undefined; // this.users = []; this.createGroupModalOpen = false; + TaskCompletionBox.taskCompleted = false; } /** @@ -132,7 +136,6 @@ export default class GroupManager extends React.Component<{}> { /** * @returns a list of all group documents. */ - // private ? getAllGroups(): Doc[] { const groupDoc = this.GroupManagerDoc; return groupDoc ? DocListCast(groupDoc.data) : []; @@ -142,7 +145,6 @@ export default class GroupManager extends React.Component<{}> { * @returns a group document based on the group name. * @param groupName */ - // private? getGroup(groupName: string): Doc | undefined { const groupDoc = this.getAllGroups().find(group => group.groupName === groupName); return groupDoc; @@ -209,8 +211,6 @@ export default class GroupManager extends React.Component<{}> { deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { - // TODO look at this later - // SharingManager.Instance.setInternalGroupSharing(group, "Not Shared"); Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); SharingManager.Instance.removeGroup(group); const members: string[] = JSON.parse(StrCast(group.members)); @@ -311,7 +311,9 @@ export default class GroupManager extends React.Component<{}> { <div className="group-create"> <div className="group-heading" style={{ marginBottom: 0 }}> <p><b>New Group</b></p> - <div className={"close-button"} onClick={action(() => this.createGroupModalOpen = false)}> + <div className={"close-button"} onClick={action(() => { + this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; + })}> <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> </div> </div> @@ -319,6 +321,7 @@ export default class GroupManager extends React.Component<{}> { className="group-input" ref={this.inputRef} onKeyDown={this.handleKeyDown} + autoFocus type="text" placeholder="Group name" onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} /> @@ -363,7 +366,7 @@ export default class GroupManager extends React.Component<{}> { interactive={true} contents={contents} dialogueBoxStyle={{ width: "90%", height: "70%" }} - closeOnExternalClick={action(() => this.createGroupModalOpen = false)} + closeOnExternalClick={action(() => { this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; })} /> ); } @@ -405,7 +408,10 @@ export default class GroupManager extends React.Component<{}> { <div className="sort-groups" onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} + Name {this.groupSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> + : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> + : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} /> + } </div> <div className="group-body"> {groups.map(group => diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index f1e6155d2..cb0a4bea0 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -134,7 +134,7 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) { Scripting.addGlobal(constructor); } -const _scriptingGlobals: { [name: string]: any } = {}; +export const _scriptingGlobals: { [name: string]: any } = {}; let scriptingGlobals: { [name: string]: any } = _scriptingGlobals; const _scriptingDescriptions: { [name: string]: any } = {}; const _scriptingParams: { [name: string]: any } = {}; diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 0a01d8ac7..7b2c601fe 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -29,6 +29,8 @@ export namespace SearchUtil { rows?: number; fq?: string; allowAliases?: boolean; + "facet"?: string; + "facet.field"?: string; } export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>; export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>; diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index 6923fe879..41bce8a1b 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -1,7 +1,7 @@ @import "../views/globalCssVariables"; .settings-interface { - background-color: whitesmoke !important; + //background-color: whitesmoke !important; color: grey; width: 450px; height: 300px; @@ -22,85 +22,232 @@ } } -.settings-interface { +.settings-title { + font-size: 25px; + font-weight: bold; + padding-right: 10px; + color: black; +} + +.settings-username { + font-size: 14px; + padding-right: 15px; + color: black; + margin-top: 10px; +} + +.settings-section { display: flex; - flex-direction: column; + border-bottom: 1px solid grey; + padding-bottom: 8px; + padding-top: 6px; - button { - width: 100%; - align-self: center; - background: #252b33; - margin-top: 4px; + .settings-section-title { + font-size: 16; + font-weight: bold; + text-align: left; + color: black; + width: 80; + margin-right: 50px; + } + + &:last-child { + border-bottom: none; + } +} + + +.password-content { + display: flex; + + .password-content-inputs { + width: 100; + + .password-inputs { + border: none; + margin-bottom: 8px; + width: 180; + color: black; + border-radius: 5px; + } + } + + .password-content-buttons { + margin-left: 84px; + width: 100; + + .password-submit { + margin-left: 85px; + } + + .password-forgot { + margin-left: 65px; + margin-top: -20px; + white-space: nowrap; + } + } +} + +.accounts-content { + display: flex; +} + +.modes-content { + display: flex; + + .modes-select { + width: 170px; + margin-right: 65px; + color: black; + border-radius: 5px; &:hover { - background: $main-accent; + cursor: pointer; } } - .delete-button { - background: rgb(227, 86, 86); + .modes-playground { + display: flex; + + .playground-check { + margin-right: 5px; + + &:hover { + cursor: pointer; + } + } + + .playground-text { + color: black; + } } +} - .close-button { - position: absolute; - right: 1em; - top: 1em; +.colorFlyout { + margin-top: 2px; + margin-right: 25px; + + &:hover { cursor: pointer; } - .settings-heading { - letter-spacing: .5em; + .colorFlyout-button { + width: 20px; + height: 20px; + border: 0.5px solid black; + border-radius: 5px; } +} +.preferences-content { + display: flex; + margin-top: 4px; - .settings-body { + .preferences-color { display: flex; - justify-content: space-between; - margin-top: -10; - .settings-type { - display: flex; - flex-direction: column; - flex-basis: 45%; + .preferences-color-text { + color: black; + font-size: 11; + margin-top: 4; + margin-right: 4; + } + } + .preferences-font { + display: flex; + + .preferences-font-text { + color: black; + font-size: 11; + margin-top: 4; + margin-right: 4; } - .settings-content { - padding-left: 1em; - padding-right: 1em; - display: flex; - flex-direction: column; - flex-basis: 70%; - justify-content: space-around; - text-align: left; - - ::placeholder { - color: $intermediate-color; - } + .font-select { + width: 100px; + color: black; + font-size: 9; + margin-right: 6; + border-radius: 5px; - input { - border-radius: 5px; - border: none; - padding: 4px; - min-width: 100%; - margin: 2px 0; + &:hover { + cursor: pointer; } + } - .error-text { - color: #C40233; - } + .size-select { + width: 60px; + color: black; + font-size: 9; + border-radius: 5px; - .success-text { - color: #009F6B; + &:hover { + cursor: pointer; } + } + } +} - p { - padding: 0 0 .1em .2em; - } +.settings-interface { + display: flex; + flex-direction: column; + + button { + width: auto; + align-self: center; + background: #252b33; + margin-right: 15px; + //margin-top: 4px; + + &:hover { + background: $main-accent; } } + // .delete-button { + // background: rgb(227, 86, 86); + // } + + .close-button { + position: absolute; + right: 1em; + top: 1em; + cursor: pointer; + } + + .settings-content { + background: #e4e4e4; + border-radius: 6px; + padding: 10px; + width: 560px; + } + + .settings-top { + display: flex; + margin-bottom: 10px; + } + + + .error-text { + color: #C40233; + width: 300; + margin-left: -20; + font-size: 10; + margin-bottom: 4; + margin-top: -3; + } + + .success-text { + width: 300; + margin-left: -20; + font-size: 10; + margin-bottom: 4; + margin-top: -3; + color: #009F6B; + } + .focus-span { text-decoration: underline; } @@ -138,8 +285,6 @@ color: black; } - - } } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index d438ec971..2fd77b915 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,4 +1,4 @@ -import { observable, runInAction, action } from "mobx"; +import { observable, runInAction, action, computed } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; import { observer } from "mobx-react"; @@ -14,6 +14,12 @@ import { Doc } from "../../fields/Doc"; import GroupManager from "./GroupManager"; import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; import { DocServer } from "../DocServer"; +import { BoolCast, StrCast, NumCast } from "../../fields/Types"; +import { undoBatch } from "./UndoManager"; +import { ColorState, SketchPicker } from "react-color"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; library.add(fa.faTimes); @@ -32,6 +38,9 @@ export default class SettingsManager extends React.Component<{}> { private new_password_ref = React.createRef<HTMLInputElement>(); private new_confirm_ref = React.createRef<HTMLInputElement>(); + + @computed get backgroundColor() { return Doc.UserDoc().defaultColor; } + public open = action(() => { SelectionManager.DeselectAll(); this.isOpen = true; @@ -102,49 +111,148 @@ export default class SettingsManager extends React.Component<{}> { addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" }); } + @action + changeMode = (e: any) => { + if (e.currentTarget.value === "Novice") { + Doc.UserDoc().noviceMode = true; + } else { + Doc.UserDoc().noviceMode = false; + } + } + + @action + changeFontFamily = (e: any) => { + Doc.UserDoc().fontFamily = e.currentTarget.value; + } + + @action + changeFontSize = (e: any) => { + Doc.UserDoc().fontSize = e.currentTarget.value; + } + + @action @undoBatch + switchColor = (color: ColorState) => { + const val = String(color.hex); + Doc.UserDoc().defaultColor = val; + return true; + } + private get settingsInterface() { - return ( - <div className={"settings-interface"}> - <div className="settings-heading"> - <h1>settings</h1> - <div className={"close-button"} onClick={this.close}> - <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} /> - </div> - </div> - <div className="settings-body"> - <div className="settings-type"> - <button onClick={this.onClick} value="password">reset password</button> - <button onClick={this.noviceToggle} value="data">{`Set ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button> - <button onClick={this.togglePlaygroundMode}>{`${this.playgroundMode ? "Disable" : "Enable"} playground mode`}</button> - <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button> - <button onClick={() => GroupManager.Instance.open()}>Manage groups</button> - <button onClick={() => window.location.assign(Utils.prepend("/logout"))}> - {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} - </button> + const passwordContent = <div className="password-content"> + <div className="password-content-inputs"> + <input className="password-inputs" type="password" placeholder="current password" ref={this.curr_password_ref} /> + <input className="password-inputs" type="password" placeholder="new password" ref={this.new_password_ref} /> + <input className="password-inputs" type="password" placeholder="confirm new password" ref={this.new_confirm_ref} /> + </div> + <div className="password-content-buttons"> + {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined} + {this.successText ? <div className="success-text">{this.successText}</div> : undefined} + <button className="password-submit" onClick={this.dispatchRequest}>submit</button> + <a className="password-forgot" style={{ marginLeft: 65, marginTop: -20 }} + href="/forgotPassword">forgot password?</a> + </div> + </div>; + + const modesContent = <div className="modes-content"> + <select className="modes-select" + onChange={e => this.changeMode(e)}> + <option key={"Novice"} value={"Novice"} selected={BoolCast(Doc.UserDoc().noviceMode)}> + Novice + </option> + <option key={"Developer"} value={"Developer"} selected={!BoolCast(Doc.UserDoc().noviceMode)}> + Developer + </option> + </select> + <div className="modes-playground"> + <input className="playground-check" type="checkbox" + checked={this.playgroundMode} + onChange={undoBatch(action(() => this.togglePlaygroundMode()))} + /><div className="playground-text">Playground Mode</div> + </div> + </div>; + + const accountsContent = <div className="accounts-content"> + <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button> + <button onClick={() => GroupManager.Instance.open()}>Manage groups</button> + </div>; + + const colorBox = <SketchPicker onChange={this.switchColor} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb', 'transparent']} + color={StrCast(this.backgroundColor)} />; + + const colorFlyout = <div className="colorFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={colorBox}> + <div> + <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} + onPointerDown={e => e.stopPropagation()} > + <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> </div> - {this.settingsContent === "password" ? - <div className="settings-content"> - <input placeholder="current password" ref={this.curr_password_ref} /> - <input placeholder="new password" ref={this.new_password_ref} /> - <input placeholder="confirm new password" ref={this.new_confirm_ref} /> - {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined} - {this.successText ? <div className="success-text">{this.successText}</div> : undefined} - <button onClick={this.dispatchRequest}>submit</button> - <a style={{ marginLeft: 65, marginTop: -20 }} href="/forgotPassword">forgot password?</a> - - </div> - : undefined} - {this.settingsContent === "data" ? - <div className="settings-content"> - <p>WARNING: <br /> - THIS WILL ERASE ALL YOUR CURRENT DOCUMENTS STORED ON DASH. IF YOU WISH TO PROCEED, CLICK THE BUTTON BELOW.</p> - <button className="delete-button">DELETE</button> - </div> - : undefined} </div> + </Flyout> + </div>; + + const fontFamilies: string[] = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; + const fontSizes: string[] = ["7pt", "8pt", "9pt", "10pt", "12pt", "14pt", "16pt", "18pt", "20pt", "24pt", "32pt", "48pt", "72pt"]; + const preferencesContent = <div className="preferences-content"> + <div className="preferences-color"> + <div className="preferences-color-text">Background Color</div> {colorFlyout} </div> - ); + <div className="preferences-font"> + <div className="preferences-font-text">Default Font</div> + <select className="font-select" + onChange={e => this.changeFontFamily(e)}> + {fontFamilies.map((font) => { + return <option key={font} value={font} selected={StrCast(Doc.UserDoc().fontFamily) === font}> + {font} + </option>; + })} + </select> + <select className="size-select" + onChange={e => this.changeFontSize(e)}> + {fontSizes.map((size) => { + return <option key={size} value={size} selected={StrCast(Doc.UserDoc().fontSize) === size}> + {size} + </option>; + })} + </select> + </div> + </div>; + + return (<div className="settings-interface"> + <div className="settings-top"> + <div className="settings-title">Settings</div> + <div className="settings-username">{Doc.CurrentUserEmail}</div> + <button onClick={() => window.location.assign(Utils.prepend("/logout"))} + style={{ right: 35, position: "absolute" }} > + {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} + </button> + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} /> + </div> + </div> + <div className="settings-content"> + <div className="settings-section"> + <div className="settings-section-title">Password</div> + <div className="settings-section-context">{passwordContent}</div> + </div> + <div className="settings-section"> + <div className="settings-section-title">Modes</div> + <div className="settings-section-context">{modesContent}</div> + </div> + <div className="settings-section"> + <div className="settings-section-title">Accounts</div> + <div className="settings-section-context">{accountsContent}</div> + </div> + <div className="settings-section" style={{ paddingBottom: 4 }}> + <div className="settings-section-title">Preferences</div> + <div className="settings-section-context">{preferencesContent}</div> + </div> + </div> + </div>); } render() { @@ -154,6 +262,7 @@ export default class SettingsManager extends React.Component<{}> { isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} + dialogueBoxStyle={{ width: "600px", height: "340px" }} /> ); } diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 8da80ef52..7912db74d 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -84,6 +84,7 @@ .user-sort { text-align: left; margin-left: 10; + width: 100px; cursor: pointer; } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 9d91ce1ba..d50a132f8 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,7 +1,7 @@ import { observable, runInAction, action } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, DocListCastAsync, AclAdmin, DataSym, AclPrivate } from "../../fields/Doc"; +import { Doc, Opt, AclAdmin, AclPrivate, DocListCast } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; @@ -21,6 +21,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { List } from "../../fields/List"; import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; +import { library } from "@fortawesome/fontawesome-svg-core"; + +library.add(fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown); + export interface User { email: string; @@ -71,6 +75,7 @@ export default class SharingManager extends React.Component<{}> { // if both showUserOptions and showGroupOptions are false then both are displayed @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) + private populating: boolean = false; // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; @@ -87,13 +92,13 @@ export default class SharingManager extends React.Component<{}> { this.isOpen = true; this.permissions = SharingPermissions.Edit; }); - + this.targetDoc!.author === Doc.CurrentUserEmail && !this.targetDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, this.targetDoc!); } public close = action(() => { this.isOpen = false; this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component) - + TaskCompletionBox.taskCompleted = false; setTimeout(action(() => { // this.copied = false; DictationOverlay.Instance.hasActiveModal = false; @@ -117,24 +122,27 @@ export default class SharingManager extends React.Component<{}> { * Populates the list of validated users (this.users) by adding registered users which have a sidebar-sharing. */ populateUsers = async () => { - runInAction(() => this.users = []); - const userList = await RequestPromise.get(Utils.prepend("/getUsers")); - const raw = JSON.parse(userList) as User[]; - const evaluating = raw.map(async user => { - const isCandidate = user.email !== Doc.CurrentUserEmail; - if (isCandidate) { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push({ user, notificationDoc }); - } - }); + if (!this.populating) { + this.populating = true; + runInAction(() => this.users = []); + const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const raw = JSON.parse(userList) as User[]; + const evaluating = raw.map(async user => { + const isCandidate = user.email !== Doc.CurrentUserEmail; + if (isCandidate) { + const userDocument = await DocServer.GetRefField(user.userDocumentId); + if (userDocument instanceof Doc) { + const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); + runInAction(() => { + if (notificationDoc instanceof Doc) { + this.users.push({ user, notificationDoc }); + } + }); + } } - } - }); - return Promise.all(evaluating); + }); + return Promise.all(evaluating).then(() => this.populating = false); + } } /** @@ -152,13 +160,11 @@ export default class SharingManager extends React.Component<{}> { target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); // if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target - group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]); + group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([target]); users.forEach(({ notificationDoc }) => { - DocListCastAsync(notificationDoc[storage]).then(resolved => { - if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added - else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists - }); + if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added + else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists }); } @@ -170,13 +176,7 @@ export default class SharingManager extends React.Component<{}> { shareWithAddedMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - if (group.docsShared) { - DocListCastAsync(group.docsShared).then(docsShared => { - docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); // add the doc if it isn't already in the list - }); - }); - } + if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); } shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, target: Doc) => { @@ -194,10 +194,8 @@ export default class SharingManager extends React.Component<{}> { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { - DocListCastAsync(group.docsShared).then(docsShared => { - docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); // remove the doc only if it is in the list - }); + DocListCast(group.docsShared).forEach(doc => { + Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc); // remove the doc only if it is in the list }); } } @@ -208,39 +206,36 @@ export default class SharingManager extends React.Component<{}> { */ removeGroup = (group: Doc) => { if (group.docsShared) { - DocListCastAsync(group.docsShared).then(resolved => { - resolved?.forEach(doc => { - const ACL = `ACL-${StrCast(group.groupName)}`; + DocListCast(group.docsShared).forEach(doc => { + const ACL = `ACL-${StrCast(group.groupName)}`; - distributeAcls(ACL, SharingPermissions.None, doc); + distributeAcls(ACL, SharingPermissions.None, doc); - const members: string[] = JSON.parse(StrCast(group.members)); - const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - - users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); - }); + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); }); } } + /** + * Shares the document with a user. + */ setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { const { user, notificationDoc } = recipient; const target = targetDoc || this.targetDoc!; const key = user.email.replace('.', '_'); const ACL = `ACL-${key}`; + target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); if (permission !== SharingPermissions.None) { - DocListCastAsync(notificationDoc[storage]).then(resolved => { - Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); - }); + Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); } else { - DocListCastAsync(notificationDoc[storage]).then(resolved => { - Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); - }); + Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); } } @@ -268,14 +263,17 @@ export default class SharingManager extends React.Component<{}> { // } // }); + /** + * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share + */ private get sharingOptions() { - return Object.values(SharingPermissions).map(permission => { - return ( - <option key={permission} value={permission} selected={permission === SharingPermissions.Edit}> + return Object.values(SharingPermissions).map(permission => + ( + <option key={permission} value={permission}> {permission} </option> - ); - }); + ) + ); } private focusOn = (contents: string) => { @@ -308,16 +306,25 @@ export default class SharingManager extends React.Component<{}> { ); } + /** + * Handles changes in the users selected in react-select + */ @action handleUsersChange = (selectedOptions: any) => { this.selectedUsers = selectedOptions as UserOptions[]; } + /** + * Handles changes in the permission chosen to share with someone with + */ @action handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => { this.permissions = event.currentTarget.value as SharingPermissions; } + /** + * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. + */ @action share = () => { if (this.selectedUsers) { @@ -341,18 +348,27 @@ export default class SharingManager extends React.Component<{}> { } } + /** + * Sorting algorithm to sort users. + */ sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { const { email: e1 } = u1.user; const { email: e2 } = u2.user; return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; } + /** + * Sorting algorithm to sort groups. + */ sortGroups = (group1: Doc, group2: Doc) => { const g1 = StrCast(group1.groupName); const g2 = StrCast(group2.groupName); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; } + /** + * @returns the main interface of the SharingManager. + */ private get sharingInterface() { const groupList = GroupManager.Instance?.getAllGroups() || []; @@ -361,8 +377,8 @@ export default class SharingManager extends React.Component<{}> { const sortedGroups = groupList.slice().sort(this.sortGroups) .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); + // the next block handles the users shown (individuals/groups/both) const options: GroupedOptions[] = []; - if (GroupManager.Instance) { if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { options.push({ @@ -393,6 +409,7 @@ export default class SharingManager extends React.Component<{}> { const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate; + // the list of users shared with const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { const userKey = user.email.replace('.', '_'); const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]); @@ -422,6 +439,7 @@ export default class SharingManager extends React.Component<{}> { ); }); + // the owner of the doc and the current user are placed at the top of the user list. userListContents.unshift( ( <div @@ -452,6 +470,7 @@ export default class SharingManager extends React.Component<{}> { ) : null ); + // the list of groups shared with const groupListContents = groups.map(group => { const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`]); @@ -477,6 +496,7 @@ export default class SharingManager extends React.Component<{}> { ); }); + // don't display the group list if all groups are null const displayGroupList = !groupListContents?.every(group => group === null); return ( @@ -538,7 +558,7 @@ export default class SharingManager extends React.Component<{}> { }) }} /> - <select className="permissions-select" onChange={this.handlePermissionsChange}> + <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}> {this.sharingOptions} </select> <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> @@ -556,7 +576,9 @@ export default class SharingManager extends React.Component<{}> { <div className="user-sort" onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> - Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */} + Individuals {this.individualSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> + : this.individualSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> + : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />} </div> <div className={"users-list"} style={{ display: "block" }}>{/*200*/} {userListContents} @@ -566,7 +588,9 @@ export default class SharingManager extends React.Component<{}> { <div className="user-sort" onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Groups {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */} + Groups {this.groupSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> + : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> + : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />} </div> <div className={"groups-list"} style={{ display: !displayGroupList ? "flex" : "block" }}>{/*200*/} diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 08aec3724..ab6c94f83 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -187,6 +187,11 @@ declare class List<T extends Field> extends ObjectField { [Copy](): ObjectField; } +declare class InkField extends ObjectField { + constructor(data:Array<{X:number, Y:number}>); + [Copy](): ObjectField; +} + // @ts-ignore declare const console: any; diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 3717a2923..c379549d0 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 86e0a568a..7467bc043 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -7,7 +7,7 @@ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; flex-direction: column; background: whitesmoke; - padding-top: 10px; + padding-top: 10px; padding-bottom: 10px; border-radius: 15px; border: solid #BBBBBBBB 1px; @@ -72,6 +72,7 @@ margin-left: 5px; } } + .contextMenu-description { // width: 11vw; //10vw background: whitesmoke; @@ -100,6 +101,8 @@ border-color: $intermediate-color; // rgb(187, 186, 186); border-bottom-style: solid; border-top-style: solid; + + cursor: pointer; } .contextMenu-itemSelected { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 804c7a8d4..831c246d1 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,4 +1,4 @@ -import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast } from '../../fields/Doc'; +import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast, AclAdmin } from '../../fields/Doc'; import { Touchable } from './Touchable'; import { computed, action, observable } from 'mobx'; import { Cast, BoolCast, ScriptCast } from '../../fields/Types'; @@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; -import { GetEffectiveAcl, SharingPermissions } from '../../fields/util'; +import { GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../fields/util'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -96,7 +96,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T [AclPrivate, SharingPermissions.None], [AclReadonly, SharingPermissions.View], [AclAddonly, SharingPermissions.Add], - [AclEdit, SharingPermissions.Edit] + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] ]); lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; @@ -154,15 +155,14 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T return false; } else { - // if (this.props.Document[AclSym]) { - // added.forEach(d => { - // const dataDoc = d[DataSym]; - // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; - // for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - // dataDoc[key] = d[key] = this.AclMap.get(value); - // } - // }); - // } + if (this.props.Document[AclSym]) { + added.forEach(d => { + for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + } + }); + } + if (effectiveAcl === AclAddonly) { added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)); } diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss index c2ca93900..09ae14016 100644 --- a/src/client/views/DocumentButtonBar.scss +++ b/src/client/views/DocumentButtonBar.scss @@ -64,9 +64,13 @@ $linkGap : 3px; text-align: center; border-radius: 50%; pointer-events: auto; - color: $dark-color; - border: $dark-color 1px solid; + background-color: $dark-color; + border: none; transition: 0.2s ease all; + + &:hover { + background-color: $main-accent; + } } .documentButtonBar-linker:hover { diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index c9f380737..8748b1880 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -198,7 +198,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const isPinned = targetDoc && Doc.isDocPinned(targetDoc); return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> <div className="documentButtonBar-linker" - style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} + style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white", border: isPinned ? "black 1px solid " : "" }} onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> @@ -278,9 +278,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="documentButtonBar-button"> <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> - <div className="documentButtonBar-button"> + {DocumentLinksButton.StartLink || !Doc.UserDoc()["documentLinksButton-hideEnd"] ? <div className="documentButtonBar-button"> <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> - </div> + </div> : (null)} {/* <div className="documentButtonBar-button"> {this.templateButton} </div> diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 424a06431..5401623e8 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -24,8 +24,7 @@ $linkGap : 3px; .documentDecorations-rotation { pointer-events: auto; - // cursor: grabbing; - cursor: ns-resize; + cursor: alias; width: 10px; height: 10px; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index f16cb273b..f1169763e 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -27,6 +27,8 @@ import { GetEffectiveAcl } from '../../fields/util'; import { DocumentIcon } from './nodes/DocumentIcon'; import { render } from 'react-dom'; import { createLessThan } from 'typescript'; +import FormatShapePane from './collections/collectionFreeForm/FormatShapePane'; +import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; library.add(faCaretUp); library.add(faObjectGroup); @@ -59,6 +61,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; private _resizeUndo?: UndoManager.Batch; + private _rotateUndo?: UndoManager.Batch; private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border private _snapX = 0; _snapY = 0; // last snapped location of resize border private _prevX = 0; @@ -258,8 +261,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return false; } + @undoBatch @action onRotateDown = (e: React.PointerEvent): void => { + this._rotateUndo = UndoManager.StartBatch("rotatedown"); + setupMoveUpEvents(this, e, this.onRotateMove, this.onRotateUp, (e) => { }); this._prevX = e.clientX; this._prevY = e.clientY; @@ -281,6 +287,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> })); } + + @undoBatch @action onRotateMove = (e: PointerEvent, down: number[]): boolean => { @@ -332,12 +340,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onRotateUp = (e: PointerEvent) => { this._centerPoints = []; + this._rotateUndo?.end(); + this._rotateUndo = undefined; } _initialAutoHeight = false; _dragHeights = new Map<Doc, number>(); + @action onPointerDown = (e: React.PointerEvent): void => { @@ -346,8 +357,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const doc = Document(element.rootDoc); if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height) { this._inkDocs.push({ x: doc.x, y: doc.y, width: doc._width, height: doc._height }); + if (FormatShapePane.Instance._lock) { + doc._nativeHeight = doc._height; + doc._nativeWidth = doc._width; + } } - })); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { }); @@ -373,7 +387,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.SelectedDocuments()[0]; let thisPt = { thisX: e.clientX - this._offX, thisY: e.clientY - this._offY }; - const fixedAspect = first.layoutDoc._nativeWidth ? NumCast(first.layoutDoc._nativeWidth) / NumCast(first.layoutDoc._nativeHeight) : 0; + var fixedAspect = first.layoutDoc._nativeWidth ? NumCast(first.layoutDoc._nativeWidth) / NumCast(first.layoutDoc._nativeHeight) : 0; + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { + const doc = Document(element.rootDoc); + if (doc.type === DocumentType.INK && doc._width && doc._height && FormatShapePane.Instance._lock) { + fixedAspect = NumCast(doc._nativeWidth) / NumCast(doc._nativeHeight); + } + })); + + if (fixedAspect && (this._resizeHdlId === "documentDecorations-bottomRightResizer" || this._resizeHdlId === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles const project = (p: number[], a: number[], b: number[]) => { const atob = [b[0] - a[0], b[1] - a[1]]; @@ -544,7 +566,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc.data = new InkField(newPoints); } - + doc._nativeWidth = 0; + doc._nativeHeight = 0; } })); } @@ -588,7 +611,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } - const canDelete = SelectionManager.SelectedDocuments().map(docView => GetEffectiveAcl(docView.props.ContainingCollectionDoc)).some(permission => permission === AclAdmin || permission === AclEdit); + const canDelete = SelectionManager.SelectedDocuments().some(docView => { + const docAcl = GetEffectiveAcl(docView.props.Document); + const collectionAcl = GetEffectiveAcl(docView.props.ContainingCollectionDoc); + return [docAcl, collectionAcl].some(acl => [AclAdmin, AclEdit].includes(acl)); + }); const minimal = bounds.r - bounds.x < 100 ? true : false; const maximizeIcon = minimal ? ( <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"> @@ -639,10 +666,14 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight); } var offset = 0; + var rotButton = <></>; //make offset larger for ink to edit points if (seldoc.rootDoc.type === DocumentType.INK) { offset = 20; + rotButton = <div id="documentDecorations-rotation" title="rotate" className="documentDecorations-rotation" + onPointerDown={this.onRotateDown}> ⟲ </div>; } + return (<div className="documentDecorations" style={{ background: darkScheme }} > <div className="documentDecorations-background" style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", @@ -670,8 +701,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <Tooltip title={<><div className="dash-tooltip">Open Document In Tab</div></>} placement="top"><div className="documentDecorations-openInTab" onPointerDown={this.onMaximizeDown}> {SelectionManager.SelectedDocuments().length === 1 ? <FontAwesomeIcon icon="external-link-alt" className="documentView-minimizedIcon" /> : "..."} </div></Tooltip> - <div id="documentDecorations-rotation" className="documentDecorations-rotation" - onPointerDown={this.onRotateDown}> ⟲ </div> + {rotButton} <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topResizer" className="documentDecorations-resizer" diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index f9d060681..ec3e754fb 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -6,6 +6,8 @@ import { ObjectField } from '../../fields/ObjectField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import "./EditableView.scss"; import { DragManager } from '../util/DragManager'; +import { ComputedField } from '../../fields/ScriptField'; +import { FieldValue } from '../../fields/Types'; export interface EditableProps { /** @@ -52,6 +54,10 @@ export interface EditableProps { color?: string | undefined; onDrop?: any; placeholder?: string; + highlight?: boolean; + positions?: number[]; + search?: string; + bing?: () => string | undefined; } /** @@ -179,6 +185,34 @@ export class EditableView extends React.Component<EditableProps> { placeholder={this.props.placeholder} />; } + + returnHighlights() { + const results = []; + const contents = this.props.bing!(); + + if (contents !== undefined) { + if (this.props.positions !== undefined) { + const positions = this.props.positions; + const length = this.props.search!.length; + + // contents = String(this.props.contents.valueOf()); + + results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(0, this.props.positions[0]) : this.props.placeholder?.valueOf()}</span>); + positions.forEach((num, cur) => { + results.push(<span style={{ backgroundColor: "#FFFF00", fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num, num + length) : this.props.placeholder?.valueOf()}</span>); + let end = 0; + cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; + results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num + length, end) : this.props.placeholder?.valueOf()}</span>); + } + ); + } + return results; + } + else { + return <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>; + } + } + render() { if (this._editing && this.props.GetValue() !== undefined) { return this.props.sizeToContent ? @@ -193,11 +227,8 @@ export class EditableView extends React.Component<EditableProps> { ref={this._ref} style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} onClick={this.onClick} placeholder={this.props.placeholder}> - <span style={{ - fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, - color: this.props.contents ? this.props.color ? this.props.color : "black" : "grey" - }}> - {this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> + {this.props.highlight === undefined || this.props.positions === undefined || this.props.bing === undefined ? <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> + : this.returnHighlights()} </div> ); } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 7c0a8635e..30df7cf9a 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -676,6 +676,15 @@ export default class GestureOverlay extends Touchable { var left = Math.min(...xs); var bottom = Math.max(...ys); var top = Math.min(...ys); + const firstx = this._points[0].X; + const firsty = this._points[0].Y; + const lastx = this._points[this._points.length - 2].X; + const lasty = this._points[this._points.length - 2].Y; + var fourth = (lastx - firstx) / 4; + if (isNaN(fourth) || fourth === 0) { fourth = 0.01; } + var m = (lasty - firsty) / (lastx - firstx); + if (isNaN(m) || m === 0) { m = 0.01; } + const b = firsty - m * firstx; if (shape === "noRec") { return false; } @@ -725,6 +734,8 @@ export default class GestureOverlay extends Touchable { this._points.push({ X: left, Y: top }); this._points.push({ X: left, Y: top }); // this._points.push({ X: left, Y: top }); + + // this._points.push({ X: left, Y: top }); // this._points.push({ X: left, Y: top }); // this._points.push({ X: left, Y: top - 1 }); @@ -797,8 +808,33 @@ export default class GestureOverlay extends Touchable { break; case "line": - this._points.push({ X: left, Y: top }); - this._points.push({ X: right, Y: bottom }); + // const firstx = this._points[0].X; + // const firsty = this._points[0].Y; + // const lastx = this._points[this._points.length - 1].X; + // const lasty = this._points[this._points.length - 1].Y; + // const fourth = (lastx - firstx) / 4; + // const m = (lasty - firsty) / (lastx - firstx); + // const b = firsty - m * firstx; + this._points.push({ X: firstx, Y: firsty }); + this._points.push({ X: firstx, Y: firsty }); + + this._points.push({ X: firstx + fourth, Y: m * (firstx + fourth) + b }); + this._points.push({ X: firstx + fourth, Y: m * (firstx + fourth) + b }); + this._points.push({ X: firstx + fourth, Y: m * (firstx + fourth) + b }); + this._points.push({ X: firstx + fourth, Y: m * (firstx + fourth) + b }); + + this._points.push({ X: firstx + 2 * fourth, Y: m * (firstx + 2 * fourth) + b }); + this._points.push({ X: firstx + 2 * fourth, Y: m * (firstx + 2 * fourth) + b }); + this._points.push({ X: firstx + 2 * fourth, Y: m * (firstx + 2 * fourth) + b }); + this._points.push({ X: firstx + 2 * fourth, Y: m * (firstx + 2 * fourth) + b }); + + this._points.push({ X: firstx + 3 * fourth, Y: m * (firstx + 3 * fourth) + b }); + this._points.push({ X: firstx + 3 * fourth, Y: m * (firstx + 3 * fourth) + b }); + this._points.push({ X: firstx + 3 * fourth, Y: m * (firstx + 3 * fourth) + b }); + this._points.push({ X: firstx + 3 * fourth, Y: m * (firstx + 3 * fourth) + b }); + + this._points.push({ X: firstx + 4 * fourth, Y: m * (firstx + 4 * fourth) + b }); + this._points.push({ X: firstx + 4 * fourth, Y: m * (firstx + 4 * fourth) + b }); break; case "arrow": const x1 = left; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index b4a518326..0ea02e3cb 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -1,6 +1,6 @@ import { action } from "mobx"; import { DateField } from "../../fields/DateField"; -import { Doc, DocListCast } from "../../fields/Doc"; +import { Doc, DocListCast, AclEdit, AclAdmin } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; @@ -23,6 +23,7 @@ import PDFMenu from "./pdf/PDFMenu"; import { ContextMenu } from "./ContextMenu"; import GroupManager from "../util/GroupManager"; import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; +import { GetEffectiveAcl } from "../../fields/util"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -116,8 +117,18 @@ export default class KeyManager { return { stopPropagation: false, preventDefault: false }; } } - UndoManager.RunInBatch(() => - SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument?.(dv.props.Document)), "delete"); + + const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const selected = SelectionManager.SelectedDocuments().slice(); + UndoManager.RunInBatch(() => { + selected.map(dv => { + const effectiveAcl = GetEffectiveAcl(dv.props.Document); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete + recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); + dv.props.removeDocument?.(dv.props.Document); + } + }); + }, "delete"); SelectionManager.DeselectAll(); break; case "arrowleft": diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 8e3f72cee..4a77728b6 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -18,6 +18,8 @@ import { Doc } from "../../fields/Doc"; import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; import { action } from "mobx"; import { setupMoveUpEvents } from "../../Utils"; +import { undoBatch, UndoManager } from "../util/UndoManager"; + library.add(faPaintBrush); @@ -26,8 +28,12 @@ const InkDocument = makeInterface(documentSchema); @observer export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) { + private _controlUndo?: UndoManager.Batch; + public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } + + private analyzeStrokes = () => { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); @@ -52,6 +58,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume @action onControlDown = (e: React.PointerEvent, i: number): void => { setupMoveUpEvents(this, e, this.onControlMove, this.onControlup, (e) => { }); + this._controlUndo = UndoManager.StartBatch("DocDecs set radius"); this._prevX = e.clientX; this._prevY = e.clientY; this._controlNum = i; @@ -76,6 +83,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume this._prevX = 0; this._prevY = 0; this._controlNum = 0; + this._controlUndo?.end(); + this._controlUndo = undefined; } public static MaskDim = 50000; @@ -90,10 +99,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const top = Math.min(...ys) - strokeWidth / 2; const right = Math.max(...xs) + strokeWidth / 2; const bottom = Math.max(...ys) + strokeWidth / 2; - const width = right - left; - const height = bottom - top; - const scaleX = (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); - const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); + const width = Math.max(right - left); + const height = Math.max(1, bottom - top); + const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); + const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ""); const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, @@ -106,9 +115,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), "none", "none", "0", scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true); - var controlPoints: { X: number, Y: number, I: number }[] = []; - var handlePoints: { X: number, Y: number, I: number, dot1: number, dot2: number }[] = []; - var handleLine: { X1: number, Y1: number, X2: number, Y2: number, X3: number, Y3: number, dot1: number, dot2: number }[] = []; + const controlPoints: { X: number, Y: number, I: number }[] = []; + const handlePoints: { X: number, Y: number, I: number, dot1: number, dot2: number }[] = []; + const handleLine: { X1: number, Y1: number, X2: number, Y2: number, X3: number, Y3: number, dot1: number, dot2: number }[] = []; if (data.length >= 4) { for (var i = 0; i <= data.length - 4; i += 4) { controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); @@ -125,18 +134,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume } handleLine.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); - - } - if (data.length <= 4) { - handlePoints = []; - handleLine = []; - controlPoints = []; - for (var i = 0; i < data.length; i++) { - controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); - } - } - const dotsize = String(Math.min(width * scaleX, height * scaleY) / 40); + // if (data.length <= 4) { + // handlePoints = []; + // handleLine = []; + // controlPoints = []; + // for (var i = 0; i < data.length; i++) { + // controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); + // } + + // } + const dotsize = String(Math.max(width * scaleX, height * scaleY) / 40); const controls = controlPoints.map((pts, i) => diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index a2a9ceca5..97ed0a901 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -26,7 +26,7 @@ body { height: 100%; border-radius: inherit; position: inherit; - // background: inherit; + // background: inherit; } p { @@ -37,7 +37,7 @@ p { ::-webkit-scrollbar { -webkit-appearance: none; height: 8px; - width: 8px; + width: 8px; } ::-webkit-scrollbar-thumb { @@ -47,7 +47,7 @@ p { // button stuff button { - background: $dark-color; + background: black; outline: none; border: 0px; color: $light-color; diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index a57d22afd..f3fba82bc 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -21,7 +21,7 @@ // add nodes menu. Note that the + button is actually an input label, not an actual button. .mainView-docButtons { position: absolute; - bottom: 20px; + bottom: 35px; left: calc(100% + 5px); z-index: 1; } @@ -103,7 +103,8 @@ } .mainView-propertiesDragger { - background-color: rgb(140, 139, 139); + //background-color: rgb(140, 139, 139); + background-color: lightgrey; height: 55px; width: 17px; position: absolute; @@ -155,8 +156,8 @@ .mainView-menuPanel { width: 60px; - background-color: black; - height: 100%; + background-color: #121721; + height: calc(100% - 32px); //overflow-y: scroll; //overflow-x: hidden; @@ -165,6 +166,7 @@ padding: 7px; padding-left: 7px; width: 100%; + background: black; .mainView-menuPanel-button-wrap { width: 45px; @@ -213,12 +215,11 @@ .mainView-searchPanel { width: 100%; - height: 33px; + height: 32px; background-color: black; color: white; text-align: center; vertical-align: middle; - padding-top: 6px; } .mainView-mainDiv { @@ -297,6 +298,7 @@ position: absolute; z-index: 2; touch-action: none; + background-color: lightgrey; cursor: grab; .mainView-libraryHandle-icon { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 11ffcc734..3cb4fbb17 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -62,6 +62,7 @@ import { Hypothesis } from '../apis/hypothesis/HypothesisUtils'; import { undoBatch } from '../util/UndoManager'; import { WebBox } from './nodes/WebBox'; import * as ReactDOM from 'react-dom'; +import { SearchBox } from './search/SearchBox'; @observer export class MainView extends React.Component { @@ -81,8 +82,9 @@ export class MainView extends React.Component { @computed private get userDoc() { return Doc.UserDoc(); } @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } + @computed public get searchDoc() { return Cast(this.userDoc["search-panel"], Doc) as Doc; } - @observable public sidebarContent: any = this.userDoc?.["sidebar"]; + @observable public sidebarContent: any = this.userDoc?.sidebar; @observable public panelContent: string = "none"; @observable public showProperties: boolean = false; public isPointerDown = false; @@ -177,7 +179,8 @@ export class MainView extends React.Component { fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faChevronLeft, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper, fa.faDesktop, fa.faTrashRestore, fa.faUsers, fa.faWrench, fa.faCog, fa.faMap, fa.faBellSlash, fa.faExpandAlt, fa.faArchive, fa.faBezierCurve, fa.faCircle, - fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, faBuffer, fa.faExpand, fa.faUndo, fa.faSlidersH, fa.faAngleDoubleLeft); + fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, faBuffer, fa.faExpand, fa.faUndo, fa.faSlidersH, fa.faAngleDoubleLeft, fa.faAngleUp, + fa.faAngleDown, fa.faPlayCircle, fa.faClock, fa.faRocket, fa.faExchangeAlt, faBuffer); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -192,6 +195,20 @@ export class MainView extends React.Component { if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) { TimelineMenu.Instance.closeMenu(); } + if (targets && targets.length && SearchBox.Instance._searchbarOpen) { + let check = false; + const icon = "icon"; + targets.forEach((thing) => { + if (thing.className.toString() === "collectionSchemaView-table" || (thing as any)?.dataset[icon] === "filter" || thing.className.toString() === "beta" || thing.className.toString() === "collectionSchemaView-menuOptions-wrapper") { + check = true; + } + }); + if (check === false) { + SearchBox.Instance.closeSearch(); + } + } + + }); globalPointerUp = () => this.isPointerDown = false; @@ -317,6 +334,16 @@ export class MainView extends React.Component { defaultBackgroundColors = (doc: Opt<Doc>) => { if (this.panelContent === doc?.title) return "lightgrey"; + + if (doc?.type === DocumentType.COL) { + if (doc.title === "Basic Item Creators" || doc.title === "sidebar-tools" + || doc.title === "sidebar-recentlyClosed" || doc.title === "sidebar-catalog" + || doc.title === "Mobile Uploads" || doc.title === "COLLECTION_PROTO" + || doc.title === "Advanced Item Prototypes" || doc.title === "all Creators") { + return "lightgrey"; + } + return StrCast(Doc.UserDoc().defaultColor); + } if (this.darkScheme) { switch (doc?.type) { case DocumentType.FONTICON: return "white"; @@ -341,6 +368,7 @@ export class MainView extends React.Component { } } } + @computed get mainDocView() { return <DocumentView Document={this.mainContainer!} @@ -369,11 +397,12 @@ export class MainView extends React.Component { renderDepth={-1} />; } + @computed get dockingContent() { TraceMobx(); const mainContainer = this.mainContainer; const width = this.flyoutWidth + this.propertiesWidth(); - return <div className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)` }}> + return <div className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)`, height: `calc(100% - 32px)` }}> {!mainContainer ? (null) : this.mainDocView} </div>; } @@ -412,17 +441,17 @@ export class MainView extends React.Component { } sidebarScreenToLocal = () => new Transform(0, (CollectionMenu.Instance.Pinned ? -35 : 0), 1); //sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1); - mainContainerXf = () => this.sidebarScreenToLocal().translate(0, -this._buttonBarHeight); + mainContainerXf = () => this.sidebarScreenToLocal().translate(-55, 0); @computed get closePosition() { return 55 + this.flyoutWidth; } @computed get flyout() { if (!this.sidebarContent) return null; return <div className="mainView-libraryFlyout"> - <div className="mainView-contentArea" style={{ position: "relative", height: `100%`, width: "100%", overflow: "visible" }}> - {this.flyoutWidth > 0 ? <div className="mainView-libraryFlyout-close" + <div className="mainView-contentArea" style={{ position: "relative", height: `calc(100% - 32px)`, width: "100%", overflow: "visible" }}> + {/* {this.flyoutWidth > 0 ? <div className="mainView-libraryFlyout-close" onPointerDown={this.closeFlyout}> <FontAwesomeIcon icon="times" color="black" size="lg" /> - </div> : null} + </div> : null} */} <DocumentView Document={this.sidebarContent} @@ -450,6 +479,7 @@ export class MainView extends React.Component { ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} relative={true} + forcedBackgroundColor={() => "lightgrey"} /> </div> {this.docButtons}</div>; @@ -489,7 +519,7 @@ export class MainView extends React.Component { } - @action @undoBatch + @action closeFlyout = () => { this._lastButton && (this._lastButton.color = "white"); this._lastButton && (this._lastButton._backgroundColor = ""); @@ -500,7 +530,7 @@ export class MainView extends React.Component { get groupManager() { return GroupManager.Instance; } _lastButton: Doc | undefined; - @action @undoBatch + @action selectMenu = (button: Doc, str: string) => { this._lastButton && (this._lastButton.color = "white"); this._lastButton && (this._lastButton._backgroundColor = ""); @@ -529,7 +559,7 @@ export class MainView extends React.Component { return true; } - @action @undoBatch + @action closeProperties = () => { CurrentUserUtils.propertiesWidth = 0; } @@ -557,7 +587,8 @@ export class MainView extends React.Component { <div className="mainView-flyoutContainer" style={{ width: this.flyoutWidth }}> {this.flyoutWidth !== 0 ? <div className="mainView-libraryHandle" onPointerDown={this.onFlyoutPointerDown} - style={{ backgroundColor: 'lightgrey' }}> + //style={{ backgroundColor: '#8c8b8b' }} + > <span title="library View Dragger" style={{ width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", @@ -584,17 +615,16 @@ export class MainView extends React.Component { <div className="mainView-propertiesDragger" title="Properties View Dragger" onPointerDown={this.onPropertiesPointerDown} style={{ right: rightFlyout, top: "50%" }}> <div className="mainView-propertiesDragger-icon"> - <FontAwesomeIcon icon={this.propertiesIcon} color="white" size="sm" /> </div> + <FontAwesomeIcon icon={this.propertiesIcon} color="black" size="sm" /> </div> </div> } {this.propertiesWidth() < 10 ? (null) : - <div style={{ width: this.propertiesWidth() }}> {this.propertiesView} </div>} + <div style={{ width: this.propertiesWidth(), height: "calc(100% - 35px)" }}> {this.propertiesView} </div>} </div> </>; } @computed get mainContent() { - //const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0); const n = (CollectionMenu.Instance?.Pinned ? 1 : 0); const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`; const pinned = FormatShapePane.Instance?.Pinned; @@ -717,8 +747,32 @@ export class MainView extends React.Component { @computed get search() { return <div className="mainView-searchPanel"> - <div style={{ float: "left", marginLeft: "10px" }}>{Doc.CurrentUserEmail}</div> - <div>SEARCH GOES HERE</div> + {/* <div style={{ float: "left", marginLeft: "10px" }}>{Doc.CurrentUserEmail}</div> */} + <div><DocumentView Document={this.searchDoc} + DataDoc={undefined} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={this.addDocTabFunc} + pinToPres={emptyFunction} + rootSelected={returnTrue} + onClick={undefined} + backgroundColor={this.defaultBackgroundColors} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} + PanelWidth={this.getPWidth} + PanelHeight={this.getPHeight} + renderDepth={0} + focus={emptyFunction} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /></div> </div>; } @@ -763,12 +817,12 @@ export class MainView extends React.Component { <GroupManager /> <GoogleAuthenticationManager /> <DocumentDecorations /> - {/* {this.search} */} + {this.search} <CollectionMenu /> <FormatShapePane /> <div style={{ display: "none" }}><RichTextMenu key="rich" /></div> {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} - {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} + {DocumentLinksButton.EditLink ? <LinkMenu docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href} addDocTab={LinkDocPreview.LinkInfo.addDocTab} /> : (null)} diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 66ea2dbf8..19387f619 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -21,7 +21,9 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps> const dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1; const overlayOpacity = p.overlayDisplayedOpacity || 0.4; return !p.isDisplayed ? (null) : ( - <div style={{ pointerEvents: p.isDisplayed && p.interactive ? "all" : "none" }}> + <div style={{ + pointerEvents: p.isDisplayed && p.interactive ? "all" : "none" + }}> <div className={"dialogue-box"} style={{ diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss index 1cba252de..6199d34d0 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -21,9 +21,8 @@ $linkGap : 3px; .propertiesButtons-linkButton-empty, .propertiesButtons-linkButton-nonempty { height: 30px; - width: 30px; - border-radius: 5px; - opacity: 0.9; + width: 32px; + border-radius: 6px; pointer-events: auto; background-color: #121721; color: #fcfbf7; @@ -36,6 +35,7 @@ $linkGap : 3px; justify-content: center; align-items: center; margin-right: 10px; + margin-left: 3.5px; &:hover { background: $main-accent; @@ -68,23 +68,35 @@ $linkGap : 3px; padding-right: 5px; width: 25px; border-radius: 5px; - margin-right: 18px; + margin-right: 22px; margin-bottom: 8px; } +.propertiesButtons-title { + background: #121721; + color: white; + font-size: 6px; + width: 40px; + padding: 3px; + height: 13px; + border-radius: 7px; + text-transform: uppercase; + text-align: center; + margin-top: -4px; +} + .propertiesButtons-linker { height: 30px; - width: 30px; + width: 32px; text-align: center; - border-radius: 5px; + border-radius: 6px; pointer-events: auto; - // color: $dark-color; - // border: $dark-color 1px solid; - background-color: #252b33; + background-color: #121721; color: #fcfbf7; transition: 0.2s ease all; margin-right: 5px; padding-top: 5px; + margin-left: 3.5px; &:hover { background: $main-accent; diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index d46c03470..5c584d270 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -28,8 +28,8 @@ import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../fields/URLField'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { DocumentType } from '../documents/DocumentTypes'; -import { CollectionFreeFormView } from './collections/collectionFreeForm/CollectionFreeFormView'; import { InkField } from '../../fields/InkField'; +import { PresBox } from './nodes/PresBox'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -128,18 +128,22 @@ export class PropertiesButtons extends React.Component<{}, {}> { const targetDoc = this.selectedDoc; const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined; const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none"; - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}> - <div - className="propertiesButtons-linker" - style={{ animation }} - onClick={async () => { - await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - !published && runInAction(() => this.isAnimatingPulse = true); - PropertiesButtons.hasPushedHack = false; - targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; - }}> - <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} /> - </div></Tooltip>; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div>} placement="top"> + <div> + <div + className="propertiesButtons-linker" + style={{ animation }} + onClick={async () => { + await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + !published && runInAction(() => this.isAnimatingPulse = true); + PropertiesButtons.hasPushedHack = false; + targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "lg" : "sm"} /> + </div> + <div className="propertiesButtons-title">Google</div> + </div> + </Tooltip>; } @computed @@ -158,74 +162,124 @@ export class PropertiesButtons extends React.Component<{}, {}> { })(); return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip - title={<><div className="dash-tooltip">{title}</div></>}> - <div className="propertiesButtons-linker" - style={{ backgroundColor: this.pullColor }} - onPointerEnter={action(e => { - if (e.altKey) { - this.openHover = UtilityButtonState.OpenExternally; - } else if (e.shiftKey) { - this.openHover = UtilityButtonState.OpenRight; - } - })} - onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} - onClick={async e => { - const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; - if (e.shiftKey) { - e.preventDefault(); - let googleDoc = await Cast(dataDoc.googleDoc, Doc); - if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; - googleDoc = Docs.Create.WebDocument(googleDocUrl, options); - dataDoc.googleDoc = googleDoc; + title={<><div className="dash-tooltip">{title}</div></>} placement="top"> + <div> + <div className="propertiesButtons-linker" + style={{ backgroundColor: this.pullColor }} + onPointerEnter={action(e => { + if (e.altKey) { + this.openHover = UtilityButtonState.OpenExternally; + } else if (e.shiftKey) { + this.openHover = UtilityButtonState.OpenRight; } - CollectionDockingView.AddRightSplit(googleDoc); - } else if (e.altKey) { - e.preventDefault(); - window.open(googleDocUrl); - } else { - this.clearPullColor(); - PropertiesButtons.hasPulledHack = false; - targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; - dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); - } - }}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" - style={{ WebkitAnimation: animation, MozAnimation: animation }} - icon={(() => { - switch (this.openHover) { - default: - case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; - case UtilityButtonState.OpenRight: return "arrow-alt-circle-right"; - case UtilityButtonState.OpenExternally: return "share"; + })} + onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} + onClick={async e => { + const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; + if (e.shiftKey) { + e.preventDefault(); + let googleDoc = await Cast(dataDoc.googleDoc, Doc); + if (!googleDoc) { + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + googleDoc = Docs.Create.WebDocument(googleDocUrl, options); + dataDoc.googleDoc = googleDoc; + } + CollectionDockingView.AddRightSplit(googleDoc); + } else if (e.altKey) { + e.preventDefault(); + window.open(googleDocUrl); + } else { + this.clearPullColor(); + PropertiesButtons.hasPulledHack = false; + targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); } - })()} - /> - </div></Tooltip>; + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="lg" + color="black" + style={{ WebkitAnimation: animation, MozAnimation: animation }} + icon={(() => { + switch (this.openHover) { + default: + case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; + case UtilityButtonState.OpenRight: return "arrow-alt-circle-right"; + case UtilityButtonState.OpenExternally: return "share"; + } + })()} + /> + </div> + <div className="propertiesButtons-title" style={{ backgroundColor: "white", color: "black" }}>Fetch</div> + </div> + </Tooltip>; } @computed get pinButton() { const targetDoc = this.selectedDoc; const isPinned = targetDoc && Doc.isDocPinned(targetDoc); - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> - <div className="propertiesButtons-linker" - style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white" }} - onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" - /> - </div></Tooltip>; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> + <div> + <div className="propertiesButtons-linker" + style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white" }} + onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + </div> + + <div className="propertiesButtons-title" style={{ + backgroundColor: Doc.isDocPinned(targetDoc) ? "white" : "black", + color: Doc.isDocPinned(targetDoc) ? "black" : "white" + }} + >{Doc.isDocPinned(targetDoc) ? "Unpin" : "Pin"}</div> + </div> + </Tooltip>; + } + + @computed + get pinWithViewButton() { + const targetDoc = this.selectedDoc; + if (targetDoc) { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + } + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with this view"}</div></>} placement="top"> + <div> + <div className="propertiesButtons-linker" + onClick={e => { + if (targetDoc) { + DockedFrameRenderer.PinDoc(targetDoc, false); + const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeDoc.presPinView = true; + activeDoc.presPinViewX = x; + activeDoc.presPinViewY = y; + activeDoc.presPinViewScale = scale; + } + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + <div style={{ position: 'relative', fontSize: 25, fontWeight: 700, transform: 'translate(0, -20px)', color: 'rgba(250,250,250,0.5)' }}>V</div> + </div> + + <div className="propertiesButtons-title">{"View"}</div> + </div> + </Tooltip>; } + @computed get metadataButton() { //const view0 = this.view0; if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}> + return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>} placement="top"> <div className="propertiesButtons-linkFlyout"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={<MetadataEntryMenu docs={[this.selectedDoc]} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> - <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} + <div> + <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="lg" />} + </div> + <div className="propertiesButtons-title">Metadata</div> </div> </Flyout> </div></Tooltip>; @@ -264,12 +318,15 @@ export class PropertiesButtons extends React.Component<{}, {}> { Array.from(Object.values(Templates.TemplateList)).map(template => templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); return !docView ? (null) : - <Tooltip title={<><div className="dash-tooltip">Customize layout</div></>}> + <Tooltip title={<><div className="dash-tooltip">Customize layout</div></>} placement="top"> <div className="propertiesButtons-linkFlyout"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} //onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} content={<TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> - <div className={"propertiesButtons-linkButton-empty"} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + <div> + <div className={"propertiesButtons-linkButton-empty"} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="lg" />} + </div> + <div className="propertiesButtons-title">Layout</div> </div> </Flyout> </div></Tooltip>; @@ -292,12 +349,15 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get copyButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Tap or Drag to create an alias"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - ref={this._dragRef} - onPointerDown={this.onAliasButtonDown} - onClick={this.onCopy}> - {<FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="sm" />} + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Tap or Drag to create an alias"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + ref={this._dragRef} + onPointerDown={this.onAliasButtonDown} + onClick={this.onCopy}> + {<FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" />} + </div> + <div className="propertiesButtons-title">Alias</div> </div> </Tooltip>; } @@ -312,11 +372,19 @@ export class PropertiesButtons extends React.Component<{}, {}> { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{this.selectedDoc?.lockedPosition ? - "Unlock Position" : "Lock Position"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={this.onLock} > - {<FontAwesomeIcon className="documentdecorations-icon" - icon={BoolCast(this.selectedDoc?.lockedPosition) ? "unlock" : "lock"} size="sm" />} + "Unlock Position" : "Lock Position"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + style={{ backgroundColor: BoolCast(this.selectedDoc?.lockedPosition) ? "white" : "" }} + onPointerDown={this.onLock} > + {<FontAwesomeIcon className="documentdecorations-icon" + color={BoolCast(this.selectedDoc?.lockedPosition) ? "black" : "white"} + icon={BoolCast(this.selectedDoc?.lockedPosition) ? "unlock" : "lock"} size="lg" />} + </div> + <div className="propertiesButtons-title" style={{ + backgroundColor: BoolCast(this.selectedDoc?.lockedPosition) ? "white" : "black", + color: BoolCast(this.selectedDoc?.lockedPosition) ? "black" : "white" + }}>Position </div> </div> </Tooltip>; } @@ -325,15 +393,18 @@ export class PropertiesButtons extends React.Component<{}, {}> { get downloadButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Download Document"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={async () => { - if (this.selectedDocumentView?.props.Document) { - Doc.Zip(this.selectedDocumentView?.props.Document); - } - }}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="download" size="sm" />} + title={<><div className="dash-tooltip">{"Download Document"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={async () => { + if (this.selectedDocumentView?.props.Document) { + Doc.Zip(this.selectedDocumentView?.props.Document); + } + }}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="download" size="lg" />} + </div> + <div className="propertiesButtons-title"> downld </div> </div> </Tooltip>; } @@ -342,11 +413,14 @@ export class PropertiesButtons extends React.Component<{}, {}> { get deleteButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Delete Document"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={this.deleteDocument}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="trash-alt" size="sm" />} + title={<><div className="dash-tooltip">{"Delete Document"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={this.deleteDocument}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="trash-alt" size="lg" />} + </div> + <div className="propertiesButtons-title"> delete </div> </div> </Tooltip>; } @@ -361,15 +435,18 @@ export class PropertiesButtons extends React.Component<{}, {}> { get sharingButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Share Document"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={() => { - if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); - } - }}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="users" size="sm" />} + title={<><div className="dash-tooltip">{"Share Document"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={() => { + if (this.selectedDocumentView) { + SharingManager.Instance.open(this.selectedDocumentView); + } + }}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="users" size="lg" />} + </div> + <div className="propertiesButtons-title"> share </div> </div> </Tooltip>; } @@ -377,15 +454,19 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get onClickButton() { if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Choose onClick behavior</div></>}> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={this.onClickFlyout}> - <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="sm" />} - </div> - </Flyout> - </div></Tooltip>; + return <Tooltip title={<><div className="dash-tooltip">Choose onClick behavior</div></>} placement="top"> + <div> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={this.onClickFlyout}> + <div className={"propertiesButtons-linkButton-empty"} onPointerDown={e => e.stopPropagation()} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" />} + </div> + </Flyout> + </div> + <div className="propertiesButtons-title"> onclick </div> + </div> + </Tooltip>; } else { return null; } @@ -472,15 +553,18 @@ export class PropertiesButtons extends React.Component<{}, {}> { get googlePhotosButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={() => { - if (this.selectedDocumentView) { - GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log); - } - }}> - {<FontAwesomeIcon className="documentdecorations-icon" - icon="cloud-upload-alt" size="sm" />} + title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={() => { + if (this.selectedDocumentView) { + GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log); + } + }}> + {<FontAwesomeIcon className="documentdecorations-icon" + icon="cloud-upload-alt" size="lg" />} + </div> + <div className="propertiesButtons-title"> google </div> </div> </Tooltip>; } @@ -489,13 +573,19 @@ export class PropertiesButtons extends React.Component<{}, {}> { get clustersButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{this.selectedDoc?.useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: this.selectedDoc?.useClusters ? "#a0a0a0" : "" }} - onPointerDown={this.changeClusters}> - {<FontAwesomeIcon className="documentdecorations-icon" - color={this.selectedDoc?.useClusters ? "black" : "white"} - icon="braille" size="sm" />} + title={<><div className="dash-tooltip">{this.selectedDoc?.useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + style={{ backgroundColor: this.selectedDoc?.useClusters ? "white" : "" }} + onPointerDown={this.changeClusters}> + {<FontAwesomeIcon className="documentdecorations-icon" + color={this.selectedDoc?.useClusters ? "black" : "white"} + icon="braille" size="lg" />} + </div> + <div className="propertiesButtons-title" style={{ + backgroundColor: this.selectedDoc?.useClusters ? "white" : "black", + color: this.selectedDoc?.useClusters ? "black" : "white" + }}> clusters </div> </div> </Tooltip>; } @@ -514,13 +604,19 @@ export class PropertiesButtons extends React.Component<{}, {}> { get fitContentButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{this.selectedDoc?._fitToBox ? "Stop Fitting Content" : "Fit Content"}</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: this.selectedDoc?._fitToBox ? "#a0a0a0" : "" }} - onPointerDown={this.changeFitToBox}> - {<FontAwesomeIcon className="documentdecorations-icon" - color={this.selectedDoc?._fitToBox ? "black" : "white"} - icon="expand" size="sm" />} + title={<><div className="dash-tooltip">{this.selectedDoc?._fitToBox ? "Stop Fitting Content" : "Fit Content"}</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + style={{ backgroundColor: this.selectedDoc?._fitToBox ? "white" : "" }} + onPointerDown={this.changeFitToBox}> + {<FontAwesomeIcon className="documentdecorations-icon" + color={this.selectedDoc?._fitToBox ? "black" : "white"} + icon="expand" size="lg" />} + </div> + <div className="propertiesButtons-title" style={{ + backgroundColor: this.selectedDoc?._fitToBox ? "white" : "black", + color: this.selectedDoc?._fitToBox ? "black" : "white" + }}> {this.selectedDoc?._fitToBox ? "unfit" : "fit"} </div> </div> </Tooltip>; } @@ -541,11 +637,14 @@ export class PropertiesButtons extends React.Component<{}, {}> { get maskButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">Make Mask</div></>}> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={this.makeMask}> - {<FontAwesomeIcon className="documentdecorations-icon" - color="white" icon="paint-brush" size="sm" />} + title={<><div className="dash-tooltip">Make Mask</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={this.makeMask}> + {<FontAwesomeIcon className="documentdecorations-icon" + color="white" icon="paint-brush" size="lg" />} + </div> + <div className="propertiesButtons-title"> mask </div> </div> </Tooltip>; } @@ -553,13 +652,16 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get contextButton() { if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Show Context</div></>}> - <div className={"propertiesButtons-linkButton-empty"}> - <ParentDocSelector Document={this.selectedDoc} addDocTab={(doc, where) => { - where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : - this.selectedDocumentView?.props.addDocTab(doc, "onRight"); - return true; - }} /> + return <Tooltip title={<><div className="dash-tooltip">Show Context</div></>} placement="top"> + <div> + <div className={"propertiesButtons-linkButton-empty"}> + <ParentDocSelector Document={this.selectedDoc} addDocTab={(doc, where) => { + where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : + this.selectedDocumentView?.props.addDocTab(doc, "onRight"); + return true; + }} /> + </div> + <div className="propertiesButtons-title"> context </div> </div> </Tooltip>; } else { @@ -608,6 +710,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { {this.pinButton} </div> <div className="propertiesButtons-button"> + {this.pinWithViewButton} + </div> + <div className="propertiesButtons-button"> {this.copyButton} </div> <div className="propertiesButtons-button"> @@ -622,9 +727,6 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-button"> {this.onClickButton} </div> - {/* <div className="propertiesButtons-button"> - {this.contextButton} - </div> */} <div className="propertiesButtons-button"> {this.sharingButton} </div> @@ -652,6 +754,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-button" style={{ display: !isInk ? "none" : "" }}> {this.maskButton} </div> + <div className="propertiesButtons-button"> + {this.contextButton} + </div> </div> </div>; } diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx deleted file mode 100644 index 084f952a3..000000000 --- a/src/client/views/SearchDocBox.tsx +++ /dev/null @@ -1,428 +0,0 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -//import "./SearchBoxDoc.scss"; -import { Doc, DocListCast } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; -import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; -import { returnFalse, returnZero } from "../../Utils"; -import { Docs } from "../documents/Documents"; -import { SearchUtil } from "../util/SearchUtil"; -import { EditableView } from "./EditableView"; -import { ContentFittingDocumentView } from "./nodes/ContentFittingDocumentView"; -import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import { FilterBox } from "./search/FilterBox"; -import { SearchItem } from "./search/SearchItem"; -import React = require("react"); - -export interface RecProps { - documents: { preview: Doc, similarity: number }[]; - node: Doc; - -} - -library.add(faBullseye, faLink); -export const keyPlaceholder = "Query"; - -@observer -export class SearchDocBox extends React.Component<FieldViewProps> { - - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchDocBox, fieldKey); } - - // @observable private _display: boolean = false; - @observable private _pageX: number = 0; - @observable private _pageY: number = 0; - @observable private _width: number = 0; - @observable private _height: number = 0; - @observable.shallow private _docViews: JSX.Element[] = []; - // @observable private _documents: { preview: Doc, score: number }[] = []; - private previewDocs: Doc[] = []; - - constructor(props: FieldViewProps) { - super(props); - this.editingMetadata = this.editingMetadata || false; - //SearchBox.Instance = this; - this.resultsScrolled = this.resultsScrolled.bind(this); - } - - - @computed - private get editingMetadata() { - return BoolCast(this.props.Document.editingMetadata); - } - - private set editingMetadata(value: boolean) { - this.props.Document.editingMetadata = value; - } - - static readonly buffer = 20; - - componentDidMount() { - runInAction(() => { - this.query = StrCast(this.props.Document.searchText); - this.content = (Docs.Create.TreeDocument(DocListCast(Doc.GetProto(this.props.Document).data), { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query })); - - }); - if (this.inputRef.current) { - this.inputRef.current.focus(); - runInAction(() => { - this._searchbarOpen = true; - }); - } - } - - @observable - private content: Doc | undefined; - - @action - updateKey = async (newKey: string) => { - this.query = newKey; - if (newKey.length > 1) { - const newdocs = await this.getAllResults(this.query); - const things = newdocs.docs; - runInAction(() => { - this.content = Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query }); - }); - } - - - //this.keyRef.current && this.keyRef.current.setIsFocused(false); - //this.query.length === 0 && (this.query = keyPlaceholder); - return true; - } - - @computed - public get query() { - return StrCast(this.props.Document.query); - } - - public set query(value: string) { - this.props.Document.query = value; - } - - @observable private _searchString: string = ""; - @observable private _resultsOpen: boolean = false; - @observable private _searchbarOpen: boolean = false; - @observable private _results: [Doc, string[], string[]][] = []; - private _resultsSet = new Map<Doc, number>(); - @observable private _openNoResults: boolean = false; - @observable private _visibleElements: JSX.Element[] = []; - - private resultsRef = React.createRef<HTMLDivElement>(); - public inputRef = React.createRef<HTMLInputElement>(); - - private _isSearch: ("search" | "placeholder" | undefined)[] = []; - private _numTotalResults = -1; - private _endIndex = -1; - - - private _maxSearchIndex: number = 0; - private _curRequest?: Promise<any> = undefined; - - @action - getViews = async (doc: Doc) => { - const results = await SearchUtil.GetViewsOfDocument(doc); - let toReturn: Doc[] = []; - await runInAction(() => { - toReturn = results; - }); - return toReturn; - } - - @action.bound - onChange(e: React.ChangeEvent<HTMLInputElement>) { - this._searchString = e.target.value; - - this._openNoResults = false; - this._results = []; - this._resultsSet.clear(); - this._visibleElements = []; - this._numTotalResults = -1; - this._endIndex = -1; - this._curRequest = undefined; - this._maxSearchIndex = 0; - } - - enter = async (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - const newdocs = await this.getAllResults(this.query); - this.content = Docs.Create.TreeDocument(newdocs.docs, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs: "Results"` }); - } - } - - - @action - submitSearch = async () => { - let query = this._searchString; - query = FilterBox.Instance.getFinalQuery(query); - this._results = []; - this._resultsSet.clear(); - this._isSearch = []; - this._visibleElements = []; - FilterBox.Instance.closeFilter(); - - //if there is no query there should be no result - if (query === "") { - return; - } - else { - this._endIndex = 12; - this._maxSearchIndex = 0; - this._numTotalResults = -1; - await this.getResults(query); - } - - runInAction(() => { - this._resultsOpen = true; - this._searchbarOpen = true; - this._openNoResults = true; - this.resultsScrolled(); - }); - } - - getAllResults = async (query: string) => { - return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); - } - - private get filterQuery() { - const types = FilterBox.Instance.filterTypes; - const includeDeleted = FilterBox.Instance.getDataStatus(); - return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}" OR type_t:"extension"`).join(" ")})` : ""); - } - - - private NumResults = 25; - private lockPromise?: Promise<void>; - getResults = async (query: string) => { - if (this.lockPromise) { - await this.lockPromise; - } - this.lockPromise = new Promise(async res => { - while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { - this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: this.NumResults, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => { - - // happens at the beginning - if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { - this._numTotalResults = res.numFound; - } - - const highlighting = res.highlighting || {}; - const highlightList = res.docs.map(doc => highlighting[doc[Id]]); - const lines = new Map<string, string[]>(); - res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i])); - const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); - const highlights: typeof res.highlighting = {}; - docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); - const filteredDocs = FilterBox.Instance.filterDocsByType(docs); - runInAction(() => { - // this._results.push(...filteredDocs); - filteredDocs.forEach(doc => { - const index = this._resultsSet.get(doc); - const highlight = highlights[doc[Id]]; - const line = lines.get(doc[Id]) || []; - const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []; - if (index === undefined) { - this._resultsSet.set(doc, this._results.length); - this._results.push([doc, hlights, line]); - } else { - this._results[index][1].push(...hlights); - this._results[index][2].push(...line); - } - }); - }); - - this._curRequest = undefined; - })); - this._maxSearchIndex += this.NumResults; - - await this._curRequest; - } - this.resultsScrolled(); - res(); - }); - return this.lockPromise; - } - - collectionRef = React.createRef<HTMLSpanElement>(); - startDragCollection = async () => { - const res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString)); - const filtered = FilterBox.Instance.filterDocsByType(res.docs); - const docs = filtered.map(doc => { - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - }); - let x = 0; - let y = 0; - for (const doc of docs.map(d => Doc.Layout(d))) { - doc.x = x; - doc.y = y; - const size = 200; - const aspect = NumCast(doc._nativeHeight) / NumCast(doc._nativeWidth, 1); - if (aspect > 1) { - doc._height = size; - doc._width = size / aspect; - } else if (aspect > 0) { - doc._width = size; - doc._height = size * aspect; - } else { - doc._width = size; - doc._height = size; - } - x += 250; - if (x > 1000) { - x = 0; - y += 300; - } - } - //return Docs.Create.TreeDocument(docs, { _width: 200, _height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); - return Docs.Create.QueryDocument({ _width: 200, _height: 400, searchText: this._searchString, title: `Query Docs: "${this._searchString}"` }); - } - - @action.bound - openSearch(e: React.SyntheticEvent) { - e.stopPropagation(); - this._openNoResults = false; - FilterBox.Instance.closeFilter(); - this._resultsOpen = true; - this._searchbarOpen = true; - FilterBox.Instance._pointerTime = e.timeStamp; - } - - @action.bound - closeSearch = () => { - FilterBox.Instance.closeFilter(); - this.closeResults(); - this._searchbarOpen = false; - } - - @action.bound - closeResults() { - this._resultsOpen = false; - this._results = []; - this._resultsSet.clear(); - this._visibleElements = []; - this._numTotalResults = -1; - this._endIndex = -1; - this._curRequest = undefined; - } - - @action - resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { - if (!this.resultsRef.current) return; - const scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0; - const itemHght = 53; - const startIndex = Math.floor(Math.max(0, scrollY / itemHght)); - const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current.getBoundingClientRect().height / itemHght))); - - this._endIndex = endIndex === -1 ? 12 : endIndex; - - if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) { - this._visibleElements = [<div className="no-result">No Search Results</div>]; - return; - } - - if (this._numTotalResults <= this._maxSearchIndex) { - this._numTotalResults = this._results.length; - } - - // only hit right at the beginning - // visibleElements is all of the elements (even the ones you can't see) - else if (this._visibleElements.length !== this._numTotalResults) { - // undefined until a searchitem is put in there - this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - // indicates if things are placeholders - this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - } - - for (let i = 0; i < this._numTotalResults; i++) { - //if the index is out of the window then put a placeholder in - //should ones that have already been found get set to placeholders? - if (i < startIndex || i > endIndex) { - if (this._isSearch[i] !== "placeholder") { - this._isSearch[i] = "placeholder"; - this._visibleElements[i] = <div className="searchBox-placeholder" key={`searchBox-placeholder-${i}`}>Loading...</div>; - } - } - else { - if (this._isSearch[i] !== "search") { - let result: [Doc, string[], string[]] | undefined = undefined; - if (i >= this._results.length) { - this.getResults(this._searchString); - if (i < this._results.length) result = this._results[i]; - if (result) { - const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; - this._isSearch[i] = "search"; - } - } - else { - result = this._results[i]; - if (result) { - const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; - this._isSearch[i] = "search"; - } - } - } - } - } - if (this._maxSearchIndex >= this._numTotalResults) { - this._visibleElements.length = this._results.length; - this._isSearch.length = this._results.length; - } - } - - @computed - get resFull() { return this._numTotalResults <= 8; } - - @computed - get resultHeight() { return this._numTotalResults * 70; } - - render() { - const isEditing = this.editingMetadata; - return !this.content ? (null) : ( - <div style={{ pointerEvents: "all" }}> - <ContentFittingDocumentView {...this.props} - Document={this.content} - rootSelected={returnFalse} - bringToFront={returnFalse} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - NativeWidth={returnZero} - NativeHeight={returnZero} - parentActive={this.props.active} - ScreenToLocalTransform={this.props.ScreenToLocalTransform}> - </ContentFittingDocumentView> - <div - style={{ - position: "absolute", - right: 0, - width: 20, - height: 20, - background: "black", - pointerEvents: "all", - opacity: 1, - transition: "0.4s opacity ease", - zIndex: 99, - top: 0, - }} - title={"Add Metadata"} - onClick={action(() => this.editingMetadata = !this.editingMetadata)} - /> - <div className="editableclass" onKeyPress={this.enter} style={{ opacity: isEditing ? 1 : 0, pointerEvents: isEditing ? "auto" : "none", transition: "0.4s opacity ease", position: "absolute", top: 0, left: 0, height: 20, width: "-webkit-fill-available" }}> - <EditableView - contents={this.query} - SetValue={this.updateKey} - GetValue={() => ""} - /> - </div> - </div > - ); - } - -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 6ebd5103b..4204ef5bb 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -20,6 +20,82 @@ } } +.miniPres:hover { + opacity: 1; +} + +.miniPres { + position: absolute; + overflow: hidden; + right: 10; + top: 10; + opacity: 0.1; + transition: all 0.4s; + /* border: solid 1px; */ + color: white; + /* box-shadow: black 0.4vw 0.4vw 0.8vw; */ + + .miniPresOverlay { + display: grid; + grid-template-columns: auto auto auto auto auto auto auto auto; + grid-template-rows: 100%; + height: 100%; + justify-items: center; + align-items: center; + + .miniPres-button-text { + display: flex; + height: 20; + font-weight: 400; + min-width: 100%; + border-radius: 5px; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .miniPres-button-frame { + justify-self: center; + align-self: center; + align-items: center; + display: grid; + grid-template-columns: auto auto auto; + justify-content: space-around; + font-size: 11; + margin-left: 7; + width: 30; + height: 85%; + background-color: rgba(91, 157, 221, 0.4); + border-radius: 5px; + } + + .miniPres-divider { + width: 0.5px; + height: 80%; + border-right: solid 2px #5a5a5a; + } + + .miniPres-button { + display: flex; + height: 20; + min-width: 20; + border-radius: 100%; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .miniPres-button:hover { + background-color: #5a5a5a; + } + + .miniPres-button-text:hover { + background-color: #5a5a5a; + } + } +} + + .lm_title { margin-top: 3px; border-radius: 5px; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 533c8bffe..7e096fa37 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -30,6 +30,8 @@ import { SnappingManager } from '../../util/SnappingManager'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { listSpec } from '../../../fields/Schema'; import { clamp } from 'lodash'; +import { PresBox } from '../nodes/PresBox'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { InteractionUtils } from '../../util/InteractionUtils'; import { InkTool } from '../../../fields/InkField'; const _global = (window /* browser */ || global /* node */) as any; @@ -793,10 +795,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { let scaling = 1; if (!this.layoutDoc?._fitWidth && (!nativeW || !nativeH)) { scaling = 1; - } else if ((this.layoutDoc?._fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { + } else if (NumCast(this.layoutDoc!._nativeWidth) && ((this.layoutDoc?._fitWidth) || + this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth))) { scaling = this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); - } else { + } else if (nativeW && nativeH) { // if (this.layoutDoc!.type === DocumentType.PDF || this.layoutDoc!.type === DocumentType.WEB) { // if ((this.layoutDoc?._fitWidth) || // this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { @@ -807,7 +809,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { // } const wscale = this.panelWidth() / nativeW; scaling = wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; - } + } else scaling = 1; return scaling; } @@ -862,6 +864,31 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { return false; }), emptyFunction, emptyFunction); } + getCurrentFrame = (): number => { + const presTargetDoc = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null); + const currentFrame = Cast(presTargetDoc.currentFrame, "number", null); + return currentFrame; + } + renderMiniPres() { + return ( + <div className="miniPres" + style={{ width: 250, height: 30, background: '#323232' }} + > + {<div className="miniPresOverlay"> + <div className="miniPres-button" onClick={PresBox.Instance.back}><FontAwesomeIcon icon={"arrow-left"} /></div> + <div className="miniPres-button" onClick={() => PresBox.Instance.startAutoPres(PresBox.Instance.itemIndex)}><FontAwesomeIcon icon={PresBox.Instance.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div> + <div className="miniPres-button" onClick={PresBox.Instance.next}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="miniPres-divider"></div> + <div className="miniPres-button-text"> + Slide {PresBox.Instance.itemIndex + 1} / {PresBox.Instance.childDocs.length} + {PresBox.Instance.playButtonFrames} + </div> + <div className="miniPres-divider"></div> + <div className="miniPres-button-text" onClick={PresBox.Instance.updateMinimize}>EXIT</div> + </div>} + </div> + ); + } renderMiniMap() { return <div className="miniMap" style={{ width: this.returnMiniSize(), height: this.returnMiniSize(), background: StrCast(this._document!._backgroundColor, @@ -944,6 +971,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> {document._viewType === CollectionViewType.Freeform && !this._document?.hideMinimap ? this.renderMiniMap() : (null)} + {document._viewType === CollectionViewType.Freeform && this._document?.miniPres ? this.renderMiniPres() : (null)} </>; } diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index b8b72e756..f5c4299a9 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -2,7 +2,7 @@ @import "../_nodeModuleOverrides"; .collectionLinearView-outer { - overflow: hidden; + overflow: visible; height: 100%; .collectionLinearView { diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 0a316317f..b41cbe92d 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -368,6 +368,7 @@ } } + .numKeyframe { flex-direction: column; padding-top: 5px; diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index a7d2c07fa..53d2a136e 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -1,37 +1,35 @@ import React = require("react"); +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction, runInAction, Lambda } from "mobx"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt, Field } from "../../../fields/Doc"; -import { BoolCast, Cast, StrCast, NumCast } from "../../../fields/Types"; -import AntimodeMenu from "../AntimodeMenu"; -import "./CollectionMenu.scss"; -import { undoBatch } from "../../util/UndoManager"; -import { CollectionViewType, CollectionView, COLLECTION_BORDER_WIDTH } from "./CollectionView"; -import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; -import { DragManager } from "../../util/DragManager"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { List } from "../../../fields/List"; -import { EditableView } from "../EditableView"; +import { ColorState } from "react-color"; +import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Document } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; -import { listSpec } from "../../../fields/Schema"; -import FormatShapePane from "./collectionFreeForm/FormatShapePane"; -import { ActiveFillColor, SetActiveInkWidth, ActiveInkColor, SetActiveBezierApprox, SetActiveArrowEnd, SetActiveArrowStart, SetActiveFillColor, SetActiveInkColor } from "../InkingStroke"; -import GestureOverlay from "../GestureOverlay"; import { InkTool } from "../../../fields/InkField"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { Document } from "../../../fields/documentSchemas"; -import { SelectionManager } from "../../util/SelectionManager"; -import { DocumentView } from "../nodes/DocumentView"; -import { ColorState } from "react-color"; +import { List } from "../../../fields/List"; import { ObjectField } from "../../../fields/ObjectField"; -import RichTextMenu from "../nodes/formattedText/RichTextMenu"; import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { DocUtils } from "../../documents/Documents"; -import { Tooltip } from "@material-ui/core"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { DragManager } from "../../util/DragManager"; +import { SelectionManager } from "../../util/SelectionManager"; +import { undoBatch } from "../../util/UndoManager"; +import AntimodeMenu from "../AntimodeMenu"; +import { EditableView } from "../EditableView"; +import GestureOverlay from "../GestureOverlay"; +import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DocumentView } from "../nodes/DocumentView"; +import RichTextMenu from "../nodes/formattedText/RichTextMenu"; +import "./CollectionMenu.scss"; +import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; @observer export default class CollectionMenu extends AntimodeMenu { @@ -185,7 +183,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField ? ObjectField.MakeCopy(this.target._docFilters as any as ObjectField) : ""; }, }; - @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand, this._saveFilterCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } + @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } @computed get _stacking_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _masonry_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _schema_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._templateCommand, this._narrativeCommand]; } @@ -236,7 +234,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @computed get subChrome() { switch (this.props.type) { - default: + default: return this.otherSubChrome; + case CollectionViewType.Invalid: case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />); case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" {...this.props} />); case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" {...this.props} />); @@ -247,6 +246,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp case CollectionViewType.Docking: return (<CollectionDockingChrome key="collchrome" {...this.props} />); } } + + @computed get otherSubChrome() { + const docType = this.props.docView.Document.type; + switch (docType) { + default: return (null); + case DocumentType.IMG: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.PDF: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.INK: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.WEB: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.VID: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.RTF: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} isDoc={true} />); + } + } + + private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer?.(); @@ -392,7 +406,7 @@ export class CollectionDockingChrome extends React.Component<CollectionMenuProps } @observer -export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps & { isOverlay: boolean }> { +export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps & { isOverlay: boolean, isDoc?: boolean }> { public static Instance: CollectionFreeFormViewChrome; constructor(props: any) { super(props); @@ -567,6 +581,8 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu style={{ backgroundColor: this._colorBtn ? "121212" : "", zIndex: 1001 }}> {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */} <div className="color-previewII" style={{ backgroundColor: color }} /> + {color === "" ? <p style={{ fontSize: 45, color: "red", marginTop: -16, marginLeft: -5, position: "fixed" }}>☒</p> : ""} + </button>)} </div>; } @@ -580,40 +596,44 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu <button className="antimodeMenu-button" key={color} onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })} style={{ backgroundColor: this._fillBtn ? "121212" : "", zIndex: 1001 }}> - <div className="color-previewII" style={{ backgroundColor: color }}></div> + <div className="color-previewII" style={{ backgroundColor: color }}> + {color === "" ? <p style={{ fontSize: 45, color: "red", marginTop: -16, marginLeft: -5, position: "fixed" }}>☒</p> : ""} + </div> </button>)} </div>; } + @observable viewType = this.selectedDoc?._viewType; + render() { return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> - {this.props.docView.props.renderDepth !== 0 || this.isText ? (null) : + {this.props.docView.props.renderDepth !== 0 || this.isText || this.props.isDoc ? (null) : <Tooltip key="map" title={<div className="dash-tooltip">Toggle Mini Map</div>} placement="bottom"> - <div className="backKeyframe" onClick={this.miniMap}> + <div className="backKeyframe" onClick={this.miniMap} style={{ marginRight: "5px" }}> <FontAwesomeIcon icon={"map"} size={"lg"} /> </div> </Tooltip> } - {!this.isText ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> + {!this.isText && !this.props.isDoc ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> <div className="backKeyframe" onClick={this.prevKeyframe}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> </div> </Tooltip> : null} - {!this.isText ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> + {!this.isText && !this.props.isDoc ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> <div className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.document.editing = !this.document.editing)} > {NumCast(this.document.currentFrame)} </div> </Tooltip> : null} - {!this.isText ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> + {!this.isText && !this.props.isDoc ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> <div className="fwdKeyframe" onClick={this.nextKeyframe}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> </div> </Tooltip> : null} - {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText ? (null) : + {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText || this.props.isDoc ? (null) : <Tooltip key="hypothesis" title={<div className="dash-tooltip">Use Hypothesis</div>} placement="bottom"> <button className={"antimodeMenu-button"} key="hypothesis" style={{ @@ -625,7 +645,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </button> </Tooltip> } - {(!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating) && !this.isText ? + {!this.isText ? <> {this.drawButtons} {this.widthPicker} @@ -651,6 +671,24 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu getKeySuggestions = async (value: string): Promise<string[]> => { value = value.toLowerCase(); const docs = DocListCast(this.document[this.props.fieldKey]); + + if (Doc.UserDoc().noviceMode) { + if (docs instanceof Doc) { + const keys = Object.keys(docs).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || + key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || + (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + return keys.filter(key => key.toLowerCase().indexOf(value.toLowerCase()) > -1); + } else { + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || + key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || + key.indexOf("lastModified") >= 0 || (key[0].toUpperCase() === key[0] && + key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + return noviceKeys.filter(key => key.toLowerCase().indexOf(value.toLowerCase()) > -1); + } + } + if (docs instanceof Doc) { return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); } else { diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index eecaf7672..d11d6a5ba 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -32,6 +32,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import { DateField } from "../../../fields/DateField"; +import { RichTextField } from "../../../fields/RichTextField"; const path = require('path'); library.add(faExpand); @@ -193,7 +194,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc); const onItemDown = (e: React.PointerEvent) => { - fieldIsDoc && SetupDrag(this._focusRef, + //fieldIsDoc && + SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, this._document[props.fieldKey] instanceof Doc ? (doc: Doc | Doc[], target: Doc | undefined, addDoc: (newDoc: Doc | Doc[]) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); @@ -240,25 +242,72 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // <FontAwesomeIcon icon="expand" size="sm" /> // </div> // ); - trace(); - - - + const positions = []; + if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { + const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); + let term = ""; + if (cfield !== undefined) { + if (cfield.Text !== undefined) { + term = cfield.Text; + } + else if (StrCast(cfield)) { + term = StrCast(cfield); + } + else { + term = String(NumCast(cfield)); + } + } + term = term.toLowerCase(); + const search = StrCast(this.props.Document._searchString).toLowerCase(); + let start = term.indexOf(search); + let tally = 0; + if (start !== -1) { + positions.push(start); + } + while (start < contents.length && start !== -1) { + term = term.slice(start + search.length + 1); + tally += start + search.length + 1; + start = term.indexOf(search); + positions.push(tally + start); + } + if (positions.length > 1) { + positions.pop(); + } + } return ( - <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> + <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} + ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> <div className={className} ref={this._focusRef} onPointerDown={onItemDown} tabIndex={-1}> <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null} key={props.Document[Id]}> - - <EditableView + positions={positions.length > 0 ? positions : undefined} + search={StrCast(this.props.Document._searchString) ? StrCast(this.props.Document._searchString) : undefined} editing={this._isEditing} isEditingCallback={this.isEditingCallback} display={"inline"} contents={contents ? contents : type === "number" ? "0" : "undefined"} + highlight={positions.length > 0 ? true : undefined} //contents={StrCast(contents)} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} placeholder={"enter value"} + bing={() => { + const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); + if (cfield !== undefined) { + console.log(typeof (cfield)); + // if (typeof(cfield)===RichTextField) + const a = cfield as RichTextField; + if (a.Text !== undefined) { + return (a.Text); + } + else if (StrCast(cfield)) { + return StrCast(cfield); + } + else { + return String(NumCast(cfield)); + } + } + }} GetValue={() => { if (type === "number" && (contents === 0 || contents === "0")) { return "0"; @@ -272,6 +321,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const val = cscript !== undefined ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; return val; + } }} @@ -827,3 +877,69 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { ); } } + + +@observer +export class CollectionSchemaButtons extends CollectionSchemaCell { + + render() { + // const reference = React.createRef<HTMLDivElement>(); + // const onItemDown = (e: React.PointerEvent) => { + // (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : + // SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); + // }; + const doc = this.props.rowProps.original; + let buttons: JSX.Element | undefined = undefined; + buttons = <div style={{ + paddingTop: 8, + paddingLeft: 3, + }}><button onClick={() => { + doc.searchMatch = false; + setTimeout(() => doc.searchMatch = true, 0); + doc.searchIndex = NumCast(doc.searchIndex); + }} style={{ padding: 2, left: 77 }}> + <FontAwesomeIcon icon="arrow-up" size="sm" /> + </button> + <button onClick={() => { + { + doc.searchMatchAlt = false; + setTimeout(() => doc.searchMatchAlt = true, 0); + doc.searchIndex = NumCast(doc.searchIndex); + } + }} style={{ padding: 2 }}> + <FontAwesomeIcon icon="arrow-down" size="sm" /> + </button></div>; + const type = StrCast(doc.type); + if (type === "pdf") { + buttons = <div><button + style={{ + position: "relative", + height: 30, + width: 28, + left: 1, + }} + + onClick={() => { + doc.searchMatch = false; + setTimeout(() => doc.searchMatch = true, 0); + doc.searchIndex = NumCast(doc.searchIndex); + }}> + <FontAwesomeIcon icon="arrow-down" size="sm" /> + </button></div >; + } + else if (type !== "rtf") { + buttons = undefined; + } + + if (BoolCast(this.props.Document._searchDoc) === true) { + + } + else { + buttons = undefined; + } + return ( + <div> {buttons}</div> + ); + } +} + diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index efff4db98..e65adcf76 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -9,6 +9,8 @@ import { ColumnType } from "./CollectionSchemaView"; import { faFile } from "@fortawesome/free-regular-svg-icons"; import { SchemaHeaderField, PastelSchemaPalette } from "../../../fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; +import { Doc } from "../../../fields/Doc"; +import { StrCast } from "../../../fields/Types"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -76,14 +78,6 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe - - - - - - - - export interface ColumnMenuProps { columnField: SchemaHeaderField; // keyValue: string; @@ -288,9 +282,10 @@ export interface KeysDropdownProps { existingKeys: string[]; canAddNew: boolean; addNew: boolean; - onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + onSelect: (oldKey: string, newKey: string, addnew: boolean, filter?: string) => void; setIsEditing: (isEditing: boolean) => void; width?: string; + docs?: Doc[]; } @observer export class KeysDropdown extends React.Component<KeysDropdownProps> { @@ -306,10 +301,23 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @action onSelect = (key: string): void => { - this.props.onSelect(this._key, key, this.props.addNew); - this.setKey(key); + if (key.slice(0, this._key.length) === this._key && this._key !== key) { + const filter = key.slice(this._key.length - key.length); + this.props.onSelect(this._key, this._key, this.props.addNew, filter); + } + else { + this.props.onSelect(this._key, key, this.props.addNew); + this.setKey(key); + this._isOpen = false; + this.props.setIsEditing(false); + } + } + + @action + onSelect2 = (key: string): void => { + this._searchTerm = this._searchTerm.slice(0, this._key.length) + key; this._isOpen = false; - this.props.setIsEditing(false); + } @undoBatch @@ -371,22 +379,53 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { }); // if search term does not already exist as a group type, give option to create new group type - if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { - options.push(<div key={""} className="key-option" style={{ + if (this._key !== this._searchTerm.slice(0, this._key.length)) { + if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + options.push(<div key={""} className="key-option" style={{ + border: "1px solid lightgray", + width: this.props.width, maxWidth: this.props.width, overflowX: "hidden" + }} + onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> + Create "{this._searchTerm}" key</div>); + } + } + + return options; + } + + renderFilterOptions = (): JSX.Element[] | JSX.Element => { + if (!this._isOpen) return <></>; + const keyOptions: string[] = []; + const temp = this._searchTerm.slice(this._key.length); + this.props.docs?.forEach((doc) => { + const key = StrCast(doc[this._key]); + if (keyOptions.includes(key) === false && key.includes(temp)) { + keyOptions.push(key); + } + }); + + + const options = keyOptions.map(key => { + return <div key={key} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden" }} - onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> - Create "{this._searchTerm}" key</div>); - } + onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect2(key); }}>{key}</div>; + }); return options; } + render() { return ( - <div className="keys-dropdown" style={{ width: this.props.width, maxWidth: this.props.width, overflowX: "hidden" }}> - <input className="keys-search" //style={{ width: this.props.width, maxWidth: "1000" }} + <div className="keys-dropdown" style={{ zIndex: 10, width: this.props.width, maxWidth: this.props.width }}> + {this._key === this._searchTerm.slice(0, this._key.length) ? + <div style={{ position: "absolute", marginLeft: "4px", marginTop: "3", color: "grey", pointerEvents: "none", lineHeight: 1.15 }}> + {this._key} + </div> + : undefined} + <input className="keys-search" style={{ width: "100%" }} ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} onChange={e => this.onChange(e.target.value)} onClick={(e) => { @@ -395,10 +434,11 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { }} onFocus={this.onFocus} onBlur={this.onBlur}></input> <div className="keys-options-wrapper" style={{ backgroundColor: "white", - width: this.props.width, maxWidth: this.props.width, overflowX: "hidden" + width: this.props.width, maxWidth: this.props.width, }} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> - {this.renderOptions()} + {this._key === this._searchTerm.slice(0, this._key.length) ? + this.renderFilterOptions() : this.renderOptions()} </div> </div > ); diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index b77173b25..dade4f2f2 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -209,6 +209,14 @@ export class MovableRow extends React.Component<MovableRowProps> { return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); } + @action + onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + console.log("yes"); + if (e.key === "Backspace" || e.key === "Delete") { + undoBatch(() => this.props.removeDoc(this.props.rowInfo.original)); + } + } + render() { const { children = null, rowInfo } = this.props; if (!rowInfo) { @@ -227,14 +235,14 @@ export class MovableRow extends React.Component<MovableRowProps> { if (this.props.rowWrapped) className += " row-wrapped"; return ( - <div className={className} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> - <div className="collectionSchema-row-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - <ReactTableDefaults.TrComponent> - <div className="row-dragger"> + <div className={className} onKeyPress={this.onKeyDown} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> + <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown} > + {/* <div className="row-dragger"> <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "onRight")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> - </div> + </div> */} {children} </ReactTableDefaults.TrComponent> </div> diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 5226a60f1..ba0a259c5 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -6,7 +6,7 @@ border-style: solid; border-radius: $border-radius; box-sizing: border-box; - position: absolute; + position: relative; top: 0; width: 100%; height: 100%; @@ -25,7 +25,6 @@ .collectionSchemaView-tableContainer { width: 100%; height: 100%; - overflow: scroll; } .collectionSchemaView-dividerDragger { @@ -59,9 +58,7 @@ } .rt-thead { - width: calc(100% - 52px); - margin-left: 50px; - + width: 100%; z-index: 100; overflow-y: visible; @@ -96,7 +93,7 @@ } .rt-tbody { - width: calc(100% - 2px); + width: 100%; direction: rtl; overflow: visible; } @@ -164,16 +161,6 @@ .collectionSchema-col { height: 100%; - - .collectionSchema-col-wrapper { - &.col-before { - border-left: 2px solid red; - } - - &.col-after { - border-right: 2px solid red; - } - } } @@ -297,7 +284,6 @@ button.add-column { background-color: white; border: 1px solid lightgray; padding: 2px 3px; - overflow-x: hidden; &:not(:first-child) { border-top: 0; @@ -525,14 +511,21 @@ button.add-column { .collectionSchemaView-table { width: 100%; height: 100%; - overflow: visible; } .reactTable-sub { padding: 10px 30px; background-color: rgb(252, 252, 252); - width: calc(100% - 50px); - margin-left: 50px; + width: 100%; + + .rt-thead { + display:none; + } + .collectionSchemaView-table{ + border: solid 1px; + overflow: hidden; + } + .row-dragger { background-color: rgb(252, 252, 252); @@ -567,7 +560,6 @@ button.add-column { text-transform: uppercase; cursor: pointer; font-size: 10.5px; - padding: 10px; margin-left: 50px; margin-top: 10px; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index f67e049fd..a003de0d3 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -4,27 +4,26 @@ import { faCog, faPlus, faSortDown, faSortUp, faTable } from '@fortawesome/free- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, untracked } from "mobx"; import { observer } from "mobx-react"; +import Measure from "react-measure"; import { Resize } from "react-table"; import "react-table/react-table.css"; import { Doc } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; -import { SchemaHeaderField, PastelSchemaPalette } from "../../../fields/SchemaHeaderField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { Docs, DocumentOptions } from "../../documents/Documents"; +import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; +import { Cast, NumCast } from "../../../fields/Types"; +import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents } from "../../../Utils"; +import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import '../DocumentDecorations.scss'; +import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; import { KeysDropdown } from "./CollectionSchemaHeaders"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { setupMoveUpEvents, emptyFunction, returnZero, returnOne, returnFalse } from "../../../Utils"; -import { SnappingManager } from "../../util/SnappingManager"; -import Measure from "react-measure"; import { SchemaTable } from "./SchemaTable"; -import { TraceMobx } from "../../../fields/util"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -170,6 +169,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { const columns = this.columns; + columns.forEach(col => col.setDesc(undefined)); + const index = columns.findIndex(c => c.heading === columnField.heading); const column = columns[index]; column.setDesc(descending); @@ -310,7 +311,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @undoBatch @action - changeColumns = (oldKey: string, newKey: string, addNew: boolean) => { + changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => { const columns = this.columns; if (columns === undefined) { this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); @@ -325,6 +326,20 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { column.setHeading(newKey); columns[index] = column; this.columns = columns; + if (filter) { + Doc.setDocFilter(this.props.Document, newKey, filter, "match"); + if (this.props.Document.selectedDoc !== undefined) { + const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; + Doc.setDocFilter(doc, newKey, filter, "match"); + } + } + else { + this.props.Document._docFilters = undefined; + if (this.props.Document.selectedDoc !== undefined) { + const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; + doc._docFilters = undefined; + } + } } } } @@ -591,6 +606,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } } + + + onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { + } render() { TraceMobx(); const menuContent = this.renderMenuContent; @@ -608,14 +627,15 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>} </Measure> </div>; - return <div className="collectionSchemaView-container" style={{ + overflow: this.props.overflow === true ? "auto" : undefined, pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, - width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%" + width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", }} > <div className="collectionSchemaView-tableContainer" - style={{ width: `calc(100% - ${this.previewWidth()}px)` }} + style={{ backgroundColor: "white", width: `calc(100% - ${this.previewWidth()}px)` }} + onKeyPress={this.onKeyPress} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index cca78cf9f..fe3d57bdb 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -184,7 +184,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) if (found) { const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); - smoothScroll(500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll(doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } afterFocus && setTimeout(() => { if (afterFocus?.()) { } @@ -287,19 +287,31 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } }); if (super.onInternalDrop(e, de)) { - const newDoc = de.complete.docDragData.droppedDocuments[0]; + const newDocs = de.complete.docDragData.droppedDocuments; const docs = this.childDocList; if (docs) { - if (targInd === -1) targInd = docs.length; - else targInd = docs.indexOf(this.filteredChildren[targInd]); - const srcInd = docs.indexOf(newDoc); - docs.splice(srcInd, 1); - docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, newDoc); + newDocs.map((doc, i) => { + console.log(doc.title); + if (i === 0) { + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(this.filteredChildren[targInd]); + const srcInd = docs.indexOf(doc); + docs.splice(srcInd, 1); + docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc); + } else if (i < (newDocs.length / 2)) { //glr: for some reason dragged documents are duplicated + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(newDocs[0]) + 1; + const srcInd = docs.indexOf(doc); + docs.splice(srcInd, 1); + docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc); + } + }); } } } return false; } + @undoBatch @action onExternalDrop = async (e: React.DragEvent): Promise<void> => { diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 4042a070d..f193a9787 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -12,6 +12,7 @@ import { NumCast, StrCast, Cast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; import { Docs, DocUtils } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -346,7 +347,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; const chromeStatus = this.props.parent.props.Document._chromeStatus; - + const type = this.props.parent.props.Document.type; return <> {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} { @@ -366,9 +367,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {this.props.parent.children(this.props.docList, uniqueHeadings.length)} {singleColumn ? (null) : this.props.parent.columnDragger} </div> - {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled' && type !== DocumentType.PRES) ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns }}> + style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 70 }}> <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> </div> : null} </div> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index ce3654cf2..4d9ed358b 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,7 +1,7 @@ import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; import { basename } from 'path'; import CursorField from "../../../fields/CursorField"; -import { Doc, Opt, Field } from "../../../fields/Doc"; +import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; @@ -112,9 +112,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } @computed get childDocs() { + let rawdocs: (Doc | Promise<Doc>)[] = DocListCast(this.props.Document._searchDocs); - let rawdocs: (Doc | Promise<Doc>)[] = []; - if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; + if (rawdocs.length !== 0) { + } else if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; rawdocs = [this.dataField]; } else if (Cast(this.dataField, listSpec(Doc), null)) { // otherwise, if the collection data is a list, then use it. rawdocs = Cast(this.dataField, listSpec(Doc), null); @@ -126,11 +127,66 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc); + const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); + let childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + + const searchDocs = DocListCast(this.props.Document._searchDocs); + // if (searchDocs !== undefined && searchDocs.length > 0) { + // let newdocs: Doc[] = []; + // childDocs.forEach((el) => { + // searchDocs.includes(el) ? newdocs.push(el) : undefined; + // }); + // childDocs = newdocs; + // } + + let docsforFilter: Doc[] = childDocs; + if (searchDocs !== undefined && searchDocs.length > 0) { + docsforFilter = []; + // let newdocs: Doc[] = []; + // let newarray: Doc[] = []; + //while (childDocs.length > 0) { + //newarray = []; + childDocs.forEach((d) => { + if (d.data !== undefined) { + console.log(d); + let newdocs = DocListCast(d.data); + if (newdocs.length > 0) { + let vibecheck: boolean | undefined = undefined; + let newarray: Doc[] = []; + while (newdocs.length > 0) { + newarray = []; + newdocs.forEach((t) => { + if (d.data !== undefined) { + const newdocs = DocListCast(t.data); + newdocs.forEach((newdoc) => { + newarray.push(newdoc); + }); + } + if (searchDocs.includes(t)) { + vibecheck = true; + } + }); + newdocs = newarray; + } + if (vibecheck === true) { + docsforFilter.push(d); + } + } + } + if (searchDocs.includes(d)) { + docsforFilter.push(d); + } + }); + //childDocs = newarray; + //} + } + childDocs = docsforFilter; + + const docFilters = this.docFilters(); - const viewSpecScript = ScriptCast(this.props.Document.viewSpecScript); const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, docFilters, docRangeFilters, viewSpecScript); + return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, this.docFilters(), docRangeFilters, viewSpecScript); } @action @@ -362,7 +418,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: ...options, title: cleanedUri, _width: 400, - // _height: 315, + _height: 315, _nativeWidth: 850, _nativeHeight: 962, UseCors: true diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 50f0534bd..c9bf82406 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -22,7 +22,7 @@ ul { list-style: none; padding-left: 20px; - margin-bottom: 1px;// otherwise vertical scrollbars may pop up for no apparent reason.... + margin-bottom: 1px; // otherwise vertical scrollbars may pop up for no apparent reason.... } @@ -35,7 +35,7 @@ width: 15px; color: $intermediate-color; margin-top: 3px; - transform: scale(1.3, 1.3); + transform: scale(1.3, 1.3); border: #80808030 1px solid; border-radius: 4px; } @@ -67,8 +67,10 @@ margin-left: 3px; display: none; } + .collectionTreeView-keyHeader:hover { background: #797777; + cursor: pointer; } .collectionTreeView-subtitle { @@ -89,8 +91,10 @@ height: 17px; width: 15px; } + .treeViewItem-openRight:hover { background: #797777; + cursor: pointer; } .treeViewItem-border { @@ -106,10 +110,12 @@ .editableView-container-editing-oneLine { min-width: 15px; } + .documentView-node-topmost { width: unset; } - > svg { + + >svg { display: none; } @@ -119,7 +125,8 @@ .collectionTreeView-keyHeader { display: inherit; } - > svg { + + >svg { display: inherit; } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 89034a0c0..4d1cb670c 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { TraceMobx, GetEffectiveAcl, SharingPermissions } from '../../../fields/util'; +import { TraceMobx, GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../../fields/util'; import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -74,7 +74,7 @@ export enum CollectionViewType { Pile = "pileup" } export interface CollectionViewCustomProps { - filterAddDocument: (doc: Doc | Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) + filterAddDocument?: (doc: Doc | Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection childLayoutString?: string; // specify a layout string to use for children of the collection childOpacity?: () => number; @@ -88,6 +88,7 @@ export interface CollectionRenderProps { active: () => boolean; whenActiveChanged: (isActive: boolean) => void; PanelWidth: () => number; + PanelHeight: () => number; ChildLayoutTemplate?: () => Doc; ChildLayoutString?: string; } @@ -147,16 +148,14 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return false; } else { - // if (this.props.Document[AclSym]) { - // // change so it only adds if more restrictive - // added.forEach(d => { - // // const dataDoc = d[DataSym]; - // for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - // // key.substring(4).replace("_", ".") !== Doc.CurrentUserEmail && distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); - // distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); - // } - // }); - // } + if (this.props.Document[AclSym]) { + added.forEach(d => { + for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + if (d.author === Doc.CurrentUserEmail && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); + else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + } + }); + } if (effectiveAcl === AclAddonly) { added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); @@ -523,6 +522,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus </div> <div className="collectionTimeView-tree" key="tree"> <CollectionTreeView + PanelPosition={""} Document={facetCollection} DataDoc={facetCollection} fieldKey={`${this.props.fieldKey}-filter`} @@ -574,6 +574,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus active: this.active, whenActiveChanged: this.whenActiveChanged, PanelWidth: this.bodyPanelWidth, + PanelHeight: this.props.PanelHeight, ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; @@ -582,7 +583,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return (<div className={"collectionView"} onContextMenu={this.onContextMenu} style={{ pointerEvents: this.props.Document.isBackground ? "none" : undefined, boxShadow }}> {this.showIsTagged()} - <div className="collectionView-facetCont" style={{ width: `calc(100% - ${this.facetWidth()}px)` }}> + <div className="collectionView-facetCont" style={{ display: this.props.PanelPosition === "absolute" ? "flex" : "", justifyContent: this.props.PanelPosition === "absolute" ? "center" : "", width: `calc(100% - ${this.facetWidth()}px)` }}> {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} </div> {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index 7e2840c2c..a974c5496 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -11,22 +11,22 @@ import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../fields/ScriptField"; -import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../fields/Types"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; import { Docs, DocumentOptions } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import '../DocumentDecorations.scss'; -import { CellProps, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaDateCell } from "./CollectionSchemaCells"; +import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; +import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells"; import { CollectionSchemaAddColumnHeader, KeysDropdown } from "./CollectionSchemaHeaders"; import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; -import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { emptyFunction, returnZero, returnOne, returnFalse, returnEmptyFilter, emptyPath } from "../../../Utils"; -import { TouchScrollableMenuItem } from "../TouchScrollableMenu"; enum ColumnType { @@ -128,7 +128,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @computed get sorted(): SortingRule[] { return this.props.columns.reduce((sorted, shf) => { - shf.desc && sorted.push({ id: shf.heading, desc: shf.desc }); + shf.desc !== undefined && sorted.push({ id: shf.heading, desc: shf.desc }); return sorted; }, [] as SortingRule[]); } @@ -160,7 +160,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const focusedCol = this._focusedCell.col; const isEditable = !this.props.headerIsEditing; - if (this.childDocs.reduce((found, doc) => found || doc.type === "collection", false)) { + if (this.childDocs.reduce((found, doc) => found || doc.type === DocumentType.COL, false)) { columns.push( { expander: true, @@ -188,7 +188,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { addNew={false} onSelect={this.props.changeColumns} setIsEditing={this.props.setHeaderIsEditing} - + docs={this.props.childDocs} // try commenting this out width={"100%"} />; @@ -216,21 +216,21 @@ export class SchemaTable extends React.Component<SchemaTableProps> { className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: "2px", - display: "flex" + display: "flex", cursor: "default", height: "100%", }}> - <FontAwesomeIcon icon={icon} size="lg" style={{ display: "inline", paddingLeft: "7px" }} /> - <div className="keys-dropdown" - style={{ display: "inline", zIndex: 1000 }}> - {keysDropdown} - </div> + <FontAwesomeIcon icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px" }} /> + {/* <div className="keys-dropdown" + style={{ display: "inline", zIndex: 1000 }}> */} + {keysDropdown} + {/* </div> */} <div onClick={e => this.changeSorting(col)} - style={{ paddingRight: "6px", marginLeft: "4px", display: "inline" }}> - <FontAwesomeIcon icon={sortIcon} size="sm" /> - </div> - <div onClick={e => this.props.openHeader(col, e.clientX, e.clientY)} - style={{ float: "right", paddingRight: "6px" }}> - <FontAwesomeIcon icon={"cog"} size="sm" /> + style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit" }}> + <FontAwesomeIcon icon={sortIcon} size="lg" /> </div> + {/* <div onClick={e => this.props.openHeader(col, e.clientX, e.clientY)} + style={{ float: "right", paddingRight: "6px", zIndex: 1, background: "inherit" }}> + <FontAwesomeIcon icon={"compass"} size="sm" /> + </div> */} </div>; return { @@ -283,13 +283,62 @@ export class SchemaTable extends React.Component<SchemaTableProps> { Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, accessor: (doc: Doc) => 0, id: "add", - Cell: (rowProps: CellInfo) => <></>, + Cell: (rowProps: CellInfo) => { + const rowIndex = rowProps.index; + const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); + const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; + const props: CellProps = { + row: rowIndex, + col: columnIndex, + rowProps: rowProps, + isFocused: isFocused, + changeFocusedCellByIndex: this.changeFocusedCellByIndex, + CollectionView: this.props.CollectionView, + ContainingCollection: this.props.ContainingCollectionView, + Document: this.props.Document, + fieldKey: this.props.fieldKey, + renderDepth: this.props.renderDepth, + addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, + moveDocument: this.props.moveDocument, + setIsEditing: this.setCellIsEditing, + isEditable: isEditable, + setPreviewDoc: this.props.setPreviewDoc, + setComputed: this.setComputed, + getField: this.getField, + showDoc: this.showDoc, + }; + + return <CollectionSchemaButtons {...props} />; + }, width: 28, resizable: false }); return columns; } + + + @action + nextHighlight = (e: React.MouseEvent, doc: Doc) => { + e.preventDefault(); + e.stopPropagation(); + doc.searchMatch = false; + console.log(doc.searchMatch); + setTimeout(() => doc.searchMatch = true, 0); + console.log(doc.searchMatch); + + doc.searchIndex = NumCast(doc.searchIndex); + } + + @action + nextHighlight2 = (doc: Doc) => { + + doc.searchMatchAlt = false; + setTimeout(() => doc.searchMatchAlt = true, 0); + doc.searchIndex = NumCast(doc.searchIndex); + } + constructor(props: SchemaTableProps) { super(props); // convert old schema columns (list of strings) into new schema columns (list of schema header fields) @@ -574,7 +623,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return <div className="collectionSchemaView-table" onPointerDown={this.props.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> + {StrCast(this.props.Document.type) !== "search" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> + : undefined} {!this._showDoc ? (null) : <div className="collectionSchemaView-documentPreview" //onClick={() => { this.onOpenClick(); }} style={{ diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index bfe569853..3a2979696 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -54,15 +54,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const bfield = afield === "anchor1" ? "anchor2" : "anchor1"; // really hacky stuff to make the LinkAnchorBox display where we want it to: - // if there's an element in the DOM with a classname containing the link's id and a targetids attribute containing the other end of the link, + // if there's an element in the DOM with a classname containing the link's id and a data-targetids attribute containing the other end of the link, // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right // otherwise, we just use the computed nearest point on the document boundary to the target Document const linkId = this.props.LinkDocs[0][Id]; // this link's Id const AanchorId = (this.props.LinkDocs[0][afield] as Doc)[Id]; // anchor a's id const BanchorId = (this.props.LinkDocs[0][bfield] as Doc)[Id]; // anchor b's id const linkEles = Array.from(window.document.getElementsByClassName(linkId)); - const targetAhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(AanchorId)); - const targetBhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(BanchorId)); + const targetAhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(AanchorId)); + const targetBhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(BanchorId)); if (!targetBhyperlink) { this.props.A.rootDoc[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; this.props.A.rootDoc[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 92aee3776..2b07c4efb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -13,16 +13,153 @@ } .collectionfreeformview-viewdef { - > .collectionFreeFormDocumentView-container { + >.collectionFreeFormDocumentView-container { pointer-events: none; + .contentFittingDocumentDocumentView-previewDoc { pointer-events: all; } } + + svg.presPaths { + position: absolute; + z-index: 100000; + overflow: visible; + } + + svg.presPaths-hidden { + display: none; + } } .collectionfreeformview-none { touch-action: none; + + svg.presPaths { + position: absolute; + z-index: 100000; + overflow: visible; + } + + svg.presPaths-hidden { + display: none; + } +} + +.pathOrder { + position: absolute; + z-index: 200000; + + .pathOrder-frame { + position: absolute; + width: 40; + text-align: center; + font-size: 30; + background-color: #69a6db; + font-family: Roboto; + font-weight: 300; + } +} + +.progressivizeButton { + position: absolute; + display: grid; + grid-template-columns: auto 20px auto; + transform: translate(-105%, 0); + align-items: center; + border: black solid 1px; + border-radius: 3px; + justify-content: center; + width: 40; + z-index: 30000; + height: 20; + overflow: hidden; + background-color: #d5dce2; + transition: all 1s; + + .progressivizeButton-prev:hover { + color: #5a9edd; + } + + .progressivizeButton-frame { + justify-self: center; + text-align: center; + width: 15px; + } + + .progressivizeButton-next:hover { + color: #5a9edd; + } +} + +.resizable { + background: rgba(0, 0, 0, 0.2); + width: 100px; + height: 100px; + position: absolute; + top: 100px; + left: 100px; + + .resizers { + width: 100%; + height: 100%; + border: 3px solid #69a6db; + box-sizing: border-box; + + .resizer { + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + /*magic to turn square into circle*/ + background: white; + border: 3px solid #69a6db; + } + + .resizer.top-left { + left: -3px; + top: -3px; + cursor: nwse-resize; + /*resizer cursor*/ + } + + .resizer.top-right { + right: -3px; + top: -3px; + cursor: nesw-resize; + } + + .resizer.bottom-left { + left: -3px; + bottom: -3px; + cursor: nesw-resize; + } + + .resizer.bottom-right { + right: -3px; + bottom: -3px; + cursor: nwse-resize; + } + } +} + +.progressivizeMove-frame { + width: 20px; + border-radius: 2px; + z-index: 100000; + color: white; + text-align: center; + background-color: #5a9edd; + transform: translate(-110%, 110%); +} + +.progressivizeButton:hover { + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5); + + .progressivizeButton-frame { + background-color: #5a9edd; + color: white; + } } .collectionFreeform-customText { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index badbc48a1..ef4b7b9d2 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 } 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 } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; @@ -46,6 +46,7 @@ import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { PresBox } from "../../nodes/PresBox"; import { SearchUtil } from "../../../util/SearchUtil"; import { LinkManager } from "../../../util/LinkManager"; @@ -76,7 +77,7 @@ export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; childPointerEvents?: boolean; - scaleField?: string; // used by formattedTextBox when displaying a sidebar freeform view which needs its own scale field + scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) }; @@ -213,7 +214,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const layoutDoc = Doc.Layout(d); if (this.Document.currentFrame !== undefined) { const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.opacity); + CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, vals.opacity); } else { d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; @@ -911,7 +912,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P // !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1); // } else { if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { - if (!doc.z) this.setPan(newPanX, newPanY, "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow + // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... + if (!doc.z) this.setPan(newPanX, newPanY, doc.presTransition || doc.presTransition === 0 ? `transform ${doc.presTransition}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow } Doc.BrushDoc(this.props.Document); this.props.focus(this.props.Document); @@ -1257,7 +1259,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); - appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); + optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); if (!Doc.UserDoc().noviceMode) { optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" }); @@ -1399,6 +1401,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} + presPaths={BoolCast(this.Document.presPathView)} + progressivize={BoolCast(this.Document.editProgressivize)} + zoomProgressivize={BoolCast(this.Document.editZoomProgressivize)} transition={Cast(this.layoutDoc._viewTransition, "string", null)} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> @@ -1482,11 +1487,51 @@ interface CollectionFreeFormViewPannableContentsProps { viewDefDivClick?: ScriptField; children: () => JSX.Element[]; transition?: string; + presPaths?: boolean; + progressivize?: boolean; + zoomProgressivize?: boolean; } @observer class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ + @computed get zoomProgressivize() { + return PresBox.Instance && this.props.zoomProgressivize ? PresBox.Instance.zoomProgressivizeContainer : (null); + } + + @computed get progressivize() { + return PresBox.Instance && this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : (null); + } + + @computed get presPaths() { + const presPaths = "presPaths" + (this.props.presPaths ? "" : "-hidden"); + return !(PresBox.Instance) ? (null) : (<> + {!this.props.presPaths ? (null) : <><div>{PresBox.Instance.order}</div> + <svg className={presPaths}> + <defs> + <marker id="arrow" markerWidth="3" overflow="visible" markerHeight="3" refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> + <path d="M0,0 L0,6 L9,3 z" fill="#69a6db" /> + </marker> + <marker id="square" markerWidth="3" markerHeight="3" overflow="visible" + refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> + <path d="M 5,1 L 9,5 5,9 1,5 z" fill="#69a6db" /> + </marker> + <marker id="markerSquare" markerWidth="7" markerHeight="7" refX="4" refY="4" + orient="auto" overflow="visible"> + <rect x="1" y="1" width="5" height="5" fill="#69a6db" /> + </marker> + + <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="2" refY="7" + orient="auto" overflow="visible"> + <path d="M2,2 L2,13 L8,7 L2,2" fill="#69a6db" /> + </marker> + </defs>; + {PresBox.Instance.paths} + </svg></>} + </>); + } + render() { + // trace(); const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); const cenx = this.props.centeringShiftX(); const ceny = this.props.centeringShiftY(); @@ -1499,6 +1544,9 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF transition: this.props.transition }}> {this.props.children()} + {this.presPaths} + {this.progressivize} + {this.zoomProgressivize} </div>; } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 4334a339a..858f33291 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -339,10 +339,21 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._visible = false; } + @undoBatch @action delete = () => { - this.props.removeDocument(this.marqueeSelect(false)); + const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); + + selected.map(doc => { + const effectiveAcl = GetEffectiveAcl(doc); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete + recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); + this.props.removeDocument(doc); + } + }); + this.cleanupInteractions(false); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss index 7df56115f..5e0c9fcbb 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.scss +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss @@ -5,8 +5,8 @@ font-family: "Noto Sans"; cursor: auto; - overflow-x: visible; - overflow-y: visible; + overflow-x: hidden; + overflow-y: scroll; .propertiesView-title { background-color: rgb(159, 159, 159); @@ -41,7 +41,7 @@ font-size: 12.5px; &:hover { - cursor: pointer; + cursor: text; } } @@ -119,6 +119,19 @@ font-size: 10px; padding: 10px; margin-left: 5px; + + .change-buttons { + display: flex; + + button { + width: 5; + height: 5; + } + + input { + width: 100%; + } + } } } @@ -233,11 +246,15 @@ .propertiesView-sharingTable { + // whatever's commented out - add it back in when adding the buttons + + // border: 1.5px solid black; border: 1px solid black; - padding: 5px; - border-radius: 6px; - /* width: 170px; */ - margin-right: 10px; + padding: 5px; // remove when adding buttons + border-radius: 6px; // remove when adding buttons + margin-right: 10px; // remove when adding buttons + // width: 100%; + // display: inline-table; background-color: #ececec; max-height: 130px; overflow-y: scroll; @@ -245,9 +262,11 @@ .propertiesView-sharingTable-item { display: flex; + // padding: 5px; padding: 3px; align-items: center; border-bottom: 0.5px solid grey; + cursor: pointer; &:hover .propertiesView-sharingTable-item-name { overflow-x: unset; @@ -405,6 +424,43 @@ } } + + .propertiesView-presTrails { + border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-presTrails-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-presTrails-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-presTrails-content { + font-size: 10px; + padding: 10px; + margin-left: 5px; + } + } } .inking-button { @@ -567,6 +623,27 @@ } } +.propertiesView-presSelected { + border-top: solid 1px darkgrey; + width: 100%; + padding-top: 5px; + font-family: Roboto; + font-weight: 500; + display: inline-flex; + + .propertiesView-selectedList { + border-left: solid 1px darkgrey; + margin-left: 10px; + padding-left: 5px; + + .selectedList-items { + font-size: 12; + font-weight: 300; + margin-top: 1; + } + } +} + .widthAndDash { .width { @@ -596,6 +673,7 @@ display: flex; margin-bottom: 3px; + margin-left: 4px; .arrows-head { @@ -629,7 +707,7 @@ .dashed { display: flex; - margin-left: 74px; + margin-left: 64px; margin-bottom: 6px; .dashed-title { @@ -641,4 +719,15 @@ margin-top: 2px; } } +} + +.editable-title { + border: none; + padding: 6px; + padding-bottom: 2px; + + + &:hover { + border: 0.75px solid rgb(122, 28, 28); + } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx index f5e0cd077..15900aa33 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx @@ -2,13 +2,11 @@ import React = require("react"); import { observer } from "mobx-react"; import "./PropertiesView.scss"; import { observable, action, computed, runInAction } from "mobx"; -import { Doc, Field, DocListCast, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt } from "../../../../fields/Doc"; -import { DocumentView } from "../../nodes/DocumentView"; +import { Doc, Field, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt, DocCastAsync } from "../../../../fields/Doc"; import { ComputedField } from "../../../../fields/ScriptField"; import { EditableView } from "../../EditableView"; import { KeyValueBox } from "../../nodes/KeyValueBox"; import { Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { listSpec } from "../../../../fields/Schema"; import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentView"; import { returnFalse, returnOne, emptyFunction, emptyPath, returnTrue, returnZero, returnEmptyFilter, Utils } from "../../../../Utils"; import { Id } from "../../../../fields/FieldSymbols"; @@ -16,18 +14,26 @@ import { Transform } from "../../../util/Transform"; import { PropertiesButtons } from "../../PropertiesButtons"; import { SelectionManager } from "../../../util/SelectionManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip, Checkbox, Divider } from "@material-ui/core"; +import { Tooltip, Checkbox } from "@material-ui/core"; import SharingManager from "../../../util/SharingManager"; import { DocumentType } from "../../../documents/DocumentTypes"; -import FormatShapePane from "./FormatShapePane"; import { SharingPermissions, GetEffectiveAcl } from "../../../../fields/util"; import { InkField } from "../../../../fields/InkField"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { ColorState, SketchPicker } from "react-color"; -import AntimodeMenu from "../../AntimodeMenu"; import "./FormatShapePane.scss"; -import { discovery_v1 } from "googleapis"; +import { PresBox } from "../../nodes/PresBox"; +import { DocumentManager } from "../../../util/DocumentManager"; +import FormatShapePane from "./FormatShapePane"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; +const _global = (window /* browser */ || global /* node */) as any; + +// import * as fa from '@fortawesome/free-solid-svg-icons'; +// import { library } from "@fortawesome/fontawesome-svg-core"; +// library.add(fa.faPlus, fa.faMinus, fa.faCog); interface PropertiesViewProps { width: number; @@ -39,14 +45,21 @@ interface PropertiesViewProps { @observer export class PropertiesView extends React.Component<PropertiesViewProps> { + private _widthUndo?: UndoManager.Batch; @computed get MAX_EMBED_HEIGHT() { return 200; } @computed get selectedDocumentView() { if (SelectionManager.SelectedDocuments().length) { return SelectionManager.SelectedDocuments()[0]; + } else if (PresBox.Instance?._selectedArray.length) { + return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); } else { return undefined; } } + @computed get isPres(): boolean { + if (this.selectedDoc?.type === DocumentType.PRES) return true; + return false; + } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } @@ -58,6 +71,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openLayout: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; + // @observable selectedUser: string = ""; + // @observable addButtonPressed: boolean = false; + + //Pres Trails booleans: + @observable openPresTransitions: boolean = false; + @observable openPresProgressivize: boolean = false; + @observable openAddSlide: boolean = false; + @observable openSlideOptions: boolean = false; + + @observable inActions: boolean = false; + @observable _controlBtn: boolean = false; + @observable _lock: boolean = false; @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } @@ -156,7 +181,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); const rows: JSX.Element[] = []; for (const key of Object.keys(ids).slice().sort()) { - if ((key[0] === key[0].toUpperCase() && key.substring(0, 3) !== "ACL") + if ((key[0] === key[0].toUpperCase() && key.substring(0, 3) !== "ACL" && key !== "UseCors") || key[0] === "#" || key === "author" || key === "creationDate" || key.indexOf("lastModified") !== -1) { @@ -222,12 +247,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return false; } + @observable transform: Transform = Transform.Identity(); + getTransform = () => { return this.transform; } + propertiesDocViewRef = (ref: HTMLDivElement) => { + const observer = new _global.ResizeObserver(action((entries: any) => { + const cliRect = ref.getBoundingClientRect(); + this.transform = new Transform(-cliRect.x, -cliRect.y, 1); + })); + ref && observer.observe(ref); + } + + previewBackground = () => "lightgrey"; @computed get layoutPreview() { if (this.selectedDoc) { const layoutDoc = Doc.Layout(this.selectedDoc); const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; - return <div style={{ display: "inline-block", height: panelHeight() }} key={this.selectedDoc[Id]}> + return <div ref={this.propertiesDocViewRef} style={{ pointerEvents: "none", display: "inline-block", height: panelHeight() }} key={this.selectedDoc[Id]}> <ContentFittingDocumentView Document={layoutDoc} DataDoc={this.dataDoc} @@ -235,8 +271,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { renderDepth={this.props.renderDepth + 1} rootSelected={returnFalse} treeViewDoc={undefined} - backgroundColor={() => "lightgrey"} - fitToBox={false} + backgroundColor={this.previewBackground} + fitToBox={true} FreezeDimensions={true} NativeWidth={layoutDoc.type === StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : returnZero} @@ -245,7 +281,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { PanelWidth={panelWidth} PanelHeight={panelHeight} focus={returnFalse} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} + ScreenToLocalTransform={this.getTransform} docFilters={returnEmptyFilter} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} @@ -259,6 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { bringToFront={returnFalse} ContentScaling={returnOne} dontRegisterView={true} + dropAction={undefined} /> </div>; } else { @@ -266,13 +303,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } } + /** + * Handles the changing of a user's permissions from the permissions panel. + */ @undoBatch changePermissions = (e: any, user: string) => { SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, this.selectedDoc!); } - getPermissionsSelect(user: string) { + /** + * @returns the options for the permissions dropdown. + */ + getPermissionsSelect(user: string, permission: string) { return <select className="permissions-select" + defaultValue={permission} onChange={e => this.changePermissions(e, user)}> {Object.values(SharingPermissions).map(permission => { return ( @@ -283,16 +327,22 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </select>; } + /** + * @returns the notification icon. On clicking, it should notify someone of a document been shared with them. + */ @computed get notifyIcon() { - return <Tooltip title={<><div className="dash-tooltip">Notify with message</div></>}> + return <Tooltip title={<div className="dash-tooltip">Notify with message</div>}> <div className="notify-button"> <FontAwesomeIcon className="notify-button-icon" icon="bell" color="white" size="sm" /> </div> </Tooltip>; } + /** + * ... next to the owner that opens the main SharingManager interface on click. + */ @computed get expansionIcon() { - return <Tooltip title={<><div className="dash-tooltip">{"Show more permissions"}</div></>}> + return <Tooltip title={<div className="dash-tooltip">{"Show more permissions"}</div>}> <div className="expansion-button" onPointerDown={() => { if (this.selectedDocumentView) { SharingManager.Instance.open(this.selectedDocumentView); @@ -303,17 +353,26 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </Tooltip>; } - sharingItem(name: string, effectiveAcl: symbol, permission?: string) { - return <div className="propertiesView-sharingTable-item"> + /** + * @returns a row of the permissions panel + */ + sharingItem(name: string, effectiveAcl: symbol, permission: string) { + return <div className="propertiesView-sharingTable-item" key={name + permission} + // style={{ backgroundColor: this.selectedUser === name ? "#bcecfc" : "" }} + // onPointerDown={action(() => this.selectedUser = this.selectedUser === name ? "" : name)} + > <div className="propertiesView-sharingTable-item-name" style={{ width: name !== "Me" ? "85px" : "80px" }}> {name} </div> {/* {name !== "Me" ? this.notifyIcon : null} */} <div className="propertiesView-sharingTable-item-permission"> - {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name) : permission} + {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission} {permission === "Owner" ? this.expansionIcon : null} </div> </div>; } + /** + * @returns the sharing and permissiosn panel. + */ @computed get sharingTable() { const AclMap = new Map<symbol, string>([ [AclPrivate, SharingPermissions.None], @@ -326,13 +385,24 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const effectiveAcl = GetEffectiveAcl(this.selectedDoc!); const tableEntries = []; + // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { if (this.selectedDoc![AclSym]) { for (const [key, value] of Object.entries(this.selectedDoc![AclSym])) { const name = key.substring(4).replace("_", "."); - if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author) tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!)); + if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author/* && sidebarUsersDisplayed![name] !== false*/) { + tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!)); + } } } + // if (Doc.UserDoc().sidebarUsersDisplayed) { + // for (const [name, value] of Object.entries(sidebarUsersDisplayed!)) { + // if (value === true && !this.selectedDoc![`ACL-${name.substring(8).replace(".", "_")}`]) tableEntries.push(this.sharingItem(name.substring(8), effectiveAcl, SharingPermissions.None)); + // } + // } + // }) + + // shifts the current user and the owner to the top of the doc. tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === this.selectedDoc!.author ? "Owner" : StrCast(this.selectedDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]))); if (Doc.CurrentUserEmail !== this.selectedDoc!.author) tableEntries.unshift(this.sharingItem(StrCast(this.selectedDoc!.author), effectiveAcl, "Owner")); @@ -349,20 +419,19 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { />; } - @undoBatch @action toggleCheckbox = () => { this.layoutFields = !this.layoutFields; } @computed get editableTitle() { - return <EditableView + return <div className="editable-title"><EditableView key="editableView" contents={StrCast(this.selectedDoc?.title)} height={25} fontSize={14} GetValue={() => StrCast(this.selectedDoc?.title)} - SetValue={this.setTitle} />; + SetValue={this.setTitle} /> </div>; } @undoBatch @@ -399,15 +468,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { var index = 0; if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { doc.rotation = Number(doc.rotation) + Number(angle); - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - + const inks = Cast(doc.data, InkField)?.inkData; + if (inks) { const newPoints: { X: number, Y: number }[] = []; - for (var i = 0; i < ink.length; i++) { - const newX = Math.cos(angle) * (ink[i].X - _centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].X; - const newY = Math.sin(angle) * (ink[i].X - _centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].Y; + inks.forEach(ink => { + const newX = Math.cos(angle) * (ink.X - _centerPoints[index].X) - Math.sin(angle) * (ink.Y - _centerPoints[index].Y) + _centerPoints[index].X; + const newY = Math.sin(angle) * (ink.X - _centerPoints[index].X) + Math.cos(angle) * (ink.Y - _centerPoints[index].Y) + _centerPoints[index].Y; newPoints.push({ X: newX, Y: newY }); - } + }); doc.data = new InkField(newPoints); const xs = newPoints.map(p => p.X); const ys = newPoints.map(p => p.Y); @@ -424,23 +492,22 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } } - @observable _controlBtn: boolean = false; - @observable _lock: boolean = false; + @computed get controlPointsButton() { return <div className="inking-button"> - <Tooltip title={<><div className="dash-tooltip">{"Edit points"}</div></>}> - <div className="inking-button-points" onPointerDown={action(() => this._controlBtn = !this._controlBtn)} style={{ backgroundColor: this._controlBtn ? "black" : "" }}> + <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}> + <div className="inking-button-points" onPointerDown={action(() => FormatShapePane.Instance._controlBtn = !FormatShapePane.Instance._controlBtn)} style={{ backgroundColor: FormatShapePane.Instance._controlBtn ? "black" : "" }}> <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> </div> </Tooltip> - <Tooltip title={<><div className="dash-tooltip">{this._lock ? "Unlock points" : "Lock points"}</div></>}> - <div className="inking-button-lock" onPointerDown={action(() => this._lock = !this._lock)} > - <FontAwesomeIcon icon={this._lock ? "unlock" : "lock"} color="white" size="lg" /> + <Tooltip title={<div className="dash-tooltip">{FormatShapePane.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> + <div className="inking-button-lock" onPointerDown={action(() => FormatShapePane.Instance._lock = !FormatShapePane.Instance._lock)} > + <FontAwesomeIcon icon={FormatShapePane.Instance._lock ? "lock" : "unlock"} color="white" size="lg" /> </div> </Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Rotate 90˚"}</div></>}> + <Tooltip title={<div className="dash-tooltip">{"Rotate 90˚"}</div>}> <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}> <FontAwesomeIcon icon="undo" color="white" size="lg" /> </div> @@ -492,12 +559,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oldX = NumCast(this.selectedDoc?.x); const oldY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); - this._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); + FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); const doc = this.selectedDoc; if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - console.log(doc.x, doc.y, doc._height, doc._width); const ink = Cast(doc.data, InkField)?.inkData; - console.log(ink); if (ink) { const newPoints: { X: number, Y: number }[] = []; for (var j = 0; j < ink.length; j++) { @@ -516,12 +581,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oX = NumCast(this.selectedDoc?.x); const oY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); - this._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); + FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); const docu = this.selectedDoc; if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { - console.log(docu.x, docu.y, docu._height, docu._width); const ink = Cast(docu.data, InkField)?.inkData; - console.log(ink); if (ink) { const newPoints: { X: number, Y: number }[] = []; for (var j = 0; j < ink.length; j++) { @@ -539,7 +602,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { getField(key: string) { //if (this.selectedDoc) { - return Field.toString(this.selectedDoc[key] as Field); + return Field.toString(this.selectedDoc?.[key] as Field); // } else { // return undefined as Opt<string>; // } @@ -556,17 +619,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { set shapeWid(value) { const oldWidth = NumCast(this.selectedDoc?._width); this.selectedDoc && (this.selectedDoc._width = Number(value)); - this._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); + FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); } set shapeHgt(value) { const oldHeight = NumCast(this.selectedDoc?._height); this.selectedDoc && (this.selectedDoc._height = Number(value)); - this._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); + FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); } - @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val, "H:", "wid", this.shapeWid, (val: string) => this.shapeWid = val, "W:"); } - @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } - @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } + @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); } + @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => { if (val !== "0" && !isNaN(Number(val))) { this.shapeXps = val; } return true; }, "X:", "Yps", this.shapeYps, (val: string) => { if (val !== "0" && !isNaN(Number(val))) { this.shapeYps = val; } return true; }, "Y:"); } + @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { if (!isNaN(Number(val))) { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; } return true; }, "∠:", "rot", this.shapeRot, (val: string) => { if (!isNaN(Number(val))) { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; } return true; }, ""); } + @observable private _fillBtn = false; @observable private _lineBtn = false; @@ -580,14 +644,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { set colorFil(value) { value && (this._lastFill = value); this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); } set colorStk(value) { value && (this._lastLine = value); this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); } - colorButton(value: string, setter: () => {}) { - return <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}> - <div className="color-button-preview" style={{ - backgroundColor: value ?? "121212", width: 15, height: 15, - display: value === "" || value === "transparent" ? "none" : "" - }} /> - {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -14, position: "fixed" }}>☒</p> : ""} - </div>; + colorButton(value: string, type: string, setter: () => {}) { + return <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={type === "fill" ? this.fillPicker : this.linePicker}> + <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}> + <div className="color-button-preview" style={{ + backgroundColor: value ?? "121212", width: 15, height: 15, + display: value === "" || value === "transparent" ? "none" : "" + }} /> + {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -14, position: "fixed" }}>☒</p> : ""} + </div> + </Flyout>; + } @undoBatch @@ -613,8 +681,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { color={type === "stk" ? this.colorStk : this.colorFil} />; } - @computed get fillButton() { return this.colorButton(this.colorFil, () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } - @computed get lineButton() { return this.colorButton(this.colorStk, () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } + @computed get fillButton() { return this.colorButton(this.colorFil, "fill", () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } + @computed get lineButton() { return this.colorButton(this.colorStk, "line", () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); } @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); } @@ -631,8 +699,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="stroke-button">{this.lineButton}</div> </div> </div> - {this._fillBtn ? this.fillPicker : ""} - {this._lineBtn ? this.linePicker : ""} + {/* {this._fillBtn ? this.fillPicker : ""} + {this._lineBtn ? this.linePicker : ""} */} </div>; } @@ -683,7 +751,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> <input className="width-range" type="range" defaultValue={Number(this.widthStk)} min={1} max={100} - onChange={undoBatch(action((e) => this.widthStk = e.target.value))} /> + onChange={(action((e) => this.widthStk = e.target.value))} + onMouseDown={(e) => { this._widthUndo = UndoManager.StartBatch("width undo"); }} + onMouseUp={(e) => { this._widthUndo?.end(); this._widthUndo = undefined; }} + /> </div> <div className="arrows"> @@ -701,7 +772,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> </div> <div className="dashed"> - <div className="dashed-title">Dash:</div> + <div className="dashed-title">Dashed Line:</div> <input key="markHead" className="dashed-input" type="checkbox" checked={this.dashdStk === "2"} onChange={this.changeDash} /> @@ -730,121 +801,250 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } - render() { + /** + * Handles adding and removing members from the sharing panel + */ + // handleUserChange = (selectedUser: string, add: boolean) => { + // if (!Doc.UserDoc().sidebarUsersDisplayed) Doc.UserDoc().sidebarUsersDisplayed = new Doc; + // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { + // sidebarUsersDisplayed![`display-${selectedUser}`] = add; + // !add && runInAction(() => this.selectedUser = ""); + // }); + // } - if (!this.selectedDoc) { + render() { + if (!this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width }}> <div className="propertiesView-title" style={{ width: this.props.width }}> No Document Selected - <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> - <FontAwesomeIcon icon="times" color="black" size="xs" /> - </div> </div> </div>; - } - - const novice = Doc.UserDoc().noviceMode; - return <div className="propertiesView" style={{ width: this.props.width }} > - <div className="propertiesView-title" style={{ width: this.props.width }}> - Properties - <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> - <FontAwesomeIcon icon="times" color="black" size="sm" /> - </div> - </div> - <div className="propertiesView-name"> - {this.editableTitle} - </div> - <div className="propertiesView-settings"> - <div className="propertiesView-settings-title" - onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })} - style={{ backgroundColor: this.openActions ? "black" : "" }}> - Actions - <div className="propertiesView-settings-title-icon"> - <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> + } else { + const novice = Doc.UserDoc().noviceMode; + + if (this.selectedDoc && !this.isPres) { + return <div className="propertiesView" style={{ + width: this.props.width, + // overflowY: this.inActions ? "visible" : "scroll" + }} > + <div className="propertiesView-title" style={{ width: this.props.width }}> + Properties + {/* <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> + <FontAwesomeIcon icon="times" color="black" size="sm" /> + </div> */} </div> - </div> - {!this.openActions ? (null) : - <div className="propertiesView-settings-content"> - <PropertiesButtons /> - </div>} - </div> - <div className="propertiesView-sharing"> - <div className="propertiesView-sharing-title" - onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} - style={{ backgroundColor: this.openSharing ? "black" : "" }}> - Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> - <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> + <div className="propertiesView-name"> + {this.editableTitle} </div> - </div> - {!this.openSharing ? (null) : - <div className="propertiesView-sharing-content"> - {this.sharingTable} - </div>} - </div> - - {!this.isInk ? (null) : - <div className="propertiesView-appearance"> - <div className="propertiesView-appearance-title" - onPointerDown={() => runInAction(() => { this.openAppearance = !this.openAppearance; })} - style={{ backgroundColor: this.openAppearance ? "black" : "" }}> - Appearance - <div className="propertiesView-appearance-title-icon"> - <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> + <div className="propertiesView-settings" onPointerEnter={() => runInAction(() => { this.inActions = true; })} + onPointerLeave={action(() => this.inActions = false)}> + <div className="propertiesView-settings-title" + onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })} + style={{ backgroundColor: this.openActions ? "black" : "" }}> + Actions + <div className="propertiesView-settings-title-icon"> + <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openActions ? (null) : + <div className="propertiesView-settings-content"> + <PropertiesButtons /> + </div>} + </div> + <div className="propertiesView-sharing"> + <div className="propertiesView-sharing-title" + onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} + style={{ backgroundColor: this.openSharing ? "black" : "" }}> + Sharing {"&"} Permissions + <div className="propertiesView-sharing-title-icon"> + <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> </div> + {!this.openSharing ? (null) : + <div className="propertiesView-sharing-content"> + {this.sharingTable} + {/* <div className="change-buttons"> + <button + onPointerDown={action(() => this.addButtonPressed = !this.addButtonPressed)} + > + <FontAwesomeIcon icon={fa.faPlus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /> + </button> + <button + id="sharingProperties-removeUser" + onPointerDown={() => this.handleUserChange(this.selectedUser, false)} + style={{ backgroundColor: this.selectedUser ? "#121721" : "#777777" }} + ><FontAwesomeIcon icon={fa.faMinus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button> + <button onClick={() => SharingManager.Instance.open(this.selectedDocumentView!)}><FontAwesomeIcon icon={fa.faCog} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button> + {this.addButtonPressed ? + // <input type="text" onKeyDown={this.handleKeyPress} /> : + <select onChange={e => this.handleUserChange(e.target.value, true)}> + <option selected disabled hidden> + Add users + </option> + {SharingManager.Instance.users.map(user => + (<option value={user.user.email}> + {user.user.email} + </option>) + )} + {GroupManager.Instance.getAllGroups().map(group => + (<option value={StrCast(group.groupName)}> + {StrCast(group.groupName)} + </option>))} + </select> : + null} + </div> */} + </div>} </div> - {!this.openAppearance ? (null) : - <div className="propertiesView-appearance-content"> - {this.appearanceEditor} + + {!this.isInk ? (null) : + <div className="propertiesView-appearance"> + <div className="propertiesView-appearance-title" + onPointerDown={() => runInAction(() => { this.openAppearance = !this.openAppearance; })} + style={{ backgroundColor: this.openAppearance ? "black" : "" }}> + Appearance + <div className="propertiesView-appearance-title-icon"> + <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openAppearance ? (null) : + <div className="propertiesView-appearance-content"> + {this.appearanceEditor} + </div>} </div>} - </div>} - - {this.isInk ? <div className="propertiesView-transform"> - <div className="propertiesView-transform-title" - onPointerDown={() => runInAction(() => { this.openTransform = !this.openTransform; })} - style={{ backgroundColor: this.openTransform ? "black" : "" }}> - Transform - <div className="propertiesView-transform-title-icon"> - <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> + + {this.isInk ? <div className="propertiesView-transform"> + <div className="propertiesView-transform-title" + onPointerDown={() => runInAction(() => { this.openTransform = !this.openTransform; })} + style={{ backgroundColor: this.openTransform ? "black" : "" }}> + Transform + <div className="propertiesView-transform-title-icon"> + <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openTransform ? <div className="propertiesView-transform-content"> + {this.transformEditor} + </div> : null} + </div> : null} + + <div className="propertiesView-fields"> + <div className="propertiesView-fields-title" + onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })} + style={{ backgroundColor: this.openFields ? "black" : "" }}> + <div className="propertiesView-fields-title-name"> + Fields {"&"} Tags + <div className="propertiesView-fields-title-icon"> + <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + </div> + {!novice && this.openFields ? <div className="propertiesView-fields-checkbox"> + {this.fieldsCheckbox} + <div className="propertiesView-fields-checkbox-text">Layout</div> + </div> : null} + {!this.openFields ? (null) : + <div className="propertiesView-fields-content"> + {novice ? this.noviceFields : this.expandedField} + </div>} </div> - </div> - {this.openTransform ? <div className="propertiesView-transform-content"> - {this.transformEditor} - </div> : null} - </div> : null} - - <div className="propertiesView-fields"> - <div className="propertiesView-fields-title" - onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })} - style={{ backgroundColor: this.openFields ? "black" : "" }}> - <div className="propertiesView-fields-title-name"> - Fields {"&"} Tags - <div className="propertiesView-fields-title-icon"> - <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> + <div className="propertiesView-layout"> + <div className="propertiesView-layout-title" + onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })} + style={{ backgroundColor: this.openLayout ? "black" : "" }}> + Layout + <div className="propertiesView-layout-title-icon" onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })}> + <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> </div> + {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} </div> - </div> - {!novice && this.openFields ? <div className="propertiesView-fields-checkbox"> - {this.fieldsCheckbox} - <div className="propertiesView-fields-checkbox-text">Layout</div> - </div> : null} - {!this.openFields ? (null) : - <div className="propertiesView-fields-content"> - {novice ? this.noviceFields : this.expandedField} + </div>; + } + if (this.isPres) { + const selectedItem: boolean = PresBox.Instance?._selectedArray.length > 0; + return <div className="propertiesView" style={{ width: this.props.width }} > + <div className="propertiesView-title" style={{ width: this.props.width }}> + Presentation + <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> + <FontAwesomeIcon icon="times" color="black" size="sm" /> + </div> + </div> + <div className="propertiesView-name"> + {this.editableTitle} + <div className="propertiesView-presSelected"> + {PresBox.Instance?._selectedArray.length} selected + <div className="propertiesView-selectedList"> + {PresBox.Instance?.listOfSelected} + </div> + </div> + </div> + {!selectedItem ? (null) : <div className="propertiesView-presTrails"> + <div className="propertiesView-presTrails-title" + onPointerDown={() => runInAction(() => { this.openPresTransitions = !this.openPresTransitions; })} + style={{ backgroundColor: this.openPresTransitions ? "black" : "" }}> + <FontAwesomeIcon icon={"rocket"} /> Transitions + <div className="propertiesView-presTrails-title-icon"> + <FontAwesomeIcon icon={this.openPresTransitions ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openPresTransitions ? <div className="propertiesView-presTrails-content"> + {PresBox.Instance.transitionDropdown} + </div> : null} </div>} - </div> - <div className="propertiesView-layout"> - <div className="propertiesView-layout-title" - onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })} - style={{ backgroundColor: this.openLayout ? "black" : "" }}> - Layout - <div className="propertiesView-layout-title-icon" onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })}> - <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> + {!selectedItem ? (null) : <div className="propertiesView-presTrails"> + <div className="propertiesView-presTrails-title" + onPointerDown={() => runInAction(() => { this.openPresProgressivize = !this.openPresProgressivize; })} + style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}> + <FontAwesomeIcon icon={"tasks"} /> Progressivize + <div className="propertiesView-presTrails-title-icon"> + <FontAwesomeIcon icon={this.openPresProgressivize ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openPresProgressivize ? <div className="propertiesView-presTrails-content"> + {PresBox.Instance.progressivizeDropdown} + </div> : null} + </div>} + {!selectedItem ? (null) : <div className="propertiesView-presTrails"> + <div className="propertiesView-presTrails-title" + onPointerDown={() => runInAction(() => { this.openSlideOptions = !this.openSlideOptions; })} + style={{ backgroundColor: this.openSlideOptions ? "black" : "" }}> + <FontAwesomeIcon icon={"cog"} /> {PresBox.Instance.stringType} options + <div className="propertiesView-presTrails-title-icon"> + <FontAwesomeIcon icon={this.openSlideOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openSlideOptions ? <div className="propertiesView-presTrails-content"> + {PresBox.Instance.optionsDropdown} + </div> : null} + </div>} + <div className="propertiesView-presTrails"> + <div className="propertiesView-presTrails-title" + onPointerDown={() => runInAction(() => { this.openAddSlide = !this.openAddSlide; })} + style={{ backgroundColor: this.openAddSlide ? "black" : "" }}> + <FontAwesomeIcon icon={"plus"} /> Add new slide + <div className="propertiesView-presTrails-title-icon"> + <FontAwesomeIcon icon={this.openAddSlide ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openAddSlide ? <div className="propertiesView-presTrails-content"> + {PresBox.Instance.newDocumentDropdown} + </div> : null} </div> - </div> - {this.openLayout ? <div className="propertiesView-layout-content">{this.layoutPreview}</div> : null} - </div> - </div>; + <div className="propertiesView-sharing"> + <div className="propertiesView-sharing-title" + onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} + style={{ backgroundColor: this.openSharing ? "black" : "" }}> + Sharing {"&"} Permissions + <div className="propertiesView-sharing-title-icon"> + <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openSharing ? <div className="propertiesView-sharing-content"> + {this.sharingTable} + </div> : null} + </div> + </div>; + } + } } -}
\ No newline at end of file +}
\ 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 21f77e47b..e6ac7021a 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -240,7 +240,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { w: Math.min(w, this.numCols), // reduces width if greater than numCols static: BoolCast(this.childLayoutPairs.find(({ layout }) => layout[Id] === i)?.layout.lockedPosition, false) // checks if the lock position item has been selected in the context menu })) : - this.savedLayoutList.map((layout, index) => Object.assign(layout, this.unflexedPosition(index))); + this.savedLayoutList.map((layout, index) => { Object.assign(layout, this.unflexedPosition(index)); return layout; }); } /** diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss index d26b7920a..7e6999cdc 100644 --- a/src/client/views/linking/LinkEditor.scss +++ b/src/client/views/linking/LinkEditor.scss @@ -89,7 +89,7 @@ /* float: right; */ border-radius: 7px; font-size: 9px; - background-color: black; + background: black; /* padding: 3px; */ padding-top: 4px; padding-left: 7px; @@ -100,6 +100,7 @@ &:hover { cursor: pointer; + background: grey; } } } diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index fb014af51..5832a2181 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -287,7 +287,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { @observable openDropdown: boolean = false; @observable showInfo: boolean = false; @computed get infoIcon() { if (this.showInfo) { return "chevron-up"; } return "chevron-down"; } - @observable private buttonColor: string = "black"; + @observable private buttonColor: string = ""; //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; @@ -303,7 +303,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { if (LinkManager.currentLink) { LinkManager.currentLink.description = value; this.buttonColor = "rgb(62, 133, 55)"; - setTimeout(action(() => this.buttonColor = "black"), 750); + setTimeout(action(() => this.buttonColor = ""), 750); return true; } } @@ -345,7 +345,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { ></input> </div> <div className="linkEditor-description-add-button" - style={{ backgroundColor: this.buttonColor }} + style={{ background: this.buttonColor }} onPointerDown={this.onDown}>Set</div> </div></div>; } diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 98e4171f0..4dc25031d 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -6,6 +6,7 @@ position: absolute; top: 0; left: 0; + z-index: 999; .linkMenu-list { diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 7b5fb0127..8ecde959f 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -4,14 +4,13 @@ import { DocumentView } from "../nodes/DocumentView"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); -import { Doc, Opt } from "../../../fields/Doc"; +import { Doc } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; import { LinkMenuGroup } from "./LinkMenuGroup"; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { library } from "@fortawesome/fontawesome-svg-core"; import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; import { LinkDocPreview } from "../nodes/LinkDocPreview"; -import { isUndefined } from "util"; library.add(faTrash); @@ -19,7 +18,6 @@ interface Props { docView: DocumentView; changeFlyout: () => void; addDocTab: (document: Doc, where: string) => boolean; - location: number[]; } @observer @@ -85,17 +83,25 @@ export class LinkMenu extends React.Component<Props> { return linkItems; } + @computed + get position() { + const docView = this.props.docView; + const transform = (docView.props.ScreenToLocalTransform().scale(docView.props.ContentScaling())).inverse(); + const [sptX, sptY] = transform.transformPoint(0, 0); + const [bptX, bptY] = transform.transformPoint(docView.props.PanelWidth(), docView.props.PanelHeight()); + return { x: sptX, y: sptY, r: bptX, b: bptY }; + } + render() { + console.log("computed", this.position.x, this.position.b); const sourceDoc = this.props.docView.props.Document; const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); return <div className="linkMenu" ref={this._linkMenuRef} > - {!this._editingLink ? <div className="linkMenu-list" style={{ - left: this.props.location[0], top: this.props.location[1] - }}> - {this.renderAllGroups(groups)} - </div> : <div className="linkMenu-listEditor" style={{ - left: this.props.location[0], top: this.props.location[1] - }}> + {!this._editingLink ? + <div className="linkMenu-list" style={{ left: this.position.x, top: this.position.b + 15 }}> + {this.renderAllGroups(groups)} + </div> : + <div className="linkMenu-listEditor" style={{ left: this.position.x, top: this.position.b + 15 }}> <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} /> </div> diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 681f6ae54..17c2f42bf 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -207,7 +207,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { case DocumentType.AUDIO: destinationIcon = "microphone"; break; case DocumentType.BUTTON: destinationIcon = "bolt"; break; case DocumentType.PRES: destinationIcon = "tv"; break; - case DocumentType.QUERY: destinationIcon = "search"; break; case DocumentType.SCRIPTING: destinationIcon = "terminal"; break; case DocumentType.IMPORT: destinationIcon = "cloud-upload-alt"; break; case DocumentType.DOCHOLDER: destinationIcon = "expand"; break; diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index e9420a072..306062ced 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -46,6 +46,40 @@ width: 100%; height: 100%; position: relative; + + + } + + .recording { + margin-top: auto; + margin-bottom: auto; + width: 100%; + height: 100%; + position: relative; + padding-right: 5px; + display: flex; + background-color: red; + + .time { + position: relative; + height: 100%; + width: 100%; + font-size: 20; + text-align: center; + top: 5; + } + + .buttons { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 5px; + } + + .buttons:hover { + background-color: crimson; + } } .audiobox-controls { @@ -54,6 +88,17 @@ position: relative; display: flex; padding-left: 2px; + background: black; + + .audiobox-dictation { + position: absolute; + width: 30px; + height: 100%; + align-items: center; + display: inherit; + background: dimgray; + left: 0px; + } .audiobox-player { margin-top: auto; @@ -64,16 +109,32 @@ padding-right: 5px; display: flex; - .audiobox-playhead, - .audiobox-dictation { + .audiobox-playhead { position: relative; margin-top: auto; margin-bottom: auto; - width: 25px; + margin-right: 2px; + width: 30px; + height: 25px; padding: 2px; + border-radius: 50%; + background-color: black; + color: white; + } + + .audiobox-playhead:hover { + // background-color: black; + // border-radius: 5px; + background-color: grey; + color: lightgrey; } .audiobox-dictation { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 2px; align-items: center; display: inherit; background: dimgray; @@ -81,17 +142,29 @@ .audiobox-timeline { position: relative; - height: 100%; + height: 80%; width: 100%; background: white; border: gray solid 1px; border-radius: 3px; + z-index: 1000; + overflow: hidden; .audiobox-current { width: 1px; height: 100%; background-color: red; position: absolute; + top: 0px; + } + + .waveform { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + z-index: -1000; + bottom: -30%; } .audiobox-linker, @@ -104,7 +177,6 @@ background: gray; border-radius: 100%; opacity: 0.9; - background-color: transparent; box-shadow: black 2px 2px 1px; .linkAnchorBox-cont { @@ -142,11 +214,37 @@ .audiobox-marker-minicontainer { position: absolute; width: 10px; + height: 10px; + top: 2.5%; + background: gray; + border-radius: 50%; + box-shadow: black 2px 2px 1px; + overflow: visible; + cursor: pointer; + + .audiobox-marker { + position: relative; + height: 100%; + // height: calc(100% - 15px); + width: 100%; + //margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + } + + .audiobox-marker-container1, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; height: 90%; top: 2.5%; background: gray; border-radius: 5px; box-shadow: black 2px 2px 1px; + opacity: 0.3; .audiobox-marker { position: relative; @@ -157,6 +255,36 @@ .audio-marker:hover { border: orange 2px solid; } + + .resizer { + position: absolute; + right: 0; + cursor: ew-resize; + height: 100%; + width: 2px; + z-index: 100; + } + + .click { + position: relative; + height: 100%; + width: 100%; + z-index: 100; + } + + .left-resizer { + position: absolute; + left: 0; + cursor: ew-resize; + height: 100%; + width: 2px; + z-index: 100; + } + } + + .audiobox-marker-container1:hover, + .audiobox-marker-minicontainer:hover { + opacity: 0.8; } .audiobox-marker-minicontainer { @@ -170,6 +298,22 @@ } } } + + .current-time { + position: absolute; + font-size: 8; + top: calc(100% - 8px); + left: 30px; + color: white; + } + + .total-time { + position: absolute; + top: calc(100% - 8px); + font-size: 8; + right: 2px; + color: white; + } } } } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 5c921cea4..eba1046b2 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -2,31 +2,30 @@ import React = require("react"); import { FieldViewProps, FieldView } from './FieldView'; import { observer } from "mobx-react"; import "./AudioBox.scss"; -import { Cast, DateCast, NumCast } from "../../../fields/Types"; +import { Cast, DateCast, NumCast, FieldValue, ScriptCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { ViewBoxBaseComponent } from "../DocComponent"; +import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { makeInterface, createSchema } from "../../../fields/Schema"; import { documentSchema } from "../../../fields/documentSchemas"; -import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero } from "../../../Utils"; -import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx"; +import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero, formatTime } from "../../../Utils"; +import { runInAction, observable, reaction, IReactionDisposer, computed, action, trace, toJS } from "mobx"; import { DateField } from "../../../fields/DateField"; import { SelectionManager } from "../../util/SelectionManager"; -import { Doc, DocListCast } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { ContextMenuProps } from "../ContextMenuItem"; import { ContextMenu } from "../ContextMenu"; import { Id } from "../../../fields/FieldSymbols"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DocumentView } from "./DocumentView"; import { Docs, DocUtils } from "../../documents/Documents"; -import { ComputedField } from "../../../fields/ScriptField"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Networking } from "../../Network"; import { LinkAnchorBox } from "./LinkAnchorBox"; - -// testing testing - -interface Window { - MediaRecorder: MediaRecorder; -} +import { List } from "../../../fields/List"; +import { Scripting } from "../../util/Scripting"; +import Waveform from "react-audio-waveform"; +import axios from "axios"; +const _global = (window /* browser */ || global /* node */) as any; declare class MediaRecorder { // whatever MediaRecorder has @@ -40,32 +39,72 @@ type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>; const AudioDocument = makeInterface(documentSchema, audioSchema); @observer -export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument>(AudioDocument) { +export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } public static Enabled = false; + static Instance: AudioBox; + static RangeScript: ScriptField; + static LabelScript: ScriptField; + _linkPlayDisposer: IReactionDisposer | undefined; _reactionDisposer: IReactionDisposer | undefined; _scrubbingDisposer: IReactionDisposer | undefined; _ele: HTMLAudioElement | null = null; _recorder: any; _recordStart = 0; + _pauseStart = 0; + _pauseEnd = 0; + _pausedTime = 0; _stream: MediaStream | undefined; + _start: number = 0; + _hold: boolean = false; + _left: boolean = false; + _markers: Array<any> = []; + _first: boolean = false; + _dragging = false; + + _count: Array<any> = []; + _timeline: Opt<HTMLDivElement>; + _duration = 0; + + private _isPointerDown = false; + private _currMarker: any; + @observable _position: number = 0; + @observable _buckets: Array<number> = new Array<number>(); + @observable private _height: number = NumCast(this.layoutDoc._height); + @observable private _paused: boolean = false; @observable private static _scrubTime = 0; @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); } set audioState(value) { this.dataDoc.audioState = value; } public static SetScrubTime = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); }; - @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); } + constructor(props: Readonly<FieldViewProps>) { + super(props); + + // onClick play script + if (!AudioBox.RangeScript) { + AudioBox.RangeScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart), (this.audioEnd))`, { scriptContext: "any" })!; + } + + if (!AudioBox.LabelScript) { + AudioBox.LabelScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart))`, { scriptContext: "any" })!; + } + } + componentWillUnmount() { this._reactionDisposer?.(); this._linkPlayDisposer?.(); this._scrubbingDisposer?.(); } componentDidMount() { + if (!this.dataDoc.markerAmount) { + this.dataDoc.markerAmount = 0; + } + runInAction(() => this.audioState = this.path ? "paused" : undefined); this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID, scrollLinkId => { @@ -77,15 +116,59 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); } }, { fireImmediately: true }); + + // for play when link is selected this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(), selected => { const sel = selected.length ? selected[0].props.Document : undefined; - this.layoutDoc.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime()); - this.layoutDoc.playOnSelect && this.recordingStart && !sel && this.pause(); + let link; + if (sel) { + // for determining if the link is created after recording (since it will use linkTime rather than creation date) + DocListCast(this.dataDoc.links).map((l, i) => { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + if (la1 === sel || la2 === sel) { // if the selected document is linked to this audio + let linkTime = NumCast(l.anchor2_timecode); + let endTime; + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + linkTime = NumCast(l.anchor1_timecode); + } + if (la2.audioStart) { + linkTime = NumCast(la2.audioStart); + } + + if (la1.audioStart) { + linkTime = NumCast(la1.audioStart); + } + + if (la1.audioEnd) { + endTime = NumCast(la1.audioEnd); + } + + if (la2.audioEnd) { + endTime = NumCast(la2.audioEnd); + } + + if (linkTime) { + link = true; + this.layoutDoc.playOnSelect && this.recordingStart && sel && !Doc.AreProtosEqual(sel, this.props.Document) && (endTime ? this.playFrom(linkTime, endTime) : this.playFrom(linkTime)); + } + } + }); + } + + // for links created during recording + if (!link) { + this.layoutDoc.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime()); + this.layoutDoc.playOnSelect && this.recordingStart && !sel && this.pause(); + } }); this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); } + // for updating the timecode timecodeChanged = () => { const htmlEle = this._ele; if (this.audioState !== "recording" && htmlEle) { @@ -105,15 +188,23 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument } } + // pause play back pause = action(() => { this._ele!.pause(); this.audioState = "paused"; }); + // play audio for documents created during recording playFromTime = (absoluteTime: number) => { this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000); } - playFrom = (seekTimeInSeconds: number) => { + + // play back the audio from time + @action + playFrom = (seekTimeInSeconds: number, endTime: number = this.dataDoc.duration) => { + let play; + clearTimeout(play); + this._duration = endTime - seekTimeInSeconds; if (this._ele && AudioBox.Enabled) { if (seekTimeInSeconds < 0) { if (seekTimeInSeconds > -1) { @@ -125,20 +216,29 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument this._ele.currentTime = seekTimeInSeconds; this._ele.play(); runInAction(() => this.audioState = "playing"); + if (endTime !== this.dataDoc.duration) { + play = setTimeout(() => this.pause(), (this._duration) * 1000); // use setTimeout to play a specific duration + } } else { this.pause(); } } } - + // update the recording time updateRecordTime = () => { if (this.audioState === "recording") { - setTimeout(this.updateRecordTime, 30); - this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart) / 1000; + if (this._paused) { + setTimeout(this.updateRecordTime, 30); + this._pausedTime += (new Date().getTime() - this._recordStart) / 1000; + } else { + setTimeout(this.updateRecordTime, 30); + this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + } } } + // starts recording recordAudioAnnotation = async () => { this._stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this._recorder = new MediaRecorder(this._stream); @@ -154,26 +254,31 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument runInAction(() => this.audioState = "recording"); setTimeout(this.updateRecordTime, 0); this._recorder.start(); - setTimeout(() => this._recorder && this.stopRecording(), 60 * 1000); // stop after an hour + setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour } + // context menu specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - + funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.hideLabels ? "Don't hide" : "Hide") + " labels", event: () => this.layoutDoc.hideLabels = !this.layoutDoc.hideLabels, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } + // stops the recording stopRecording = action(() => { this._recorder.stop(); this._recorder = undefined; - this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000; + this.dataDoc.duration = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; this.audioState = "paused"; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this.props.Document); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); }); + // button for starting and stopping the recording recordClick = (e: React.MouseEvent) => { if (e.button === 0 && !e.ctrlKey) { this._recorder ? this.stopRecording() : this.recordAudioAnnotation(); @@ -181,14 +286,13 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument } } + // for play button onPlay = (e: any) => { this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1); e.stopPropagation(); } - onStop = (e: any) => { - this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect; - e.stopPropagation(); - } + + // creates a text document for dictation onFile = (e: any) => { const newDoc = Docs.Create.TextDocument("", { title: "", _chromeStatus: "disabled", @@ -202,18 +306,21 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument e.stopPropagation(); } + // ref for updating time setRef = (e: HTMLAudioElement | null) => { e?.addEventListener("timeupdate", this.timecodeChanged); e?.addEventListener("ended", this.pause); this._ele = e; } + // returns the path of the audio file @computed get path() { const field = Cast(this.props.Document[this.props.fieldKey], AudioField); const path = (field instanceof AudioField) ? field.url.href : ""; return path === nullAudio ? "" : path; } + // returns the html audio element @computed get audio() { const interactive = this.active() ? "-interactive" : ""; return <audio ref={this.setRef} className={`audiobox-control${interactive}`}> @@ -222,33 +329,390 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument </audio>; } + // pause the time during recording phase + @action + recordPause = (e: React.MouseEvent) => { + this._pauseStart = new Date().getTime(); + this._paused = true; + this._recorder.pause(); + e.stopPropagation(); + + } + + // continue the recording + @action + recordPlay = (e: React.MouseEvent) => { + this._pauseEnd = new Date().getTime(); + this._paused = false; + this._recorder.resume(); + e.stopPropagation(); + + } + + // return the total time paused to update the correct time + @computed get pauseTime() { + return (this._pauseEnd - this._pauseStart); + } + + // creates a new label + @action + newMarker(marker: Doc) { + marker.data = ""; + if (this.dataDoc[this.annotationKey]) { + this.dataDoc[this.annotationKey].push(marker); + } else { + this.dataDoc[this.annotationKey] = new List<Doc>([marker]); + } + } + + // the starting time of the marker + start(startingPoint: number) { + this._hold = true; + this._start = startingPoint; + } + + // creates a new marker + @action + end(marker: number) { + this._hold = false; + const newMarker = Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: false, useLinkSmallAnchor: true, hideLinkButton: true, audioStart: this._start, audioEnd: marker, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document }); + newMarker.data = ""; + if (this.dataDoc[this.annotationKey]) { + this.dataDoc[this.annotationKey].push(newMarker); + } else { + this.dataDoc[this.annotationKey] = new List<Doc>([newMarker]); + } + + this._start = 0; + } + + // starting the drag event for marker resizing + onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = true; + this._currMarker = m; + this._timeline?.setPointerCapture(e.pointerId); + this._left = left; + + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + // ending the drag event for marker resizing + @action + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = false; + this._dragging = false; + + const rect = (e.target as any).getBoundingClientRect(); + this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + + this._timeline?.releasePointerCapture(e.pointerId); + + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + // resizes the marker while dragging + onPointerMove = async (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (!this._isPointerDown) { + return; + } + + const rect = await (e.target as any).getBoundingClientRect(); + + const newTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + + this.changeMarker(this._currMarker, newTime); + } + + // updates the marker with the new time + @action + changeMarker = (m: any, time: any) => { + DocListCast(this.dataDoc[this.annotationKey]).forEach((marker: Doc) => { + if (this.isSame(marker, m)) { + this._left ? marker.audioStart = time : marker.audioEnd = time; + } + }); + } + + // checks if the two markers are the same with start and end time + isSame = (m1: any, m2: any) => { + if (m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd) { + return true; + } + return false; + } + + // instantiates a new array of size 500 for marker layout + markers = () => { + const increment = NumCast(this.layoutDoc.duration) / 500; + this._count = []; + for (let i = 0; i < 500; i++) { + this._count.push([increment * i, 0]); + } + + } + + // makes sure no markers overlaps each other by setting the correct position and width + isOverlap = (m: any) => { + if (this._first) { + this._first = false; + this.markers(); + } + let max = 0; + + for (let i = 0; i < 500; i++) { + if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) { + this._count[i][1]++; + + if (this._count[i][1] > max) { + max = this._count[i][1]; + } + } + } + + for (let i = 0; i < 500; i++) { + if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) { + this._count[i][1] = max; + } + + } + + if (this.dataDoc.markerAmount < max) { + this.dataDoc.markerAmount = max; + } + return max - 1; + } + + // returns the audio waveform + @computed get waveform() { + return <Waveform + color={"darkblue"} + height={this._height} + barWidth={0.1} + // pos={this.layoutDoc.currentTimecode} + pos={this.dataDoc.duration} + duration={this.dataDoc.duration} + peaks={this._buckets.length === 100 ? this._buckets : undefined} + progressColor={"blue"} />; + } + + // decodes the audio file into peaks for generating the waveform + @action + buckets = async () => { + const audioCtx = new (window.AudioContext)(); + + axios({ url: this.path, responseType: "arraybuffer" }) + .then(response => { + const audioData = response.data; + + audioCtx.decodeAudioData(audioData, action(buffer => { + const decodedAudioData = buffer.getChannelData(0); + const NUMBER_OF_BUCKETS = 100; + const bucketDataSize = Math.floor(decodedAudioData.length / NUMBER_OF_BUCKETS); + + for (let i = 0; i < NUMBER_OF_BUCKETS; i++) { + const startingPoint = i * bucketDataSize; + const endingPoint = i * bucketDataSize + bucketDataSize; + let max = 0; + for (let j = startingPoint; j < endingPoint; j++) { + if (decodedAudioData[j] > max) { + max = decodedAudioData[j]; + } + } + const size = Math.abs(max); + this._buckets.push(size / 2); + } + + })); + }); + } + + // Returns the peaks of the audio waveform + @computed get peaks() { + return this.buckets(); + } + + // for updating the width and height of the waveform with timeline ref + timelineRef = (timeline: HTMLDivElement) => { + const observer = new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { + this.update(entry.contentRect.width, entry.contentRect.height); + this._position = entry.contentRect.width; + } + })); + timeline && observer.observe(timeline); + + this._timeline = timeline; + } + + // update the width and height of the audio waveform + @action + update = (width: number, height: number) => { + if (height) { + this._height = 0.8 * NumCast(this.layoutDoc._height); + const canvas2 = document.getElementsByTagName("canvas")[0]; + if (canvas2) { + const oldWidth = canvas2.width; + const oldHeight = canvas2.height; + canvas2.style.height = `${this._height}`; + canvas2.style.width = `${width}`; + + const ratio1 = oldWidth / window.innerWidth; + const ratio2 = oldHeight / window.innerHeight; + const context = canvas2.getContext('2d'); + if (context) { + context.scale(ratio1, ratio2); + } + } + + const canvas1 = document.getElementsByTagName("canvas")[1]; + if (canvas1) { + const oldWidth = canvas1.width; + const oldHeight = canvas1.height; + canvas1.style.height = `${this._height}`; + canvas1.style.width = `${width}`; + + const ratio1 = oldWidth / window.innerWidth; + const ratio2 = oldHeight / window.innerHeight; + const context = canvas1.getContext('2d'); + if (context) { + context.scale(ratio1, ratio2); + } + + const parent = canvas1.parentElement; + if (parent) { + parent.style.width = `${width}`; + parent.style.height = `${this._height}`; + } + } + } + } + + rangeScript = () => AudioBox.RangeScript; + + labelScript = () => AudioBox.LabelScript; + + // for indicating the first marker that is rendered + reset = () => this._first = true; + render() { const interactive = this.active() ? "-interactive" : ""; + this.reset(); + this.path && this._buckets.length !== 100 ? this.peaks : null; // render waveform if audio is done recording return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}> {!this.path ? <div className="audiobox-buttons"> <div className="audiobox-dictation" onClick={this.onFile}> <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> - <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this.audioState === "recording" ? "red" : "black" }}> - {this.audioState === "recording" ? "STOP" : "RECORD"} - </button> + {this.audioState === "recording" ? + <div className="recording" onClick={e => e.stopPropagation()}> + <div className="buttons" onClick={this.recordClick}> + <FontAwesomeIcon style={{ width: "100%" }} icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + </div> + <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}> + <FontAwesomeIcon style={{ width: "100%" }} icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + </div> + <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}</div> + </div> + : + <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}> + RECORD + </button>} </div> : - <div className="audiobox-controls"> - <div className="audiobox-player" onClick={this.onPlay}> - <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.layoutDoc.playOnSelect ? "yellow" : "dimGray" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-timeline" onClick={e => e.stopPropagation()} + <div className="audiobox-controls" > + <div className="audiobox-dictation"></div> + <div className="audiobox-player" > + <div className="audiobox-playhead" title={this.audioState === "paused" ? "play" : "pause"} onClick={this.onPlay}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div> + <div className="audiobox-timeline" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} onPointerDown={e => { + e.stopPropagation(); + e.preventDefault(); if (e.button === 0 && !e.ctrlKey) { const rect = (e.target as any).getBoundingClientRect(); - const wasPaused = this.audioState === "paused"; + + if (e.target as HTMLElement !== document.getElementById("current")) { + const wasPaused = this.audioState === "paused"; + this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + wasPaused && this.pause(); + } + } + if (e.button === 0 && e.altKey) { + this.newMarker(Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart)`) as any, useLinkSmallAnchor: true, hideLinkButton: true, isLabel: true, audioStart: this._ele!.currentTime, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document })); + } + + if (e.button === 0 && e.shiftKey) { + const rect = (e.target as any).getBoundingClientRect(); this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); - wasPaused && this.pause(); - e.stopPropagation(); + this._hold ? this.end(this._ele!.currentTime) : this.start(this._ele!.currentTime); } - }} > + }}> + <div className="waveform" id="waveform" style={{ height: `${100}%`, width: "100%", bottom: "0px" }}> + {this.waveform} + </div> + {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => { + let rect; + (!m.isLabel) ? + (this.layoutDoc.hideMarkers) ? (null) : + rect = + <div key={i} id={"audiobox-marker-container1"} className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container1"} + title={`${formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${formatTime(Math.round(NumCast(m.audioEnd)))}`} + style={{ + left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%`, + width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / NumCast(this.dataDoc.duration, 1) * 100}%`, height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%`, + top: `${this.isOverlap(m) * 1 / (this.dataDoc.markerAmount + 1) * 100}%` + }} + onClick={e => { this.playFrom(NumCast(m.audioStart), NumCast(m.audioEnd)); e.stopPropagation(); }} > + <div className="left-resizer" onPointerDown={e => this.onPointerDown(e, m, true)}></div> + <DocumentView {...this.props} + Document={m} + pointerEvents={true} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnFalse} + LayoutTemplate={undefined} + ContainingCollectionDoc={this.props.Document} + removeDocument={this.removeDocument} + parentActive={returnTrue} + onClick={this.layoutDoc.playOnClick ? this.rangeScript : undefined} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + <div className="resizer" onPointerDown={e => this.onPointerDown(e, m, false)}></div> + </div> + : + (this.layoutDoc.hideLabels) ? (null) : + rect = + <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={i} style={{ left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%` }}> + <DocumentView {...this.props} + Document={m} + pointerEvents={true} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnFalse} + LayoutTemplate={undefined} + ContainingCollectionDoc={this.props.Document} + removeDocument={this.removeDocument} + parentActive={returnTrue} + onClick={this.layoutDoc.playOnClick ? this.labelScript : undefined} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + </div>; + return rect; + })} {DocListCast(this.dataDoc.links).map((l, i) => { + let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; let linkTime = NumCast(l.anchor2_timecode); @@ -257,32 +721,45 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument la2 = l.anchor1 as Doc; linkTime = NumCast(l.anchor1_timecode); } + + if (la2.audioStart && !la2.audioEnd) { + linkTime = NumCast(la2.audioStart); + } + return !linkTime ? (null) : - <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}> - <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}> - <DocumentView {...this.props} - Document={l} - NativeHeight={returnZero} - NativeWidth={returnZero} - rootSelected={returnFalse} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)} - ContainingCollectionDoc={this.props.Document} - dontRegisterView={true} - parentActive={returnTrue} - bringToFront={emptyFunction} - backgroundColor={returnTransparent} /> - </div> - <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} - onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); wasPaused && this.pause(); e.stopPropagation(); } }} /> + <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }} onClick={e => e.stopPropagation()}> + <DocumentView {...this.props} + Document={l} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnFalse} + ContainingCollectionDoc={this.props.Document} + parentActive={returnTrue} + bringToFront={emptyFunction} + backgroundColor={returnTransparent} + ContentScaling={returnOne} + forcedBackgroundColor={returnTransparent} + pointerEvents={false} + LayoutTemplate={undefined} + LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)} + /> + <div key={i} className={`audiobox-marker`} onPointerEnter={() => Doc.linkFollowHighlight(la1)} + onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); e.stopPropagation(); e.preventDefault(); } }} /> </div>; })} - <div className="audiobox-current" style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} /> + <div className="audiobox-current" id="current" onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%`, pointerEvents: "none" }} /> {this.audio} </div> + <div className="current-time"> + {formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))} + </div> + <div className="total-time"> + {formatTime(Math.round(NumCast(this.dataDoc.duration)))} + </div> </div> </div> } </div>; } -}
\ No newline at end of file +} +Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index ce39c3735..42a42ddf1 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -11,10 +11,12 @@ import { Document } from "../../../fields/documentSchemas"; import { TraceMobx } from "../../../fields/util"; import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; import { List } from "../../../fields/List"; -import { numberRange } from "../../../Utils"; +import { numberRange, smoothScroll } from "../../../Utils"; import { ComputedField } from "../../../fields/ScriptField"; import { listSpec } from "../../../fields/Schema"; import { DocumentType } from "../../documents/DocumentTypes"; +import { Zoom, Fade, Flip, Rotate, Bounce, Roll, LightSpeed } from 'react-reveal'; +import { PresBox } from "./PresBox"; import { InkingStroke } from "../InkingStroke"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @@ -74,33 +76,84 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static getValues(doc: Doc, time: number) { const timecode = Math.round(time); return ({ + h: Cast(doc["h-indexed"], listSpec("number"), [NumCast(doc._height)]).reduce((p, h, i) => (i <= timecode && h !== undefined) || p === undefined ? h : p, undefined as any as number), + w: Cast(doc["w-indexed"], listSpec("number"), [NumCast(doc._width)]).reduce((p, w, i) => (i <= timecode && w !== undefined) || p === undefined ? w : p, undefined as any as number), x: Cast(doc["x-indexed"], listSpec("number"), [NumCast(doc.x)]).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number), y: Cast(doc["y-indexed"], listSpec("number"), [NumCast(doc.y)]).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number), + scroll: Cast(doc["scroll-indexed"], listSpec("number"), [NumCast(doc._scrollTop, 0)]).reduce((p, s, i) => (i <= timecode && s !== undefined) || p === undefined ? s : p, undefined as any as number), opacity: Cast(doc["opacity-indexed"], listSpec("number"), [NumCast(doc.opacity, 1)]).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number), }); } - public static setValues(time: number, d: Doc, x?: number, y?: number, opacity?: number) { + public static setValues(time: number, d: Doc, x?: number, y?: number, h?: number, w?: number, scroll?: number, opacity?: number) { const timecode = Math.round(time); + const hindexed = Cast(d["h-indexed"], listSpec("number"), []).slice(); + const windexed = Cast(d["w-indexed"], listSpec("number"), []).slice(); const xindexed = Cast(d["x-indexed"], listSpec("number"), []).slice(); const yindexed = Cast(d["y-indexed"], listSpec("number"), []).slice(); const oindexed = Cast(d["opacity-indexed"], listSpec("number"), []).slice(); + const scrollIndexed = Cast(d["scroll-indexed"], listSpec("number"), []).slice(); xindexed[timecode] = x as any as number; yindexed[timecode] = y as any as number; + hindexed[timecode] = h as any as number; + windexed[timecode] = w as any as number; oindexed[timecode] = opacity as any as number; + scrollIndexed[timecode] = scroll as any as number; d["x-indexed"] = new List<number>(xindexed); d["y-indexed"] = new List<number>(yindexed); + d["h-indexed"] = new List<number>(hindexed); + d["w-indexed"] = new List<number>(windexed); d["opacity-indexed"] = new List<number>(oindexed); + d["scroll-indexed"] = new List<number>(scrollIndexed); + if (d.appearFrame) { + if (d.appearFrame === timecode + 1) { + d["text-color"] = "red"; + } else if (d.appearFrame < timecode + 1) { + d["text-color"] = "grey"; + } else { d["text-color"] = "black"; } + } else if (d.appearFrame === 0) { + d["text-color"] = "black"; + } + } + + public static updateScrollframe(doc: Doc, time: number) { + const timecode = Math.round(time); + const scrollIndexed = Cast(doc['scroll-indexed'], listSpec("number"), null); + scrollIndexed?.length <= timecode + 1 && scrollIndexed.push(undefined as any as number); + setTimeout(() => doc.dataTransition = "inherit", 1010); + } + + public static setupScroll(doc: Doc, timecode: number, scrollProgressivize: boolean = false) { + const scrollList = new List<number>(); + scrollList[timecode] = NumCast(doc._scrollTop); + doc["scroll-indexed"] = scrollList; + doc.activeFrame = ComputedField.MakeFunction("self.currentFrame"); + doc._scrollTop = ComputedField.MakeInterpolated("scroll", "activeFrame"); } + + public static updateKeyframe(docs: Doc[], time: number) { const timecode = Math.round(time); docs.forEach(doc => { const xindexed = Cast(doc['x-indexed'], listSpec("number"), null); const yindexed = Cast(doc['y-indexed'], listSpec("number"), null); + const hindexed = Cast(doc['h-indexed'], listSpec("number"), null); + const windexed = Cast(doc['w-indexed'], listSpec("number"), null); const opacityindexed = Cast(doc['opacity-indexed'], listSpec("number"), null); + hindexed?.length <= timecode + 1 && hindexed.push(undefined as any as number); + windexed?.length <= timecode + 1 && windexed.push(undefined as any as number); xindexed?.length <= timecode + 1 && xindexed.push(undefined as any as number); yindexed?.length <= timecode + 1 && yindexed.push(undefined as any as number); opacityindexed?.length <= timecode + 1 && opacityindexed.push(undefined as any as number); + if (doc.appearFrame) { + if (doc.appearFrame === timecode + 1) { + doc["text-color"] = "red"; + } else if (doc.appearFrame < timecode + 1) { + doc["text-color"] = "grey"; + } else { doc["text-color"] = "black"; } + } else if (doc.appearFrame === 0) { + doc["text-color"] = "black"; + } doc.dataTransition = "all 1s"; }); setTimeout(() => docs.forEach(doc => doc.dataTransition = "inherit"), 1010); @@ -111,18 +164,49 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF setTimeout(() => docs.forEach(doc => doc.dataTransition = "inherit"), 1010); } + public static setupZoom(doc: Doc, zoomProgressivize: boolean = false) { + const width = new List<number>(); + const height = new List<number>(); + const top = new List<number>(); + const left = new List<number>(); + width.push(NumCast(doc.width)); + height.push(NumCast(doc.height)); + top.push(NumCast(doc.height) / -2); + left.push(NumCast(doc.width) / -2); + doc["viewfinder-width-indexed"] = width; + doc["viewfinder-height-indexed"] = height; + doc["viewfinder-top-indexed"] = top; + doc["viewfinder-left-indexed"] = left; + } + public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { docs.forEach((doc, i) => { + if (doc.appearFrame === undefined) doc.appearFrame = i; const curTimecode = progressivize ? i : timecode; const xlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); const ylist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < i ? 0 : 1)); + const wlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); + const hlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); + const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < (doc.appearFrame ? doc.appearFrame : i) ? 0 : 1)); + const oarray = olist; + oarray.fill(0, 0, NumCast(doc.appearFrame) - 1); + oarray.fill(1, NumCast(doc.appearFrame), timecode); + // oarray.fill(0, 0, NumCast(doc.appearFrame) - 1); + // oarray.fill(1, NumCast(doc.appearFrame), timecode);\ + wlist[curTimecode] = NumCast(doc._width); + hlist[curTimecode] = NumCast(doc._height); xlist[curTimecode] = NumCast(doc.x); ylist[curTimecode] = NumCast(doc.y); + doc.xArray = xlist; + doc.yArray = ylist; doc["x-indexed"] = xlist; doc["y-indexed"] = ylist; - doc["opacity-indexed"] = olist; + doc["w-indexed"] = wlist; + doc["h-indexed"] = hlist; + doc["opacity-indexed"] = oarray; doc.activeFrame = ComputedField.MakeFunction("self.context?.currentFrame||0"); + doc._height = ComputedField.MakeInterpolated("h", "activeFrame"); + doc._width = ComputedField.MakeInterpolated("w", "activeFrame"); doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); doc.y = ComputedField.MakeInterpolated("y", "activeFrame"); doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame"); @@ -135,6 +219,44 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF this.props.Document.y = NumCast(this.props.Document.y) + y; } + @computed get freeformNodeDiv() { + const node = <DocumentView {...this.props} + nudge={this.nudge} + dragDivName={"collectionFreeFormDocumentView-container"} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + backgroundColor={this.props.backgroundColor} + opacity={this.opacity} + NativeHeight={this.NativeHeight} + NativeWidth={this.NativeWidth} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} />; + if (PresBox.Instance && this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc) { + const effectProps = { + left: this.layoutDoc.presEffectDirection === 'left', + right: this.layoutDoc.presEffectDirection === 'right', + top: this.layoutDoc.presEffectDirection === 'top', + bottom: this.layoutDoc.presEffectDirection === 'bottom', + opposite: true, + delay: this.layoutDoc.presTransition, + // when: this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc, + }; + switch (this.layoutDoc.presEffect) { + case "Zoom": return (<Zoom {...effectProps}>{node}</Zoom>); break; + case "Fade": return (<Fade {...effectProps}>{node}</Fade>); break; + case "Flip": return (<Flip {...effectProps}>{node}</Flip>); break; + case "Rotate": return (<Rotate {...effectProps}>{node}</Rotate>); break; + case "Bounce": return (<Bounce {...effectProps}>{node}</Bounce>); break; + case "Roll": return (<Roll {...effectProps}>{node}</Roll>); break; + case "LightSpeed": return (<LightSpeed {...effectProps}>{node}</LightSpeed>); break; + case "None": return node; break; + default: return node; break; + } + } else { + return node; + } + } + contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox && !this.freezeDimensions ? this.width / this.nativeWidth : 1; panelWidth = () => (this.sizeProvider?.width || this.props.PanelWidth?.()); panelHeight = () => (this.sizeProvider?.height || this.props.PanelHeight?.()); @@ -165,6 +287,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF display: this.ZInd === -99 ? "none" : undefined, pointerEvents: this.props.Document.isBackground || this.Opacity === 0 || this.props.Document.type === DocumentType.INK || this.props.Document.isInkMask ? "none" : this.props.pointerEvents ? "all" : undefined }} > + {Doc.UserDoc().renderStyle !== "comic" ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }}> <svg style={{ transform: `scale(1,${this.props.PanelHeight() / this.props.PanelWidth()})`, transformOrigin: "top left", overflow: "visible" }} viewBox="0 0 12 14"> @@ -174,17 +297,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF </div>} {!this.props.fitToBox ? - <DocumentView {...this.props} - nudge={this.nudge} - dragDivName={"collectionFreeFormDocumentView-container"} - ContentScaling={this.contentScaling} - ScreenToLocalTransform={this.getTransform} - backgroundColor={this.props.backgroundColor} - opacity={this.opacity} - NativeHeight={this.NativeHeight} - NativeWidth={this.NativeWidth} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} /> + <>{this.freeformNodeDiv}</> : <ContentFittingDocumentView {...this.props} ContainingCollectionDoc={this.props.ContainingCollectionDoc} DataDoc={this.props.DataDoc} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index e8173d103..2408b3906 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -18,13 +18,14 @@ import { DocHolderBox } from "./DocHolderBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FontIconBox } from "./FontIconBox"; +import { MenuIconBox } from "./MenuIconBox"; import { FieldView, FieldViewProps } from "./FieldView"; import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { ImageBox } from "./ImageBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; -import { QueryBox } from "./QueryBox"; +import { SearchBox } from "../search/SearchBox"; import { ColorBox } from "./ColorBox"; import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { LinkAnchorBox } from "./LinkAnchorBox"; @@ -189,9 +190,9 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { blacklistedAttrs={[]} renderInWrapper={false} components={{ - FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView, + FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, MenuIconBox, LabelBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, - PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox, + PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, ScreenshotBox, HTMLtag, ComparisonBox }} diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 97e714cd5..9328fb96b 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -28,7 +28,13 @@ } .documentLinksButton { - background-color: $link-color; + background-color: black; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } } .documentLinksButton-endLink { diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 3057d87e8..e8f7c8e9f 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -84,7 +84,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; - DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; } })); } @@ -103,7 +102,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp //action(() => Doc.BrushDoc(this.props.View.Document)); } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; - DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; } } @@ -155,43 +153,39 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLink = undefined; DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; - } else { - if (!this.props.StartLink && DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { - const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink }, { doc: this.props.View.props.Document }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); - // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved - DocumentLinksButton.StartLink._link = this.props.View._link = linkDoc; - setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); - LinkManager.currentLink = linkDoc; - - if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation - const targetDoc = this.props.View.props.Document; - Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; - Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; - Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; - Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId, DocumentLinksButton.StartLink); // edit annotation to add a Dash hyperlink to the linked doc - } + } else if (!this.props.StartLink && DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink }, { doc: this.props.View.props.Document }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); + // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved + DocumentLinksButton.StartLink._link = this.props.View._link = linkDoc; + setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); + LinkManager.currentLink = linkDoc; + + if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation + const targetDoc = this.props.View.props.Document; + Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; + Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; + Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; + Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId, DocumentLinksButton.StartLink); // edit annotation to add a Dash hyperlink to the linked doc + } - if (linkDoc) { - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = screenX; - TaskCompletionBox.popupY = screenY - 133; - TaskCompletionBox.taskCompleted = true; + if (linkDoc) { + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = screenX; + TaskCompletionBox.popupY = screenY - 133; + TaskCompletionBox.taskCompleted = true; - if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { - LinkDescriptionPopup.popupX = screenX; - LinkDescriptionPopup.popupY = screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; - } - setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); + if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { + LinkDescriptionPopup.popupX = screenX; + LinkDescriptionPopup.popupY = screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; } + setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); } } })); @observable public static EditLink: DocumentView | undefined; - public static EditLinkLoc: number[] = [0, 0]; - @action clearLinks() { DocumentLinksButton.StartLink = undefined; @@ -224,7 +218,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp const linkButton = <div ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> <div className={"documentLinksButton"} style={{ - backgroundColor: this.props.InMenu ? "black" : "", + backgroundColor: this.props.InMenu ? "" : "#add8e6", color: this.props.InMenu ? "white" : "black", width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", fontWeight: "bold" }} @@ -245,23 +239,26 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp link : links.length} </div> - {this.props.InMenu && !this.props.StartLink && - DocumentLinksButton.StartLink !== this.props.View.props.Document ? <div className={"documentLinksButton-endLink"} + {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? + <div className={"documentLinksButton-endLink"} style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", backgroundColor: DocumentLinksButton.StartLink ? "" : "grey", border: DocumentLinksButton.StartLink ? "" : "none" }} onPointerDown={DocumentLinksButton.StartLink ? this.completeLink : emptyFunction} - onClick={e => DocumentLinksButton.StartLink ? this.finishLinkClick(e.screenX, e.screenY) : emptyFunction} /> : (null)} - {DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} - style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} - onPointerDown={this.clearLinks} onClick={this.clearLinks} - /> : (null)} + onClick={e => DocumentLinksButton.StartLink ? this.finishLinkClick(e.screenX, e.screenY) : emptyFunction} /> : (null) + } + { + DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} + style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} + onPointerDown={this.clearLinks} onClick={this.clearLinks} + /> : (null) + } </div >; return (!links.length) && !this.props.AlwaysOn ? (null) : - this.props.InMenu && (this.props.StartLink || DocumentLinksButton.StartLink) ? + this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> {linkButton} </Tooltip> : !!!DocumentLinksButton.EditLink && !this.props.InMenu ? diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a195f2813..444583af3 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -179,7 +179,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); + const effectiveAcl = GetEffectiveAcl(this.props.Document); + (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 }); @@ -570,6 +571,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu alert("Can't delete the active workspace"); } else { SelectionManager.DeselectAll(); + this.props.Document.deleted = true; this.props.removeDocument?.(this.props.Document); } } @@ -749,7 +751,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu moreItems.push({ description: "Download document", icon: "download", event: async () => Doc.Zip(this.props.Document) }); moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "users" }); //moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); - moreItems.push({ description: "Create an Alias", event: () => this.onCopy(), icon: "copy" }); + //moreItems.push({ description: "Create an Alias", event: () => this.onCopy(), icon: "copy" }); if (!Doc.UserDoc().noviceMode) { 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" }); @@ -760,8 +762,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); } moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); + Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()) && moreItems.push({ description: "Toggle Always Show Link End", event: () => Doc.UserDoc()["documentLinksButton-hideEnd"] = !Doc.UserDoc()["documentLinksButton-hideEnd"], icon: "eye" }); } - //GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); const effectiveAcl = GetEffectiveAcl(this.props.Document); (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); @@ -890,20 +892,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView ? (null) : // view that are not registered DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => - <div className="documentView-anchorCont" key={i + 1}> <DocumentView {...this.props} - Document={d} - ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox - PanelWidth={this.anchorPanelWidth} - PanelHeight={this.anchorPanelHeight} - ContentScaling={returnOne} - dontRegisterView={false} - forcedBackgroundColor={returnTransparent} - removeDocument={this.hideLinkAnchor} - pointerEvents={false} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} - /></div >); + <div className="documentView-anchorCont" key={i + 1}> + <DocumentView {...this.props} + Document={d} + ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox + PanelWidth={this.anchorPanelWidth} + PanelHeight={this.anchorPanelHeight} + ContentScaling={returnOne} + dontRegisterView={false} + forcedBackgroundColor={returnTransparent} + removeDocument={this.hideLinkAnchor} + pointerEvents={false} + LayoutTemplate={undefined} + LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} /> + </div >); } @computed get innards() { TraceMobx(); @@ -997,13 +1000,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); const borderRounding = this.layoutDoc.borderRounding; const localScale = fullDegree; - const highlightColors = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] : ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear && this.props.Document.type !== DocumentType.INK; - highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way + highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way const topmost = this.topMost ? "-topmost" : ""; return <div className={`documentView-node${topmost}`} id={this.props.Document[Id]} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 23ae48108..e631ad5fe 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -45,14 +45,22 @@ export interface FieldViewProps { whenActiveChanged: (isActive: boolean) => void; dontRegisterView?: boolean; focus: (doc: Doc) => void; + presMultiSelect?: (doc: Doc) => void; //added for selecting multiple documents in a presentation ignoreAutoHeight?: boolean; PanelWidth: () => number; PanelHeight: () => number; + PanelPosition?: string; + overflow?: boolean; NativeHeight: () => number; NativeWidth: () => number; setVideoBox?: (player: VideoBox) => void; ContentScaling: () => number; + ChromeHeight?: () => number; + childLayoutTemplate?: () => Opt<Doc>; + highlighting?: string[]; + lines?: string[]; + doc?: Doc; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) height?: number; width?: number; diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 5bdafd857..9709e1dbd 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -15,12 +15,17 @@ .menuButton-round { border-radius: 100%; + background-color: black; .fontIconBox-label { margin-left: -10px; // button padding is 10px; bottom: 0; position: absolute; } + + &:hover { + background-color: #aaaaa3; + } } .menuButton-square { diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index eff5a4160..c0eb78d98 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -63,16 +63,14 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( const color = StrCast(this.layoutDoc.color, this._foregroundColor); const backgroundColor = StrCast(this.layoutDoc._backgroundColor, StrCast(this.rootDoc.backgroundColor, this.props.backgroundColor?.(this.rootDoc))); const shape = StrCast(this.layoutDoc.iconShape, "round"); - const button = <> - <button className={`menuButton-${shape}`} ref={this._ref} onContextMenu={this.specificContextMenu} - style={{ boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined, backgroundColor }}> - <div className="menuButton-wrap"> - {<FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={StrCast(this.dataDoc.icon, "user") as any} color={color} - size={this.layoutDoc.iconShape === "square" ? "sm" : "lg"} />} - {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>} - </div> - </button> - </>; + const button = <button className={`menuButton-${shape}`} ref={this._ref} onContextMenu={this.specificContextMenu} + style={{ boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined, backgroundColor: this.layoutDoc.iconShape === "square" ? backgroundColor : "" }}> + <div className="menuButton-wrap"> + {<FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={StrCast(this.dataDoc.icon, "user") as any} color={color} + size={this.layoutDoc.iconShape === "square" ? "sm" : "lg"} />} + {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>} + </div> + </button>; return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}> {button} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 05ba6628c..826ccd340 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,4 +1,4 @@ -import { action } from 'mobx'; +import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; @@ -56,16 +56,26 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument e.stopPropagation(); } } + + @observable _mouseOver = false; + @computed get backColor() { return this.clicked || this._mouseOver ? StrCast(this.layoutDoc.hovercolor) : "unset"; } + + @observable clicked = false; // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")") render() { const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... return ( - <div className="labelBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} + <div className="labelBox-outerDiv" + onClick={action(() => this.clicked = !this.clicked)} + onMouseLeave={action(() => this._mouseOver = false)} + onMouseOver={action(() => this._mouseOver = true)} + ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this.layoutDoc.opacity ? StrCast(this.layoutDoc.boxShadow) : "" }}> <div className="labelBox-mainButton" style={{ background: StrCast(this.layoutDoc.backgroundColor), + backgroundColor: this.backColor, color: StrCast(this.layoutDoc.color, "inherit"), fontSize: StrCast(this.layoutDoc._fontSize) || "inherit", fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit", diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index be6292bb6..50b2af0d7 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -119,7 +119,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch const y = NumCast(this.rootDoc[this.fieldKey + "_y"], 100); const c = StrCast(this.layoutDoc._backgroundColor, StrCast(this.layoutDoc.backgroundColor, StrCast(this.dataDoc.backgroundColor, "lightBlue"))); // note this is not where the typical lightBlue default color comes from. See Documents.Create.LinkDocument() const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1"; - const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; + const anchorScale = !this.dataDoc[this.fieldKey + "-useLinkSmallAnchor"] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; const timecode = this.dataDoc[anchor + "_timecode"]; const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title) + (timecode !== undefined ? ":" + timecode : ""); diff --git a/src/client/views/nodes/MenuIconBox.scss b/src/client/views/nodes/MenuIconBox.scss new file mode 100644 index 000000000..1b72f5a8f --- /dev/null +++ b/src/client/views/nodes/MenuIconBox.scss @@ -0,0 +1,49 @@ +.menuButton { + //padding: 7px; + padding-left: 7px; + width: 100%; + width: 60px; + height: 70px; + + .menuButton-wrap { + width: 45px; + /* padding: 5px; */ + touch-action: none; + background: black; + transform-origin: top left; + /* margin-bottom: 5px; */ + margin-top: 5px; + margin-right: 25px; + border-radius: 8px; + + &:hover { + background: rgb(61, 61, 61); + cursor: pointer; + } + } + + .menuButton-label { + color: white; + margin-right: 4px; + border-radius: 8px; + width: 42px; + position: relative; + text-align: center; + font-size: 8px; + margin-top: 1px; + letter-spacing: normal; + padding: 3px; + background-color: inherit; + } + + .menuButton-icon { + width: auto; + height: 35px; + padding: 5px; + } + + svg { + width: 95% !important; + height: 95%; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/MenuIconBox.tsx b/src/client/views/nodes/MenuIconBox.tsx new file mode 100644 index 000000000..0aa7b327e --- /dev/null +++ b/src/client/views/nodes/MenuIconBox.tsx @@ -0,0 +1,33 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { createSchema, makeInterface } from '../../../fields/Schema'; +import { StrCast } from '../../../fields/Types'; +import { DocComponent } from '../DocComponent'; +import { FieldView, FieldViewProps } from './FieldView'; +import './MenuIconBox.scss'; +const MenuIconSchema = createSchema({ + icon: "string" +}); + +type MenuIconDocument = makeInterface<[typeof MenuIconSchema]>; +const MenuIconDocument = makeInterface(MenuIconSchema); +@observer +export class MenuIconBox extends DocComponent<FieldViewProps, MenuIconDocument>(MenuIconDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MenuIconBox, fieldKey); } + _ref: React.RefObject<HTMLButtonElement> = React.createRef(); + + render() { + + const color = this.props.backgroundColor?.(this.props.Document) === "lightgrey" ? "black" : "white"; + const menuBTN = <div className="menuButton" style={{ backgroundColor: this.props.backgroundColor?.(this.props.Document) }}> + <div className="menuButton-wrap" + style={{ backgroundColor: this.props.backgroundColor?.(this.props.Document) }} > + <FontAwesomeIcon className="menuButton-icon" icon={StrCast(this.dataDoc.icon, "user") as any} color={color} size="lg" /> + <div className="menuButton-label" style={{ color: color }}> {this.dataDoc.title} </div> + </div> + </div>; + + return menuBTN; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss index 9f6af1bde..a87b0e466 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/PresBox.scss @@ -1,7 +1,13 @@ +$light-blue: #AEDDF8; +$dark-blue: #5B9FDD; +$light-background: #ececec; + .presBox-cont { position: absolute; + display: block; pointer-events: inherit; z-index: 2; + font-family: Roboto; box-shadow: #AAAAAA .2vw .2vw .4vw; width: 100%; min-width: 20px; @@ -12,75 +18,837 @@ transition: 0.7s opacity ease; .presBox-listCont { - position: absolute; + position: relative; height: calc(100% - 25px); width: 100%; + margin-top: 3px; + } + + .presBox-toolbar-dropdown { + border-radius: 5px; + background-color: white; + transform: translate(8px, -5px); + box-shadow: 1px 1px 4px 4px rgba(0, 0, 0, 0.25); + z-index: 1000; + width: calc(100% - 50px); + height: max-content; + justify-self: center; + letter-spacing: normal; + height: max-content; + font-weight: 500; + position: relative; + font-size: 13; } - .presBox-buttons { + .presBox-toolbar { + position: relative; + display: inline-flex; + align-items: center; + height: 30px; width: 100%; - background: gray; - padding-top: 5px; - padding-bottom: 5px; + color: white; + background-color: #323232; + + .toolbar-button { + margin-left: 10px; + margin-right: 10px; + letter-spacing: 0; + display: flex; + align-items: center; + transition: 0.5s; + } + + .toolbar-button.active { + color: $light-blue; + } + + .toolbar-transitionButtons { + display: block; + + .toolbar-transition { + display: flex; + font-size: 10; + width: 100; + background-color: rgba(0, 0, 0, 0); + min-width: max-content; + + .toolbar-icon { + margin-right: 5px; + } + } + } + } + + .toolbar-moreInfo { + position: absolute; + right: 5px; + display: flex; + width: max-content; + height: 25px; + justify-content: center; + transform: rotate(90deg); + align-items: center; + transition: 0.7s ease; + + .toolbar-moreInfoBall { + width: 4px; + height: 4px; + border-radius: 100%; + background-color: white; + margin: 1px; + position: relative; + } + } + + .toolbar-moreInfo.active { + transform: rotate(0deg); + } + + .toolbar-divider { + border-left: solid #ffffff70 0.5px; + height: 20px; + } +} + +.dropdown { + font-size: 10; + margin-left: 5px; + color: darkgrey; + transition: 0.5s ease; +} + +.dropdown.active { + transform: rotate(180deg); + color: $light-blue; + opacity: 0.8; +} + +.presBox-ribbon { + position: relative; + display: inline; + font-family: Roboto; + color: black; + z-index: 100; + transition: 0.7s; + + .ribbon-doubleButton { + display: inline-flex; + } + + .presBox-reactiveGrid { display: grid; - grid-column-end: 4; - grid-column-start: 1; + justify-content: flex-start; + align-items: center; + grid-template-columns: repeat(auto-fit, 70px); + } - .presBox-viewPicker { - height: 25; + .ribbon-property { + font-size: 11; + font-weight: 200; + height: 20; + background-color: #ececec; + color: black; + border: solid 1px black; + display: flex; + margin-left: 5px; + margin-top: 5px; + margin-bottom: 5px; + margin-right: 5px; + width: max-content; + justify-content: center; + align-items: center; + padding-right: 10px; + padding-left: 10px; + } + + .presBox-subheading { + font-size: 11; + font-weight: 400; + margin-top: 10px; + } + + @media screen and (-webkit-min-device-pixel-ratio:0) { + .toolbar-slider { + margin-top: 5px; position: relative; - display: inline-block; - grid-column: 1/2; - min-width: 15px; + align-self: center; + justify-self: left; + overflow: hidden; + width: 100%; + height: 10px; + border-radius: 10px; + -webkit-appearance: none; + background-color: #ececec; } - select { - background: #323232; - color: white; + .toolbar-slider:focus { + outline: none; } - .presBox-button { - margin-right: 2.5%; - margin-left: 2.5%; - height: 25px; - border-radius: 5px; + .toolbar-slider::-webkit-slider-runnable-track { + height: 10px; + -webkit-appearance: none; + margin-top: -1px; + } + + .toolbar-slider::-webkit-slider-thumb { + width: 10px; + -webkit-appearance: none; + height: 10px; + cursor: ew-resize; + background: #5b9ddd; + box-shadow: -100vw 0 0 100vw #aedef8; + } + } + + .slider-headers { + position: relative; + display: grid; + justify-content: space-between; + width: 100%; + height: max-content; + grid-template-columns: auto auto auto; + grid-template-rows: max-content; + font-weight: 100; + margin-top: 3px; + font-size: 10px; + } + + .slider-value { + top: -20; + color: #2f86a2; + position: absolute; + } + + .slider-value.none, + .slider-headers.none, + .toolbar-slider.none { + display: none; + } + + .dropdown-header { + padding-bottom: 10px; + font-weight: 800; + text-align: center; + font-size: 16; + width: 90%; + color: black; + transform: translate(5%, 0px); + border-bottom: solid 2px darkgrey; + } + + + .ribbon-textInput { + border-radius: 2px; + height: 20px; + font-size: 11.5; + font-weight: 100; + align-self: center; + justify-self: left; + margin-top: 5px; + padding-left: 10px; + background-color: $light-background; + border: solid 1px black; + min-width: 80px; + max-width: 200px; + width: 100%; + } + + .ribbon-frameSelector { + border: black solid 1px; + width: 60px; + height: 20px; + margin-top: 5px; + display: grid; + grid-template-columns: auto 27px auto; + position: relative; + border-radius: 5px; + overflow: hidden; + align-items: center; + justify-self: left; + + .fwdKeyframe, + .backKeyframe { + cursor: pointer; + position: relative; + height: 100%; + background: $light-background; display: flex; align-items: center; - background: #323232; + justify-content: center; + text-align: center; + color: black; + } + + .numKeyframe { + font-size: 10; + font-weight: 600; + position: relative; + color: black; + display: flex; + width: 100%; + height: 100%; + text-align: center; + align-items: center; + justify-content: center; + } + } + + .ribbon-final-box { + align-self: flex-start; + justify-self: center; + display: grid; + margin-top: 10px; + grid-template-rows: auto auto; + /* padding-left: 10px; */ + /* padding-right: 10px; */ + letter-spacing: normal; + min-width: max-content; + width: 100%; + font-size: 13; + font-weight: 500; + position: relative; + + + .ribbon-final-button { + position: relative; + font-size: 11; + font-weight: normal; + letter-spacing: normal; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 5px; + height: 25px; color: white; + width: 100%; + max-width: 120; + padding-left: 10; + padding-right: 10; + border-radius: 10px; + background-color: #979797; + } + + .ribbon-final-button-hidden { + position: relative; + font-size: 11; + font-weight: normal; + letter-spacing: normal; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 5px; + height: 25px; + color: lightgrey; + width: 100%; + max-width: 120; + padding-left: 10; + padding-right: 10; + border-radius: 10px; + background-color: black; + } + } + + .selectedList { + display: block; + min-width: 50; + max-width: 120; + height: 70; + overflow-y: scroll; + + .selectedList-items { + font-size: 7; + font-weight: normal; + } + } + + .ribbon-button { + font-size: 10.5; + font-weight: 200; + height: 20; + background-color: $light-background; + border: solid 1px black; + display: flex; + margin-top: 5px; + margin-bottom: 5px; + border-radius: 5px; + margin-right: 5px; + width: max-content; + justify-content: center; + align-items: center; + padding-right: 10px; + padding-left: 10px; + } + + .ribbon-button.active { + background-color: #aedef8; + } + + .ribbon-button:hover { + background-color: lightgrey; + } + + svg.svg-inline--fa.fa-thumbtack.fa-w-12.toolbar-thumbtack { + right: 40; + position: absolute; + transform: rotate(45deg); + } + + .ribbon-box { + display: grid; + grid-template-rows: max-content auto; + justify-self: center; + margin-top: 10px; + /* padding-left: 10px; */ + padding-right: 10px; + letter-spacing: normal; + width: 100%; + /* max-width: 100%; */ + height: max-content; + font-weight: 500; + position: relative; + font-size: 13; + padding-bottom: 10px; + border-bottom: solid 1px darkgrey; - svg { - margin: auto; + .presBox-dropdown:hover { + border: solid 1px #378AD8; + border-bottom-left-radius: 0px; + + .presBox-dropdownOption { + font-size: 11; + display: block; + padding-left: 10px; + padding-right: 5px; + padding-top: 3; + padding-bottom: 3; + } + + .presBox-dropdownOption:hover { + position: relative; + background-color: lightgrey; + } + + .presBox-dropdownOption.active { + position: relative; + background-color: #aedef8; + } + + .presBox-dropdownOptions { + position: absolute; + top: 24px; + left: -1px; + z-index: 200; + width: 85%; + min-width: max-content; + display: block; + background: #FFFFFF; + border: 0.5px solid #979797; + box-sizing: border-box; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + } + + .presBox-dropdownIcon { + color: #378AD8; } } - .collectionViewBaseChrome-viewPicker { - min-width: 50; - width: 5%; + .presBox-dropdown { + display: grid; + grid-template-columns: auto 20%; + position: relative; + border: solid 1px black; + background-color: $light-background; + border-radius: 5px; + font-size: 10; height: 25; + padding-left: 5px; + align-items: center; + margin-top: 5px; + margin-bottom: 5px; + font-weight: 200; + width: 100%; + min-width: max-content; + max-width: 200px; + overflow: visible; + + .presBox-dropdownOptions { + display: none; + } + + .presBox-dropdownIcon { + position: relative; + color: black; + align-self: center; + justify-self: center; + margin-right: 2px; + } + } + } +} + +.presBox-ribbon.active { + display: grid; + grid-template-columns: auto auto auto auto auto; + grid-template-rows: 100%; + height: 100px; + padding-top: 5px; + padding-bottom: 5px; + border: solid 1px black; + // overflow: auto; + + ::-webkit-scrollbar { + -webkit-appearance: none; + height: 3px; + width: 8px; + } + + ::-webkit-scrollbar-thumb { + border-radius: 2px; + } +} + +.dropdown-play-button { + font-size: 12; + padding-left: 5px; + padding-right: 5px; + padding-top: 5px; + padding-bottom: 5px; + text-align: left; + justify-content: left; +} + +.dropdown-play-button:hover { + background-color: lightgrey; +} + +.presBox-button-left { + position: relative; + align-self: flex-start; + justify-self: flex-start; + width: 80%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + padding-left: 7px; + padding-right: 7px; + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.presBox-button-right { + position: relative; + text-align: center; + border-left: solid 1px darkgrey; + width: 20%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + padding-left: 7px; + padding-right: 7px; + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.presBox-button-right.active { + background-color: #223063; + border: #aedcf6 solid 1px; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.8); +} + +.dropdown-play { + right: 0px; + top: calc(100% + 2px); + display: none; + border-radius: 5px; + width: max-content; + min-height: 20px; + height: max-content; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.8); + z-index: 200; + background-color: white; + color: black; + position: absolute; + overflow: hidden; +} + +.dropdown-play.active { + display: block; +} + +.open-layout { + position: relative; + display: flex; + align-items: center; + justify-content: center; + transform: translate(0px, -1px); + background-color: $light-background; + width: 40px; + height: 15px; + align-self: center; + justify-self: center; + border: solid 1px black; + border-top: 0px; + border-bottom-right-radius: 7px; + border-bottom-left-radius: 7px; +} + +.layout-container { + padding: 5px; + display: grid; + background-color: $light-background; + grid-template-columns: repeat(auto-fit, minmax(90px, 100px)); + width: 100%; + border: solid 1px black; + min-width: 100px; + overflow: hidden; + + .layout:hover { + border: solid 2px #5c9edd; + } + + .layout { + align-self: center; + justify-self: center; + margin-top: 5; + margin-bottom: 5; + position: relative; + height: 55px; + min-width: 90px; + width: 90px; + overflow: hidden; + background-color: white; + border: solid darkgrey 1px; + display: grid; + grid-template-rows: auto; + align-items: center; + text-align: center; + + .title { + position: relative; + align-self: end; + padding-left: 3px; + margin-left: 3px; + margin-right: 3px; + height: 13; + font-size: 12; + display: flex; + background-color: #f1efec; + } + + .subtitle { + align-self: flex-start; + position: relative; + padding-left: 3px; + margin-left: 3px; + margin-right: 3px; + font-weight: 400; + height: 13; + font-size: 9; + display: flex; + background-color: #f1efec; + } + + .content { position: relative; - display: inline-block; + font-weight: 200; + align-self: flex-start; + padding-left: 3px; + margin-left: 3px; + margin-right: 3px; + height: 13; + font-size: 10; + display: flex; + background-color: #f1efec; + height: 33; + text-align: left; + font-size: 8px; } } +} + +.presBox-buttons { + position: relative; + width: 100%; + background: gray; + min-height: 35px; + padding-top: 5px; + padding-bottom: 5px; + display: grid; + grid-template-columns: auto auto; + + .presBox-viewPicker { + height: 25; + position: relative; + display: inline-block; + grid-column: 1; + border-radius: 5px; + min-width: 15px; + max-width: 100px; + left: 8px; + } - .presBox-backward, - .presBox-forward { - width: 25px; + .presBox-presentPanel { + display: flex; + justify-self: end; + width: 100%; + max-width: 300px; + min-width: 150px; + } + + + + select { + background: #323232; + color: white; + } + + .presBox-button { + height: 25px; border-radius: 5px; - top: 50%; + display: none; + justify-content: center; + align-content: center; + align-items: center; + text-align: center; + letter-spacing: normal; + width: inherit; + background: #323232; + color: white; + } + + .presBox-button.active { + display: flex; + } + + .presBox-button.active:hover { + background-color: #233163; + } + + .presBox-button.edit { + display: flex; + max-width: 25px; + } + + .presBox-button.present { + display: flex; + width: max-content; position: absolute; - display: inline-block; + right: 10px; + + .present-icon { + margin-right: 7px; + } } - .presBox-backward { - left: 5; + + .miniPresOverlay { + background-color: #323232; + color: white; + border-radius: 5px; + grid-template-rows: 100%; + height: 25; + width: max-content; + min-width: max-content; + justify-content: space-evenly; + align-items: center; + display: flex; + position: absolute; + right: 10px; + transition: all 0.2s; + + .miniPres-button-text { + display: flex; + height: 20; + width: max-content; + font-family: Roboto; + font-weight: 400; + margin-left: 3px; + margin-right: 3px; + padding-right: 3px; + padding-left: 3px; + letter-spacing: normal; + border-radius: 5px; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .miniPres-divider { + width: 0.5px; + height: 80%; + border-right: solid 1px #5a5a5a; + } + + .miniPres-button-frame { + justify-self: center; + align-self: center; + align-items: center; + display: grid; + grid-template-columns: auto auto auto; + justify-content: space-around; + font-size: 11; + margin-left: 7; + width: 30; + height: 85%; + background-color: rgba(91, 157, 221, 0.4); + border-radius: 5px; + } + + .miniPres-button { + display: flex; + height: 20; + min-width: 20; + margin-left: 3px; + margin-right: 3px; + border-radius: 100%; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .miniPres-button:hover { + background-color: #5a5a5a; + } + + .miniPres-button-text:hover { + background-color: #5a5a5a; + } } - .presBox-forward { - right: 5; + + + .collectionViewBaseChrome-viewPicker { + min-width: 50; + width: 5%; + height: 25; + position: relative; + display: inline-block; + left: 8px; } } +.presBox-backward, +.presBox-forward { + width: 25px; + border-radius: 5px; + top: 50%; + position: absolute; + display: inline-block; +} + +.presBox-backward { + left: 5; +} + +.presBox-forward { + right: 5; +} + // CSS adjusted for mobile devices @media only screen and (max-device-width: 480px) { .presBox-cont .presBox-buttons { diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index a304ced18..502fd51f3 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -1,25 +1,32 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, DocCastAsync } from "../../../fields/Doc"; +import { Doc, DocListCast, DocCastAsync, WidthSym } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { returnFalse, returnOne } from "../../../Utils"; +import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../fields/Types"; +import { returnFalse, returnOne, numberRange, returnTrue } from "../../../Utils"; import { documentSchema } from "../../../fields/documentSchemas"; import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionDockingView, DockedFrameRenderer } from "../collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; import { FieldView, FieldViewProps } from './FieldView'; +import { DocumentType } from "../../documents/DocumentTypes"; import "./PresBox.scss"; import { ViewBoxBaseComponent } from "../DocComponent"; -import { makeInterface } from "../../../fields/Schema"; -import { Docs } from "../../documents/Documents"; +import { makeInterface, listSpec } from "../../../fields/Schema"; +import { Docs, DocUtils } from "../../documents/Documents"; import { PrefetchProxy } from "../../../fields/Proxy"; import { ScriptField } from "../../../fields/ScriptField"; import { Scripting } from "../../util/Scripting"; -import { InkingStroke } from "../InkingStroke"; +import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; +import { List } from "../../../fields/List"; +import { Tooltip } from "@material-ui/core"; +import { CollectionFreeFormViewChrome } from "../collections/CollectionMenu"; +import { actionAsync } from "mobx-utils"; +import { SelectionManager } from "../../util/SelectionManager"; +import { AudioBox } from "./AudioBox"; type PresBoxSchema = makeInterface<[typeof documentSchema]>; const PresBoxDocument = makeInterface(documentSchema); @@ -27,218 +34,326 @@ const PresBoxDocument = makeInterface(documentSchema); @observer export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>(PresBoxDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); } + static Instance: PresBox; + @observable _isChildActive = false; + @observable _moveOnFromAudio: boolean = true; + @observable _presTimer!: NodeJS.Timeout; + + @observable _selectedArray: Doc[] = []; + @observable _sortedSelectedArray: Doc[] = []; + @observable _eleArray: HTMLElement[] = []; + @observable _dragArray: HTMLElement[] = []; + + @observable private transitionTools: boolean = false; + @observable private newDocumentTools: boolean = false; + @observable private progressivizeTools: boolean = false; + @observable private moreInfoTools: boolean = false; + @observable private playTools: boolean = false; + @observable private presentTools: boolean = false; + @observable private pathBoolean: boolean = false; + @observable private expandBoolean: boolean = false; + @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); } @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); } constructor(props: any) { super(props); + PresBox.Instance = this; if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations. Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ - title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data" + title: "pres element template", backgroundColor: "transparent", _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data" })); // this script will be called by each presElement to get rendering-specific info that the PresBox knows about but which isn't written to the PresElement // this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent // the preselement docs from being part of multiple presentations since they would all have the same field, or we'd have to keep per-presentation data - // stored on each pres element. + // stored on each pres element. (this.presElement as Doc).lookupField = ScriptField.MakeFunction("lookupPresBoxField(container, field, data)", { field: "string", data: Doc.name, container: Doc.name }); } this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox } + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else if (PresBox.Instance._selectedArray.length) { + return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); + } else { return undefined; } + } + @computed get isPres(): boolean { + if (this.selectedDoc?.type === DocumentType.PRES) { + document.addEventListener("keydown", this.keyEvents, true); + return true; + } else { + document.removeEventListener("keydown", this.keyEvents, true); + return false; + } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } componentDidMount() { this.rootDoc.presBox = this.rootDoc; this.rootDoc._forceRenderEngine = "timeline"; this.rootDoc._replacedChrome = "replaced"; + this.layoutDoc.presStatus = "edit"; + this.layoutDoc._gridGap = 5; + } + + updateCurrentPresentation = () => { + Doc.UserDoc().activePresentation = this.rootDoc; } - updateCurrentPresentation = () => Doc.UserDoc().activePresentation = this.rootDoc; + /** + * Called when the user moves to the next slide in the presentation trail. + */ @undoBatch @action next = () => { this.updateCurrentPresentation(); - const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); + const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null); + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const presTargetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const childDocs = DocListCast(presTargetDoc[Doc.LayoutFieldKey(presTargetDoc)]); + const currentFrame = Cast(presTargetDoc.currentFrame, "number", null); const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); const curFrame = NumCast(presTargetDoc.currentFrame); - if (lastFrame !== undefined && curFrame < lastFrame) { + let internalFrames: boolean = false; + if (presTargetDoc.presProgressivize || presTargetDoc.zoomProgressivize || presTargetDoc.scrollProgressivize) internalFrames = true; + // Case 1: There are still other frames and should go through all frames before going to next slide + if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) { presTargetDoc._viewTransition = "all 1s"; setTimeout(() => presTargetDoc._viewTransition = undefined, 1010); presTargetDoc.currentFrame = curFrame + 1; - } - else if (this.childDocs[this.itemIndex + 1] !== undefined) { - let nextSelected = this.itemIndex + 1; + if (presTargetDoc.scrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(presTargetDoc, currentFrame); + if (presTargetDoc.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + if (presTargetDoc.zoomProgressivize) this.zoomProgressivizeNext(presTargetDoc); + // Case 2: Audio or video therefore wait to play the audio or video before moving on + } else if ((presTargetDoc.type === DocumentType.AUDIO) && !this._moveOnFromAudio) { + AudioBox.Instance.playFrom(0); + this._moveOnFromAudio = true; + // Case 3: No more frames in current doc and next slide is defined, therefore move to next slide + } else if (this.childDocs[this.itemIndex + 1] !== undefined) { + const nextSelected = this.itemIndex + 1; this.gotoDocument(nextSelected, this.itemIndex); - - for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) { - if (!this.childDocs[nextSelected].groupButton) { - break; - } else { - this.gotoDocument(nextSelected, this.itemIndex); - } - } + const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); + if (activeNext && targetNext.type === DocumentType.AUDIO && activeNext.playAuto) { + } else this._moveOnFromAudio = false; } } + /** + * Called when the user moves back + * Design choice: If there are frames within the presentation, moving back will not + * got back through the frames but instead directly to the next point in the presentation. + */ @undoBatch @action back = () => { this.updateCurrentPresentation(); const docAtCurrent = this.childDocs[this.itemIndex]; if (docAtCurrent) { - //check if any of the group members had used zooming in including the current document - //If so making sure to zoom out, which goes back to state before zooming action let prevSelected = this.itemIndex; - let didZoom = docAtCurrent.zoomButton; - for (; !didZoom && prevSelected > 0 && this.childDocs[prevSelected].groupButton; prevSelected--) { - didZoom = this.childDocs[prevSelected].zoomButton; - } prevSelected = Math.max(0, prevSelected - 1); - this.gotoDocument(prevSelected, this.itemIndex); } } - /** - * This is the method that checks for the actions that need to be performed - * after the document has been presented, which involves 3 button options: - * Hide Until Presented, Hide After Presented, Fade After Presented - */ - showAfterPresented = (index: number) => { - this.updateCurrentPresentation(); - this.childDocs.forEach((doc, ind) => { - const presTargetDoc = doc.presentationTargetDoc as Doc; - //the order of cases is aligned based on priority - if (doc.presHideTillShownButton && ind <= index) { - presTargetDoc.opacity = 1; - } - if (doc.presHideAfterButton && ind < index) { - presTargetDoc.opacity = 0; - } - if (doc.presFadeButton && ind < index) { - presTargetDoc.opacity = 0.5; - } - }); - } - - /** - * This is the method that checks for the actions that need to be performed - * before the document has been presented, which involves 3 button options: - * Hide Until Presented, Hide After Presented, Fade After Presented - */ - hideIfNotPresented = (index: number) => { + //The function that is called when a document is clicked or reached through next or back. + //it'll also execute the necessary actions if presentation is playing. + public gotoDocument = action((index: number, fromDoc: number) => { this.updateCurrentPresentation(); - this.childDocs.forEach((key, ind) => { - //the order of cases is aligned based on priority - const presTargetDoc = key.presentationTargetDoc as Doc; - if (key.hideAfterButton && ind >= index) { - presTargetDoc.opacity = 1; - } - if (key.fadeButton && ind >= index) { - presTargetDoc.opacity = 1; - } - if (key.hideTillShownButton && ind > index) { - presTargetDoc.opacity = 0; + Doc.UnBrushAllDocs(); + if (index >= 0 && index < this.childDocs.length) { + this.rootDoc._itemIndex = index; + const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); + if (presTargetDoc?.lastFrame !== undefined) { + presTargetDoc.currentFrame = 0; } - }); - } + this.navigateToElement(this.childDocs[index]); //Handles movement to element + this._selectedArray = [this.childDocs[index]]; //Update selected array + this.onHideDocument(); //Handles hide after/before + } + }); /** * This method makes sure that cursor navigates to the element that - * has the option open and last in the group. If not in the group, and it has - * te option open, navigates to that element. + * has the option open and last in the group. + * Design choice: If the next document is not in presCollection or + * presCollection itself then if there is a presCollection it will add + * a new tab. If presCollection is undefined it will open the document + * on the right. */ - navigateToElement = async (curDoc: Doc, fromDocIndex: number) => { - this.updateCurrentPresentation(); - let docToJump = curDoc; - let willZoom = false; - - const presDocs = DocListCast(this.dataDoc[this.props.fieldKey]); - let nextSelected = presDocs.indexOf(curDoc); - const currentDocGroups: Doc[] = []; - for (; nextSelected < presDocs.length - 1; nextSelected++) { - if (!presDocs[nextSelected + 1].groupButton) { - break; - } - currentDocGroups.push(presDocs[nextSelected]); - } + navigateToElement = async (curDoc: Doc) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const srcContext = await DocCastAsync(targetDoc.context); + const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); + const collectionDocView = presCollection ? await DocumentManager.Instance.getDocumentView(presCollection) : undefined; + this.turnOffEdit(); - currentDocGroups.forEach((doc: Doc, index: number) => { - if (doc.presNavButton) { - docToJump = doc; - willZoom = false; - } - if (doc.presZoomButton) { - docToJump = doc; - willZoom = true; + if (this.itemIndex >= 0) { + if (targetDoc) { + if (srcContext) this.layoutDoc.presCollection = srcContext; + } else if (targetDoc) this.layoutDoc.presCollection = targetDoc; + } + if (collectionDocView) { + if (srcContext && srcContext !== presCollection) { + // Case 1: new srcContext inside of current collection so add a new tab to the current pres collection + collectionDocView.props.addDocTab(srcContext, "inPlace"); } - }); + } + this.updateCurrentPresentation(); + const docToJump = curDoc; + const willZoom = false; //docToJump stayed same meaning, it was not in the group or was the last element in the group - const aliasOf = await DocCastAsync(docToJump.aliasOf); - const srcContext = aliasOf && await DocCastAsync(aliasOf.context); - if (docToJump === curDoc) { + if (targetDoc.zoomProgressivize && this.rootDoc.presStatus !== 'edit') { + this.zoomProgressivizeNext(targetDoc); + } else if (docToJump === curDoc) { //checking if curDoc has navigation open - const target = (await DocCastAsync(curDoc.presentationTargetDoc)) || curDoc; - if (curDoc.presNavButton && target) { - DocumentManager.Instance.jumpToDocument(target, false, undefined, srcContext); - } else if (curDoc.presZoomButton && target) { + if (curDoc.presNavButton && targetDoc) { + await DocumentManager.Instance.jumpToDocument(targetDoc, false, undefined, srcContext); + } else if (curDoc.presZoomButton && targetDoc) { //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(target, true, undefined, srcContext); + await DocumentManager.Instance.jumpToDocument(targetDoc, true, undefined, srcContext); } } else { //awaiting jump so that new scale can be found, since jumping is async - const presTargetDoc = await DocCastAsync(docToJump.presentationTargetDoc); - presTargetDoc && await DocumentManager.Instance.jumpToDocument(presTargetDoc, willZoom, undefined, srcContext); + targetDoc && await DocumentManager.Instance.jumpToDocument(targetDoc, willZoom, undefined, srcContext); + } + // After navigating to the document, if it is added as a presPinView then it will + // adjust the pan and scale to that of the pinView when it was added. + // TODO: Add option to remove presPinView + if (activeItem.presPinView) { + targetDoc._panX = activeItem.presPinViewX; + targetDoc._panY = activeItem.presPinViewY; + targetDoc._viewScale = activeItem.presPinViewScale; + } + // If openDocument is selected then it should open the document for the user + if (collectionDocView && activeItem.openDocument) { + collectionDocView.props.addDocTab(activeItem, "inPlace"); + } + // If website and has presWebsite data associated then on click it should + // go back to that specific website + // TODO: Add progressivize for navigating web (storing websites for given frames) + if (targetDoc.presWebsiteData) { + targetDoc.data = targetDoc.presWebsiteData; } } - //The function that is called when a document is clicked or reached through next or back. - //it'll also execute the necessary actions if presentation is playing. - public gotoDocument = action((index: number, fromDoc: number) => { - this.updateCurrentPresentation(); - Doc.UnBrushAllDocs(); - if (index >= 0 && index < this.childDocs.length) { - this.rootDoc._itemIndex = index; - const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); - if (presTargetDoc.lastFrame !== undefined) { - presTargetDoc.currentFrame = 0; + /** + * Uses the viewfinder to progressivize through the different views of a single collection. + * @param presTargetDoc: document for which internal zoom is used + */ + zoomProgressivizeNext = (presTargetDoc: Doc) => { + const srcContext = Cast(presTargetDoc.context, Doc, null); + const docView = DocumentManager.Instance.getDocumentView(presTargetDoc); + const vfLeft: number = this.checkList(presTargetDoc, presTargetDoc["viewfinder-left-indexed"]); + const vfWidth: number = this.checkList(presTargetDoc, presTargetDoc["viewfinder-width-indexed"]); + const vfTop: number = this.checkList(presTargetDoc, presTargetDoc["viewfinder-top-indexed"]); + const vfHeight: number = this.checkList(presTargetDoc, presTargetDoc["viewfinder-height-indexed"]); + // Case 1: document that is not a Golden Layout tab + if (srcContext) { + const srcDocView = DocumentManager.Instance.getDocumentView(srcContext); + if (srcDocView) { + const layoutdoc = Doc.Layout(presTargetDoc); + const panelWidth: number = srcDocView.props.PanelWidth(); + const panelHeight: number = srcDocView.props.PanelHeight(); + const newPanX = NumCast(presTargetDoc.x) + NumCast(layoutdoc._width) / 2; + const newPanY = NumCast(presTargetDoc.y) + NumCast(layoutdoc._height) / 2; + const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight); + srcContext._panX = newPanX + (vfLeft + (vfWidth / 2)); + srcContext._panY = newPanY + (vfTop + (vfHeight / 2)); + srcContext._viewScale = newScale; } + } + // Case 2: document is the containing collection + if (docView && !srcContext) { + const panelWidth: number = docView.props.PanelWidth(); + const panelHeight: number = docView.props.PanelHeight(); + const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight); + presTargetDoc._panX = vfLeft + (vfWidth / 2); + presTargetDoc._panY = vfTop + (vfWidth / 2); + presTargetDoc._viewScale = newScale; + } + const resize = document.getElementById('resizable'); + if (resize) { + resize.style.width = vfWidth + 'px'; + resize.style.height = vfHeight + 'px'; + resize.style.top = vfTop + 'px'; + resize.style.left = vfLeft + 'px'; + } + } - if (!this.layoutDoc.presStatus) { - this.layoutDoc.presStatus = true; - this.startPresentation(index); + + /** + * For 'Hide Before' and 'Hide After' buttons making sure that + * they are hidden each time the presentation is updated. + */ + @action + onHideDocument = () => { + this.childDocs.forEach((doc, index) => { + const curDoc = Cast(doc, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + if (tagDoc) tagDoc.opacity = 1; + if (curDoc.presHideTillShownButton) { + if (index > this.itemIndex) { + tagDoc.opacity = 0; + } else if (!curDoc.presHideAfterButton) { + tagDoc.opacity = 1; + } + } + if (curDoc.presHideAfterButton) { + if (index < this.itemIndex) { + tagDoc.opacity = 0; + } else if (!curDoc.presHideTillShownButton) { + tagDoc.opacity = 1; + } } + }); + } - this.navigateToElement(this.childDocs[index], fromDoc); - this.hideIfNotPresented(index); - this.showAfterPresented(index); - } - }); - //The function that starts or resets presentaton functionally, depending on status flag. - startOrResetPres = () => { + //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc + @undoBatch + @action + startAutoPres = (startSlide: number) => { this.updateCurrentPresentation(); - if (this.layoutDoc.presStatus) { - this.resetPresentation(); + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + if (this.layoutDoc.presStatus === "auto") { + if (this._presTimer) clearInterval(this._presTimer); + this.layoutDoc.presStatus = "manual"; } else { - this.layoutDoc.presStatus = true; - this.startPresentation(0); - this.gotoDocument(0, this.itemIndex); + this.layoutDoc.presStatus = "auto"; + this.startPresentation(startSlide); + this.gotoDocument(startSlide, this.itemIndex); + this._presTimer = setInterval(() => { + if (this.itemIndex + 1 < this.childDocs.length) this.next(); + else { + clearInterval(this._presTimer); + this.layoutDoc.presStatus = "manual"; + } + }, targetDoc.presDuration ? NumCast(targetDoc.presDuration) + NumCast(targetDoc.presTransition) : 2000); } } //The function that resets the presentation by removing every action done by it. It also //stops the presentaton. + // TODO: Ensure resetPresentation is called when the presentation is closed resetPresentation = () => { this.updateCurrentPresentation(); - this.childDocs.forEach(doc => (doc.presentationTargetDoc as Doc).opacity = 1); this.rootDoc._itemIndex = 0; - this.layoutDoc.presStatus = false; } - //The function that starts the presentation, also checking if actions should be applied - //directly at start. + @action togglePath = () => this.pathBoolean = !this.pathBoolean; + @action toggleExpand = () => this.expandBoolean = !this.expandBoolean; + + /** + * The function that starts the presentation at the given index, also checking if actions should be applied + * directly at start. + * @param startIndex: index that the presentation will start at + */ startPresentation = (startIndex: number) => { this.updateCurrentPresentation(); this.childDocs.map(doc => { @@ -249,82 +364,1350 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (doc.presHideAfterButton && this.childDocs.indexOf(doc) < startIndex) { presTargetDoc.opacity = 0; } - if (doc.presFadeButton && this.childDocs.indexOf(doc) < startIndex) { - presTargetDoc.opacity = 0.5; - } }); } - updateMinimize = action((e: React.ChangeEvent, mode: CollectionViewType) => { - if (BoolCast(this.layoutDoc.inOverlay) !== (mode === CollectionViewType.Invalid)) { - if (this.layoutDoc.inOverlay) { - Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); + /** + * The method called to open the presentation as a minimized view + * TODO: Look at old updateMinimize and compare... + */ + updateMinimize = () => { + const srcContext = Cast(this.rootDoc.presCollection, Doc, null); + this.turnOffEdit(); + if (srcContext) { + if (srcContext.miniPres) { + srcContext.miniPres = false; CollectionDockingView.AddRightSplit(this.rootDoc); - this.layoutDoc.inOverlay = false; } else { - const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - this.rootDoc.x = pt[0];// 500;//e.clientX + 25; - this.rootDoc.y = pt[1];////e.clientY - 25; + srcContext.miniPres = true; this.props.addDocTab?.(this.rootDoc, "close"); - Doc.AddDocToList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); } } - }); + } + /** + * Called when the user changes the view type + * Either 'List' (stacking) or 'Slides' (carousel) + */ @undoBatch viewChanged = action((e: React.ChangeEvent) => { //@ts-ignore const viewType = e.target.selectedOptions[0].value as CollectionViewType; - viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here - this.updateMinimize(e, this.rootDoc._viewType = viewType); + // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here + viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined); + this.rootDoc._viewType = viewType; + if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 5; + }); + + /** + * When the movement dropdown is changes + */ + @undoBatch + movementChanged = action((movement: string) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + switch (movement) { + case 'zoom': //Pan and zoom + activeItem.presZoomButton = !activeItem.presZoomButton; + if (activeItem.presZoomButton) activeItem.presMovement = 'Zoom'; + else activeItem.presMovement = 'None'; + activeItem.presNavButton = false; + break; + case 'pan': //Pan + activeItem.presZoomButton = false; + activeItem.presNavButton = !activeItem.presNavButton; + if (activeItem.presNavButton) activeItem.presMovement = 'Pan'; + else activeItem.presMovement = 'None'; + break; + case 'jump': //Jump Cut + targetDoc.presTransition = 0; + activeItem.presSwitchButton = !activeItem.presSwitchButton; + if (activeItem.presSwitchButton) activeItem.presMovement = 'Jump cut'; + else activeItem.presMovement = 'None'; + break; + case 'none': default: + activeItem.presMovement = 'None'; + activeItem.presZoomButton = false; + activeItem.presNavButton = false; + activeItem.presSwitchButton = false; + break; + } }); whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); + // For dragging documents into the presentation trail addDocumentFilter = (doc: Doc | Doc[]) => { const docs = doc instanceof Doc ? [doc] : doc; - docs.forEach(doc => { - doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); - !this.childDocs.includes(doc) && (doc.presZoomButton = true); + docs.forEach((doc, i) => { + if (this.childDocs.includes(doc)) { + if (docs.length === i + 1) return false; + } else { + doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); + !this.childDocs.includes(doc) && (doc.presZoomButton = true); + } }); return true; } childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement; removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); - selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex)); getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight - panelHeight = () => this.props.PanelHeight() - 20; + panelHeight = () => this.props.PanelHeight() - 40; active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground) && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) - render() { - // const presOrderedDocs = DocListCast(this.rootDoc.presOrderedDocs); - // if (presOrderedDocs.length != this.childDocs.length || presOrderedDocs.some((pd, i) => pd !== this.childDocs[i])) { - // this.rootDoc.presOrderedDocs = new List<Doc>(this.childDocs.slice()); - // } - this.childDocs.slice(); // needed to insure that the childDocs are loaded for looking up fields + /** + * For sorting the array so that the order is maintained when it is dropped. + */ + @action + sortArray = (): Doc[] => { + const sort: Doc[] = this._selectedArray; + this.childDocs.forEach((doc, i) => { + if (this._selectedArray.includes(doc)) { + sort.push(doc); + } + }); + return sort; + } + + /** + * Method to get the list of selected items in the order in which they have been selected + */ + @computed get listOfSelected() { + const list = this._selectedArray.map((doc: Doc, index: any) => { + const activeItem = Cast(doc, Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc!, Doc, null); + return ( + <div className="selectedList-items">{index + 1}. {targetDoc.title}</div> + ); + }); + return list; + } + + //Regular click + @action + selectElement = (doc: Doc) => { + this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex)); + } + + //Command click + @action + multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { + if (!this._selectedArray.includes(doc)) { + this._selectedArray.push(this.childDocs[this.childDocs.indexOf(doc)]); + this._eleArray.push(ref); + this._dragArray.push(drag); + } + } + + //Shift click + @action + shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { + this._selectedArray = []; + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + if (activeItem) { + for (let i = Math.min(this.itemIndex, this.childDocs.indexOf(doc)); i <= Math.max(this.itemIndex, this.childDocs.indexOf(doc)); i++) { + this._selectedArray.push(this.childDocs[i]); + this._eleArray.push(ref); + this._dragArray.push(drag); + } + } + } + + // Key for when the presentaiton is active (according to Selection Manager) + @action + keyEvents = (e: KeyboardEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (e.keyCode === 27) { // Escape key + if (this.layoutDoc.presStatus === "edit") this._selectedArray = []; + else this.layoutDoc.presStatus = "edit"; + } if ((e.metaKey || e.altKey) && e.keyCode === 65) { // Ctrl-A to select all + if (this.layoutDoc.presStatus === "edit") this._selectedArray = this.childDocs; + } if (e.keyCode === 37 || e.keyCode === 38) { // left(37) / a(65) / up(38) to go back + this.back(); + } if (e.keyCode === 39 || e.keyCode === 40) { // right (39) / d(68) / down(40) to go to next + this.next(); + } if (e.keyCode === 32) { // spacebar to 'present' or autoplay + if (this.layoutDoc.presStatus !== "edit") this.startAutoPres(0); + else this.layoutDoc.presStatus = "manual"; + } + if (e.keyCode === 8) { // delete selected items + if (this.layoutDoc.presStatus === "edit") { + this._selectedArray.forEach((doc, i) => { + this.removeDocument(doc); + }); + } + } + } + + /** + * + */ + @undoBatch + @action + viewPaths = async () => { + const srcContext = Cast(this.rootDoc.presCollection, Doc, null); + if (this.pathBoolean) { + if (srcContext) { + this.togglePath(); + srcContext._fitToBox = false; + srcContext._viewType = "freeform"; + srcContext.presPathView = false; + } + } else { + if (srcContext) { + this.togglePath(); + srcContext._fitToBox = true; + srcContext._viewType = "freeform"; + srcContext.presPathView = true; + } + } + const viewType = srcContext?._viewType; + const fit = srcContext?._fitToBox; + } + + // Adds the index in the pres path graphically + @computed get order() { + const order: JSX.Element[] = []; + this.childDocs.forEach((doc, index) => { + const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(targetDoc.context, Doc, null); + // Case A: Document is contained within the colleciton + if (this.rootDoc.presCollection === srcContext) { + order.push( + <div className="pathOrder" style={{ top: NumCast(targetDoc.y), left: NumCast(targetDoc.x) }}> + <div className="pathOrder-frame">{index + 1}</div> + </div>); + // Case B: Document is not inside of the collection + } else { + order.push( + <div className="pathOrder" style={{ top: 0, left: 0 }}> + <div className="pathOrder-frame">{index + 1}</div> + </div>); + } + }); + return order; + } + + /** + * Method called for viewing paths which adds a single line with + * points at the center of each document added. + * Design choice: When this is called it sets _fitToBox as true so the + * user can have an overview of all of the documents in the collection. + * (Design needed for when documents in presentation trail are in another + * collection) + */ + @computed get paths() { + let pathPoints = ""; + this.childDocs.forEach((doc, index) => { + const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(targetDoc.context, Doc, null); + if (targetDoc && this.rootDoc.presCollection === srcContext) { + const n1x = NumCast(targetDoc.x) + (NumCast(targetDoc._width) / 2); + const n1y = NumCast(targetDoc.y) + (NumCast(targetDoc._height) / 2); + if (index = 0) pathPoints = n1x + "," + n1y; + else pathPoints = pathPoints + " " + n1x + "," + n1y; + } else { + if (index = 0) pathPoints = 0 + "," + 0; + else pathPoints = pathPoints + " " + 0 + "," + 0; + } + }); + return (<polyline + points={pathPoints} + style={{ + opacity: 1, + stroke: "#69a6db", + strokeWidth: 5, + strokeDasharray: '10 5', + }} + fill="none" + markerStart="url(#markerSquare)" + markerMid="url(#markerSquare)" + markerEnd="url(#markerArrow)" + />); + } + + /** + * The function that is called on click to turn fading document after presented option on/off. + * It also makes sure that the option swithches from hide-after to this one, since both + * can't coexist. + */ + @action + onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + activeItem.presFadeButton = !activeItem.presFadeButton; + if (!activeItem.presFadeButton) { + if (targetDoc) { + targetDoc.opacity = 1; + } + } else { + activeItem.presHideAfterButton = false; + if (this.rootDoc.presStatus !== "edit" && targetDoc) { + targetDoc.opacity = 0.5; + } + } + } + + // Converts seconds to ms and updates presTransition + setTransitionTime = (number: String) => { + const timeInMS = Number(number) * 1000; + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + if (targetDoc) targetDoc.presTransition = timeInMS; + } + + // Converts seconds to ms and updates presDuration + setDurationTime = (number: String) => { + const timeInMS = Number(number) * 1000; + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + if (targetDoc) targetDoc.presDuration = timeInMS; + } + + + @computed get transitionDropdown() { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + if (activeItem && targetDoc) { + const transitionSpeed = targetDoc.presTransition ? String(Number(targetDoc.presTransition) / 1000) : 0.5; + let duration = targetDoc.presDuration ? String(Number(targetDoc.presDuration) / 1000) : 2; + if (targetDoc.type === DocumentType.AUDIO) duration = NumCast(targetDoc.duration); + const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None'; + activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom'; + return ( + <div className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + Movement + <div className="presBox-dropdown" onPointerDown={e => e.stopPropagation()}> + {activeItem.presMovement} + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2 }} icon={"angle-down"} /> + <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onClick={e => e.stopPropagation()}> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'None' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('none')}>None</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Zoom' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('zoom')}>Pan and Zoom</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Pan' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('pan')}>Pan</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Jump cut' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('jump')}>Jump cut</div> + </div> + </div> + <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "inline-flex" : "none" }}> + <div className="presBox-subheading" >Transition Speed</div> + <div className="ribbon-property"> {transitionSpeed} s </div> + </div> + <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} className={`toolbar-slider ${activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "" : "none"}`} id="toolbar-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setTransitionTime(e.target.value); }} /> + <div className={`slider-headers ${activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "" : "none"}`}> + <div className="slider-text">Fast</div> + <div className="slider-text">Medium</div> + <div className="slider-text">Slow</div> + </div> + </div> + <div className="ribbon-box"> + Visibility {"&"} Duration + <div className="ribbon-doubleButton"> + <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-button ${activeItem.presHideTillShownButton ? "active" : ""}`} onClick={() => activeItem.presHideTillShownButton = !activeItem.presHideTillShownButton}>Hide before</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-button ${activeItem.presHideAfterButton ? "active" : ""}`} onClick={() => activeItem.presHideAfterButton = !activeItem.presHideAfterButton}>Hide after</div></Tooltip> + </div> + <div className="ribbon-doubleButton" > + <div className="presBox-subheading">Slide Duration</div> + <div className="ribbon-property"> {duration} s </div> + </div> + <input type="range" step="0.1" min="0.1" max="10" value={duration} style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "block" }} className={"toolbar-slider"} id="duration-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setDurationTime(e.target.value); }} /> + <div className={"slider-headers"} style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "grid" }}> + <div className="slider-text">Short</div> + <div className="slider-text">Medium</div> + <div className="slider-text">Long</div> + </div> + </div> + <div className="ribbon-box"> + Effects + <div className="presBox-dropdown" + onPointerDown={e => e.stopPropagation()} + > + {effect} + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2 }} icon={"angle-down"} /> + <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onClick={e => e.stopPropagation()}> + <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'None'}>None</div> + <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Fade'}>Fade In</div> + <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Flip'}>Flip</div> + <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Rotate'}>Rotate</div> + <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Bounce'}>Bounce</div> + <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Roll'}>Roll</div> + </div> + </div> + <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? "none" : "inline-flex" }}> + <div className="presBox-subheading" >Effect direction</div> + <div className="ribbon-property"> + {this.effectDirection} + </div> + </div> + <div className="effectDirection" style={{ display: effect === 'None' ? "none" : "grid", width: 40 }}> + <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "left" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'left'}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "right" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'right'}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === "top" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'top'}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === "bottom" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'bottom'}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection ? "solid 2px black" : "solid 2px #5a9edd", borderRadius: "100%" }} onClick={() => targetDoc.presEffectDirection = false}></div></Tooltip> + </div> + </div> + <div className="ribbon-final-box"> + <div className={this._selectedArray.length === 0 ? "ribbon-final-button" : "ribbon-final-button-hidden"} onClick={() => this.applyTo(this._selectedArray)}> + Apply to selected + </div> + <div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}> + Apply to all + </div> + </div> + </div> + ); + } + } + + @computed get effectDirection(): string { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + let effect = ''; + switch (targetDoc.presEffectDirection) { + case 'left': effect = "Enter from left"; break; + case 'right': effect = "Enter from right"; break; + case 'top': effect = "Enter from top"; break; + case 'bottom': effect = "Enter from bottom"; break; + default: effect = "Enter from center"; break; + } + return effect; + } + + applyTo = (array: Doc[]) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + array.forEach((doc, index) => { + const curDoc = Cast(doc, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + if (tagDoc && targetDoc) { + tagDoc.presTransition = targetDoc.presTransition; + tagDoc.presDuration = targetDoc.presDuration; + tagDoc.presEffect = targetDoc.presEffect; + } + }); + } + + private inputRef = React.createRef<HTMLInputElement>(); + + @computed get optionsDropdown() { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + if (activeItem && targetDoc) { + return ( + <div> + <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.AUDIO ? "inline-flex" : "none" }}> + <div className="ribbon-button" style={{ backgroundColor: activeItem.playAuto ? "#aedef8" : "" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play automatically</div> + <div className="ribbon-button" style={{ display: "flex", backgroundColor: activeItem.playAuto ? "" : "#aedef8" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play on next</div> + </div> + <div className="ribbon-doubleButton" style={{ display: "flex" }}> + <div className="ribbon-button" style={{ backgroundColor: activeItem.openDocument ? "#aedef8" : "" }} onClick={() => activeItem.openDocument = !activeItem.openDocument}>Open document</div> + </div> + <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.COL ? "inline-flex" : "none" }}> + <div className="ribbon-button" style={{ backgroundColor: activeItem.presPinView ? "#aedef8" : "" }} + onClick={() => { + activeItem.presPinView = !activeItem.presPinView; + if (activeItem.presPinView) { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeItem.presPinViewX = x; + activeItem.presPinViewY = y; + activeItem.presPinViewScale = scale; + } + }}>Presentation pin view</div> + </div> + <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.WEB ? "inline-flex" : "none" }}> + <div className="ribbon-button" onClick={this.progressivizeText}>Store original website</div> + </div> + </div> + </div> + </div > + ); + } + } + + @computed get newDocumentToolbarDropdown() { + return ( + <div> + <div className={'presBox-toolbar-dropdown'} style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="layout-container" style={{ height: 'max-content' }}> + <div className="layout" style={{ border: this.layout === 'blank' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'blank'; this.createNewSlide(this.layout); })} /> + <div className="layout" style={{ border: this.layout === 'title' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'title'; this.createNewSlide(this.layout); })}> + <div className="title">Title</div> + <div className="subtitle">Subtitle</div> + </div> + <div className="layout" style={{ border: this.layout === 'header' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'header'; this.createNewSlide(this.layout); })}> + <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> + </div> + <div className="layout" style={{ border: this.layout === 'content' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'content'; this.createNewSlide(this.layout); })}> + <div className="title" style={{ alignSelf: 'center' }}>Title</div> + <div className="content">Text goes here</div> + </div> + {/* <div className="layout" style={{ border: this.layout === 'twoColumns' ? 'solid 2px #5b9ddd' : '' }} onClick={() => runInAction(() => { this.layout = 'twoColumns'; this.createNewSlide(this.layout); })}> + <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>Title</div> + <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>Column one text</div> + <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>Column two text</div> + </div> */} + </div> + </div> + </div > + ); + } + + @observable openLayouts: boolean = false; + @observable addFreeform: boolean = true; + @observable layout: string = ""; + @observable title: string = ""; + + @computed get newDocumentDropdown() { + return ( + <div> + <div className={"presBox-ribbon"} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + Slide Title: <br></br> + <input className="ribbon-textInput" placeholder="..." type="text" name="fname" ref={this.inputRef} onChange={(e) => { + e.stopPropagation(); + runInAction(() => this.title = e.target.value); + }}></input> + </div> + <div className="ribbon-box"> + Choose type: + <div className="ribbon-doubleButton"> + <div title="Text" className={'ribbon-button'} style={{ background: this.addFreeform ? "" : "#aedef8" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> + <div title="Freeform" className={'ribbon-button'} style={{ background: this.addFreeform ? "#aedef8" : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> + </div> + </div> + <div className="ribbon-box" style={{ display: this.addFreeform ? "grid" : "none" }}> + Preset layouts: + <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> + <div className="layout" style={{ border: this.layout === 'blank' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'blank')} /> + <div className="layout" style={{ border: this.layout === 'title' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'title')}> + <div className="title">Title</div> + <div className="subtitle">Subtitle</div> + </div> + <div className="layout" style={{ border: this.layout === 'header' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'header')}> + <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> + </div> + <div className="layout" style={{ border: this.layout === 'content' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'content')}> + <div className="title" style={{ alignSelf: 'center' }}>Title</div> + <div className="content">Text goes here</div> + </div> + <div className="layout" style={{ border: this.layout === 'twoColumns' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'twoColumns')}> + <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>Title</div> + <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>Column one text</div> + <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>Column two text</div> + </div> + </div> + <div className="open-layout" onClick={action(() => this.openLayouts = !this.openLayouts)}> + <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"caret-down"} size={"lg"} /> + </div> + </div> + <div className="ribbon-final-box"> + <div className={this.title !== "" && (this.addFreeform && this.layout !== "" || !this.addFreeform) ? "ribbon-final-button-hidden" : "ribbon-final-button"} onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}> + Create New Slide + </div> + </div> + </div> + </div > + ); + } + + createNewSlide = (layout?: string, title?: string, freeform?: boolean) => { + let doc = undefined; + if (layout) doc = this.createTemplate(layout); + if (freeform && layout) doc = this.createTemplate(layout, title); + if (!freeform && !layout) doc = Docs.Create.TextDocument("", { _nativeWidth: 400, _width: 225, title: title }); + const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); + const data = Cast(presCollection?.data, listSpec(Doc)); + const presData = Cast(this.rootDoc.data, listSpec(Doc)); + if (data && doc && presData) { + data.push(doc); + DockedFrameRenderer.PinDoc(doc, false); + this.gotoDocument(this.childDocs.length, this.itemIndex); + } else { + this.props.addDocTab(doc as Doc, "onRight"); + } + } + + createTemplate = (layout: string, input?: string) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + let x = 0; + let y = 0; + if (activeItem && targetDoc) { + x = NumCast(targetDoc.x); + y = NumCast(targetDoc.y) + NumCast(targetDoc._height) + 20; + } + let doc = undefined; + const title = Docs.Create.TextDocument("Click to change title", { title: "Slide title", _width: 380, _height: 60, x: 10, y: 58, _fontSize: "24pt", }); + const subtitle = Docs.Create.TextDocument("Click to change subtitle", { title: "Slide subtitle", _width: 380, _height: 50, x: 10, y: 118, _fontSize: "16pt" }); + const header = Docs.Create.TextDocument("Click to change header", { title: "Slide header", _width: 380, _height: 65, x: 10, y: 80, _fontSize: "20pt" }); + const contentTitle = Docs.Create.TextDocument("Click to change title", { title: "Slide title", _width: 380, _height: 60, x: 10, y: 10, _fontSize: "24pt" }); + const content = Docs.Create.TextDocument("Click to change text", { title: "Slide text", _width: 380, _height: 145, x: 10, y: 70, _fontSize: "14pt" }); + const content1 = Docs.Create.TextDocument("Click to change text", { title: "Column 1", _width: 185, _height: 140, x: 10, y: 80, _fontSize: "14pt" }); + const content2 = Docs.Create.TextDocument("Click to change text", { title: "Column 2", _width: 185, _height: 140, x: 205, y: 80, _fontSize: "14pt" }); + switch (layout) { + case 'blank': + doc = Docs.Create.FreeformDocument([], { title: input ? input : "Blank slide", _width: 400, _height: 225, x: x, y: y }); + break; + case 'title': + doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : "Title slide", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + case 'header': + doc = Docs.Create.FreeformDocument([header], { title: input ? input : "Section header", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + case 'content': + doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : "Title and content", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + case 'twoColumns': + doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : "Title and two columns", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + default: + break; + } + return doc; + } + + // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view) + @computed get presentDropdown() { + return ( + <div className={`dropdown-play ${this.presentTools ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="dropdown-play-button" onClick={this.updateMinimize}> + Minimize + </div> + <div className="dropdown-play-button" onClick={(action(() => { this.layoutDoc.presStatus = "manual"; this.turnOffEdit(); }))}> + Sidebar view + </div> + </div> + ); + } + + // Case in which the document has keyframes to navigate to next key frame + @undoBatch + @action + nextKeyframe = (tagDoc: Doc): void => { + const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); + const currentFrame = Cast(tagDoc.currentFrame, "number", null); + if (currentFrame === undefined) { + tagDoc.currentFrame = 0; + CollectionFreeFormDocumentView.setupScroll(tagDoc, 0); + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame); + CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + tagDoc.currentFrame = Math.max(0, (currentFrame || 0) + 1); + tagDoc.lastFrame = Math.max(NumCast(tagDoc.currentFrame), NumCast(tagDoc.lastFrame)); + if (tagDoc.zoomProgressivize) { + const resize = document.getElementById('resizable'); + if (resize) { + resize.style.width = this.checkList(tagDoc, tagDoc["viewfinder-width-indexed"]) + 'px'; + resize.style.height = this.checkList(tagDoc, tagDoc["viewfinder-height-indexed"]) + 'px'; + resize.style.top = this.checkList(tagDoc, tagDoc["viewfinder-top-indexed"]) + 'px'; + resize.style.left = this.checkList(tagDoc, tagDoc["viewfinder-left-indexed"]) + 'px'; + } + } + } + + @undoBatch + @action + prevKeyframe = (tagDoc: Doc): void => { + const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); + const currentFrame = Cast(tagDoc.currentFrame, "number", null); + if (currentFrame === undefined) { + tagDoc.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice()); + tagDoc.currentFrame = Math.max(0, (currentFrame || 0) - 1); + if (tagDoc.zoomProgressivize) { + const resize = document.getElementById('resizable'); + if (resize) { + resize.style.width = this.checkList(tagDoc, tagDoc["viewfinder-width-indexed"]) + 'px'; + resize.style.height = this.checkList(tagDoc, tagDoc["viewfinder-height-indexed"]) + 'px'; + resize.style.top = this.checkList(tagDoc, tagDoc["viewfinder-top-indexed"]) + 'px'; + resize.style.left = this.checkList(tagDoc, tagDoc["viewfinder-left-indexed"]) + 'px'; + } + } + } + + /** + * Returns the collection type as a string for headers + */ + @computed get stringType(): string { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + let type: string = ''; + if (activeItem) { + switch (targetDoc.type) { + case DocumentType.PDF: type = "PDF"; break; + case DocumentType.RTF: type = "Text node"; break; + case DocumentType.COL: type = "Collection"; break; + case DocumentType.AUDIO: type = "Audio"; break; + case DocumentType.VID: type = "Video"; break; + case DocumentType.IMG: type = "Image"; break; + default: type = "Other node"; break; + } + } + return type; + } + + @computed get progressivizeDropdown() { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + + if (activeItem && targetDoc) { + return ( + <div> + <div className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + {this.stringType} selected + <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform' ? "inline-flex" : "none" }}> + <div className="ribbon-button" style={{ backgroundColor: activeItem.presProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeChild}>Child documents</div> + <div className="ribbon-button" style={{ display: activeItem.presProgressivize ? "flex" : "none", backgroundColor: targetDoc.editProgressivize ? "#aedef8" : "" }} onClick={this.editProgressivize}>Edit</div> + </div> + <div className="ribbon-doubleButton" style={{ display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}> + <div className="ribbon-button" style={{ backgroundColor: activeItem.zoomProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeZoom}>Internal zoom</div> + <div className="ribbon-button" style={{ display: activeItem.zoomProgressivize ? "flex" : "none", backgroundColor: targetDoc.editZoomProgressivize ? "#aedef8" : "" }} onClick={this.editZoomProgressivize}>Viewfinder</div> + {/* <div className="ribbon-button" style={{ display: activeItem.zoomProgressivize ? "flex" : "none", backgroundColor: targetDoc.editSnapZoomProgressivize ? "#aedef8" : "" }} onClick={this.editSnapZoomProgressivize}>Snapshot</div> */} + </div> + {/* <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform' ? "inline-flex" : "none" }}> + <div className="ribbon-button" onClick={this.progressivizeText}>Text progressivize</div> + <div className="ribbon-button" style={{ display: activeItem.textProgressivize ? "flex" : "none", backgroundColor: targetDoc.editTextProgressivize ? "#aedef8" : "" }} onClick={this.editTextProgressivize}>Edit</div> + </div> */} + <div className="ribbon-doubleButton" style={{ display: targetDoc._viewType === "stacking" || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> + <div className="ribbon-button" style={{ backgroundColor: activeItem.scrollProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeScroll}>Scroll progressivize</div> + <div className="ribbon-button" style={{ display: activeItem.scrollProgressivize ? "flex" : "none", backgroundColor: targetDoc.editScrollProgressivize ? "#aedef8" : "" }} onClick={this.editScrollProgressivize}>Edit</div> + </div> + </div> + <div className="ribbon-final-box" style={{ display: activeItem.zoomProgressivize || activeItem.scrollProgressivize || activeItem.presProgressivize || activeItem.textProgressivize ? "grid" : "none" }}> + Frames + <div className="ribbon-doubleButton"> + <div className="ribbon-frameSelector"> + <div key="back" title="back frame" className="backKeyframe" onClick={e => { e.stopPropagation(); this.prevKeyframe(targetDoc); }}> + <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> + </div> + <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: targetDoc.editing ? "#5a9edd" : "#5a9edd" }} + onClick={action(() => targetDoc.editing = !targetDoc.editing)} > + {NumCast(targetDoc.currentFrame)} + </div> + <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={e => { e.stopPropagation(); this.nextKeyframe(targetDoc); }}> + <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> + </div> + </div> + <Tooltip title={<><div className="dash-tooltip">{"Last frame"}</div></>}><div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div></Tooltip> + </div> + <div className="ribbon-button" style={{ height: 20, backgroundColor: "#5a9edd" }} onClick={() => console.log(" TODO: play frames")}>Play</div> + </div> + </div> + </div> + ); + } + } + + turnOffEdit = () => { + this.childDocs.forEach((doc) => { + doc.editSnapZoomProgressivize = false; + doc.editZoomProgressivize = false; + doc.editScrollProgressivize = false; + const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); + targetDoc.editSnapZoomProgressivize = false; + targetDoc.editZoomProgressivize = false; + targetDoc.editScrollProgressivize = false; + if (doc.type === DocumentType.WEB) { + doc.presWebsite = doc.data; + } + }); + } + + //Toggle whether the user edits or not + @action + editSnapZoomProgressivize = (e: React.MouseEvent) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + if (!targetDoc.editSnapZoomProgressivize) { + targetDoc.editSnapZoomProgressivize = true; + } else { + targetDoc.editSnapZoomProgressivize = false; + } + + } + + //Toggle whether the user edits or not + @action + editZoomProgressivize = (e: React.MouseEvent) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + if (!targetDoc.editZoomProgressivize) { + targetDoc.editZoomProgressivize = true; + } else { + targetDoc.editZoomProgressivize = false; + } + } + + //Toggle whether the user edits or not + @action + editScrollProgressivize = (e: React.MouseEvent) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + if (!targetDoc.editScrollProgressivize) { + targetDoc.editScrollProgressivize = true; + } else { + targetDoc.editScrollProgressivize = false; + } + } + + //Progressivize Zoom + @action + progressivizeScroll = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + activeItem.scrollProgressivize = !activeItem.scrollProgressivize; + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + targetDoc.scrollProgressivize = !targetDoc.zoomProgressivize; + CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc.currentFrame), true); + if (targetDoc.editScrollProgressivize) { + targetDoc.editScrollProgressivize = false; + targetDoc.currentFrame = 0; + targetDoc.lastFrame = 0; + } + } + + //Progressivize Zoom + @action + progressivizeZoom = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + activeItem.zoomProgressivize = !activeItem.zoomProgressivize; + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + targetDoc.zoomProgressivize = !targetDoc.zoomProgressivize; + CollectionFreeFormDocumentView.setupZoom(targetDoc, true); + if (targetDoc.editZoomProgressivize) { + targetDoc.editZoomProgressivize = false; + targetDoc.currentFrame = 0; + targetDoc.lastFrame = 0; + } + } + + //Progressivize Text nodes + @action + editTextProgressivize = (e: React.MouseEvent) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + targetDoc.currentFrame = targetDoc.lastFrame; + if (targetDoc?.editTextProgressivize) { + targetDoc.editTextProgressivize = false; + } else { + targetDoc.editTextProgressivize = true; + } + } + + @action + progressivizeText = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + activeItem.presProgressivize = !activeItem.presProgressivize; + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + targetDoc.presProgressivize = !targetDoc.presProgressivize; + if (activeItem.presProgressivize) { + targetDoc.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true); + targetDoc.lastFrame = docs.length - 1; + } + } + + //Progressivize Child Docs + @action + editProgressivize = (e: React.MouseEvent) => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + targetDoc.currentFrame = targetDoc.lastFrame; + if (targetDoc?.editProgressivize) { + targetDoc.editProgressivize = false; + } else { + targetDoc.editProgressivize = true; + } + } + + @action + progressivizeChild = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + if (!activeItem.presProgressivize) { + activeItem.presProgressivize = true; + targetDoc.presProgressivize = true; + targetDoc.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true); + targetDoc.lastFrame = docs.length - 1; + } else { + targetDoc.editProgressivize = false; + activeItem.presProgressivize = false; + targetDoc.presProgressivize = false; + // docs.forEach((doc, index) => { + // doc.appearFrame = 0; + // }); + targetDoc.currentFrame = 0; + targetDoc.lastFrame = 0; + } + } + + @action + checkMovementLists = (doc: Doc, xlist: any, ylist: any) => { + const x: List<number> = xlist; + const y: List<number> = ylist; + const tags: JSX.Element[] = []; + let pathPoints = ""; //List of all of the pathpoints that need to be added + for (let i = 0; i < x.length - 1; i++) { + if (y[i] || x[i]) { + if (i === 0) pathPoints = (x[i] - 11) + "," + (y[i] + 33); + else pathPoints = pathPoints + " " + (x[i] - 11) + "," + (y[i] + 33); + tags.push(<div className="progressivizeMove-frame" style={{ position: 'absolute', top: y[i], left: x[i] }}>{i}</div>); + } + } + tags.push(<svg style={{ overflow: 'visible', position: 'absolute' }}><polyline + points={pathPoints} + style={{ + position: 'absolute', + opacity: 1, + stroke: "#000000", + strokeWidth: 2, + strokeDasharray: '10 5', + }} + fill="none" + /></svg>); + return tags; + } + + @observable + toggleDisplayMovement = (doc: Doc) => { + if (doc.displayMovement) doc.displayMovement = false; + else doc.displayMovement = true; + } + + private _isDraggingTL = false; + private _isDraggingTR = false; + private _isDraggingBR = false; + private _isDraggingBL = false; + private _isDragging = false; + // private _drag = ""; + + // onPointerDown = (e: React.PointerEvent): void => { + // e.stopPropagation(); + // e.preventDefault(); + // if (e.button === 0) { + // this._drag = e.currentTarget.id; + // console.log(this._drag); + // } + // document.removeEventListener("pointermove", this.onPointerMove); + // document.addEventListener("pointermove", this.onPointerMove); + // document.removeEventListener("pointerup", this.onPointerUp); + // document.addEventListener("pointerup", this.onPointerUp); + // } + + + //Adds event listener so knows pointer is down and moving + onPointerMid = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isDragging = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + //Adds event listener so knows pointer is down and moving + onPointerBR = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isDraggingBR = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + //Adds event listener so knows pointer is down and moving + onPointerBL = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isDraggingBL = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + //Adds event listener so knows pointer is down and moving + onPointerTR = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isDraggingTR = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + //Adds event listener so knows pointer is down and moving + onPointerTL = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isDraggingTL = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + //Removes all event listeners + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isDraggingTL = false; + this._isDraggingTR = false; + this._isDraggingBL = false; + this._isDraggingBR = false; + this._isDragging = false; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + //Adjusts the value in NodeStore + onPointerMove = (e: PointerEvent): void => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const tagDocView = DocumentManager.Instance.getDocumentView(targetDoc); + e.stopPropagation(); + e.preventDefault(); + const doc = document.getElementById('resizable'); + if (doc && tagDocView) { + + const scale2 = tagDocView.childScaling(); + const scale3 = tagDocView.props.ScreenToLocalTransform().Scale; + const scale = NumCast(targetDoc._viewScale); + console.log("scale: " + NumCast(targetDoc._viewScale)); + let height = doc.offsetHeight; + let width = doc.offsetWidth; + let top = doc.offsetTop; + let left = doc.offsetLeft; + // const newHeightB = height += (e.movementY * NumCast(targetDoc._viewScale)); + // const newHeightT = height -= (e.movementY * NumCast(targetDoc._viewScale)); + // const newWidthR = width += (e.movementX * NumCast(targetDoc._viewScale)); + // const newWidthL = width -= (e.movementX * NumCast(targetDoc._viewScale)); + // const newLeft = left += (e.movementX * NumCast(targetDoc._viewScale)); + // const newTop = top += (e.movementY * NumCast(targetDoc._viewScale)); + // switch (this._drag) { + // case "": break; + // case "resizer-br": + // doc.style.height = newHeightB + 'px'; + // doc.style.width = newWidthR + 'px'; + // break; + // case "resizer-bl": + // doc.style.height = newHeightB + 'px'; + // doc.style.width = newWidthL + 'px'; + // doc.style.left = newLeft + 'px'; + // break; + // case "resizer-tr": + // doc.style.width = newWidthR + 'px'; + // doc.style.height = newHeightT + 'px'; + // doc.style.top = newTop + 'px'; + // case "resizer-tl": + // doc.style.width = newWidthL + 'px'; + // doc.style.height = newHeightT + 'px'; + // doc.style.top = newTop + 'px'; + // doc.style.left = newLeft + 'px'; + // case "resizable": + // doc.style.top = newTop + 'px'; + // doc.style.left = newLeft + 'px'; + // } + //Bottom right + if (this._isDraggingBR) { + const newHeight = height += (e.movementY * scale); + doc.style.height = newHeight + 'px'; + const newWidth = width += (e.movementX * scale); + doc.style.width = newWidth + 'px'; + // Bottom left + } else if (this._isDraggingBL) { + const newHeight = height += (e.movementY * scale); + doc.style.height = newHeight + 'px'; + const newWidth = width -= (e.movementX * scale); + doc.style.width = newWidth + 'px'; + const newLeft = left += (e.movementX * scale); + doc.style.left = newLeft + 'px'; + // Top right + } else if (this._isDraggingTR) { + const newWidth = width += (e.movementX * scale); + doc.style.width = newWidth + 'px'; + const newHeight = height -= (e.movementY * scale); + doc.style.height = newHeight + 'px'; + const newTop = top += (e.movementY * scale); + doc.style.top = newTop + 'px'; + // Top left + } else if (this._isDraggingTL) { + const newWidth = width -= (e.movementX * scale); + doc.style.width = newWidth + 'px'; + const newHeight = height -= (e.movementY * scale); + doc.style.height = newHeight + 'px'; + const newTop = top += (e.movementY * scale); + doc.style.top = newTop + 'px'; + const newLeft = left += (e.movementX * scale); + doc.style.left = newLeft + 'px'; + } else if (this._isDragging) { + const newTop = top += (e.movementY * scale); + doc.style.top = newTop + 'px'; + const newLeft = left += (e.movementX * scale); + doc.style.left = newLeft + 'px'; + } + this.updateList(targetDoc, targetDoc["viewfinder-width-indexed"], width); + this.updateList(targetDoc, targetDoc["viewfinder-height-indexed"], height); + this.updateList(targetDoc, targetDoc["viewfinder-top-indexed"], top); + this.updateList(targetDoc, targetDoc["viewfinder-left-indexed"], left); + } + } + + @action + checkList = (doc: Doc, list: any): number => { + const x: List<number> = list; + if (x && x.length >= NumCast(doc.currentFrame) + 1) { + return x[NumCast(doc.currentFrame)]; + } else { + x.length = NumCast(doc.currentFrame) + 1; + x[NumCast(doc.currentFrame)] = x[NumCast(doc.currentFrame) - 1]; + return x[NumCast(doc.currentFrame)]; + } + + } + + @action + updateList = (doc: Doc, list: any, val: number) => { + const x: List<number> = list; + if (x && x.length >= NumCast(doc.currentFrame) + 1) { + x[NumCast(doc.currentFrame)] = val; + list = x; + } else { + x.length = NumCast(doc.currentFrame) + 1; + x[NumCast(doc.currentFrame)] = val; + list = x; + } + } + + // scale: NumCast(targetDoc._viewScale), + @computed get zoomProgressivizeContainer() { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + if (targetDoc) { + const vfLeft: number = this.checkList(targetDoc, targetDoc["viewfinder-left-indexed"]); + const vfWidth: number = this.checkList(targetDoc, targetDoc["viewfinder-width-indexed"]); + const vfTop: number = this.checkList(targetDoc, targetDoc["viewfinder-top-indexed"]); + const vfHeight: number = this.checkList(targetDoc, targetDoc["viewfinder-height-indexed"]); + return ( + <> + {!targetDoc.editZoomProgressivize ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerMid} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> + <div className='resizers'> + <div id="resizer-tl" className='resizer top-left' onPointerDown={this.onPointerTL}></div> + <div id="resizer-tr" className='resizer top-right' onPointerDown={this.onPointerTR}></div> + <div id="resizer-bl" className='resizer bottom-left' onPointerDown={this.onPointerBL}></div> + <div id="resizer-br" className='resizer bottom-right' onPointerDown={this.onPointerBR}></div> + </div> + </div>} + </> + ); + } + } + + @computed get progressivizeChildDocs() { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + const tags: JSX.Element[] = []; + docs.forEach((doc, index) => { + if (doc["x-indexed"] && doc["y-indexed"]) { + tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? "block" : "none" }}>{this.checkMovementLists(doc, doc["x-indexed"], doc["y-indexed"])}</div>); + } + tags.push( + <div className="progressivizeButton" onPointerLeave={() => { if (NumCast(targetDoc.currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc.currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? "#aedff8" : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> + <div className="progressivizeButton-prev"><FontAwesomeIcon icon={"caret-left"} size={"lg"} onClick={e => { e.stopPropagation(); this.prevAppearFrame(doc, index); }} /></div> + <div className="progressivizeButton-frame">{doc.appearFrame}</div> + <div className="progressivizeButton-next"><FontAwesomeIcon icon={"caret-right"} size={"lg"} onClick={e => { e.stopPropagation(); this.nextAppearFrame(doc, index); }} /></div> + </div>); + }); + return tags; + } + + @undoBatch + @action + nextAppearFrame = (doc: Doc, i: number): void => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const appearFrame = Cast(doc.appearFrame, "number", null); + if (appearFrame === undefined) { + doc.appearFrame = 0; + } + doc.appearFrame = appearFrame + 1; + this.updateOpacityList(doc["opacity-indexed"], NumCast(doc.appearFrame)); + } + + @undoBatch + @action + prevAppearFrame = (doc: Doc, i: number): void => { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const appearFrame = Cast(doc.appearFrame, "number", null); + if (appearFrame === undefined) { + doc.appearFrame = 0; + } + doc.appearFrame = Math.max(0, appearFrame - 1); + this.updateOpacityList(doc["opacity-indexed"], NumCast(doc.appearFrame)); + } + + @action + updateOpacityList = (list: any, frame: number) => { + const x: List<number> = list; + if (x && x.length >= frame) { + for (let i = 0; i < x.length; i++) { + if (i < frame) { + x[i] = 0; + } else if (i >= frame) { + x[i] = 1; + } + } + list = x; + } else { + x.length = frame + 1; + for (let i = 0; i < x.length; i++) { + if (i < frame) { + x[i] = 0; + } else if (i >= frame) { + x[i] = 1; + } + } + list = x; + } + } + + @computed get moreInfoDropdown() { + return (<div></div>); + } + + @computed + get toolbarWidth(): number { + const width = this.props.PanelWidth(); + return width; + } + + @computed get toolbar() { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + return ( + <div id="toolbarContainer" className={'presBox-toolbar'} style={{ display: this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }}> + <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> + <FontAwesomeIcon icon={"plus"} /> + <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> + </div></Tooltip> + <div className="toolbar-divider" /> + <Tooltip title={<><div className="dash-tooltip">{"View paths"}</div></>}> + <div style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3 }} className={`toolbar-button ${this.pathBoolean ? "active" : ""}`} onClick={this.childDocs.length > 1 ? this.viewPaths : undefined}> + <FontAwesomeIcon icon={"exchange-alt"} /> + </div> + </Tooltip> + <Tooltip title={<><div className="dash-tooltip">{this.expandBoolean ? "Minimize all" : "Expand all"}</div></>}> + <div style={{ opacity: this.childDocs.length > 0 ? 1 : 0.3 }} className={`toolbar-button ${this.expandBoolean ? "active" : ""}`} onClick={() => { if (this.childDocs.length > 0) this.toggleExpand(); this.childDocs.forEach((doc, ind) => { if (this.expandBoolean) doc.presExpandInlineButton = true; else doc.presExpandInlineButton = false; }); }}> + <FontAwesomeIcon icon={"eye"} /> + </div> + </Tooltip> + <div className="toolbar-divider" /> + </div> + ); + } + + /** + * Top panel containes: + * viewPicker: The option to choose between List and Slides view for the presentaiton trail + * presentPanel: The button to start the presentation / open minimized view of the presentation + */ + @computed get topPanel() { const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; - return <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined }} > + return ( <div className="presBox-buttons" style={{ display: this.rootDoc._chromeStatus === "disabled" ? "none" : undefined }}> <select className="presBox-viewPicker" + style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> - <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Invalid}>Min</option> <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option> - <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Time}>Time</option> <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel}>Slides</option> </select> - <div className="presBox-button" title="Back" style={{ gridColumn: 2 }} onClick={this.back}> - <FontAwesomeIcon icon={"arrow-left"} /> - </div> - <div className="presBox-button" title={"Reset Presentation" + this.layoutDoc.presStatus ? "" : " From Start"} style={{ gridColumn: 3 }} onClick={this.startOrResetPres}> - <FontAwesomeIcon icon={this.layoutDoc.presStatus ? "stop" : "play"} /> - </div> - <div className="presBox-button" title="Next" style={{ gridColumn: 4 }} onClick={this.next}> - <FontAwesomeIcon icon={"arrow-right"} /> + <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length > 0 ? 1 : 0.3 }}> + <span className={`presBox-button ${this.layoutDoc.presStatus === "edit" ? "present" : ""}`}> + <div className="presBox-button-left" onClick={() => (this.childDocs.length > 0) && (this.layoutDoc.presStatus = "manual")}> + <FontAwesomeIcon icon={"play-circle"} /> + <div style={{ display: this.props.PanelWidth() > 200 ? "inline-flex" : "none" }}> Present</div> + </div> + <div className={`presBox-button-right ${this.presentTools ? "active" : ""}`} + onClick={(action(() => { + if (this.childDocs.length > 0) this.presentTools = !this.presentTools; + }))}> + <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"angle-down"} /> + {this.presentDropdown} + </div> + </span> + {this.playButtons} </div> </div> - <div className="presBox-listCont" > + ); + } + + @computed get playButtonFrames() { + const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + return ( + <> + {targetDoc ? <div className="miniPres-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? "inline-flex" : "none" }}> + <div>{targetDoc.currentFrame}</div> + <div className="miniPres-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> + <div>{targetDoc.lastFrame}</div> + </div> : null} + </> + ); + } + + @computed get playButtons() { + // Case 1: There are still other frames and should go through all frames before going to next slide + return (<div className="miniPresOverlay" style={{ display: this.layoutDoc.presStatus !== "edit" ? "inline-flex" : "none" }}> + <div className="miniPres-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> + <div className="miniPres-button" onClick={() => this.startAutoPres(this.itemIndex)}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div> + <div className="miniPres-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="miniPres-divider"></div> + <div className="miniPres-button-text" style={{ display: this.props.PanelWidth() > 250 ? "inline-flex" : "none" }}> + Slide {this.itemIndex + 1} / {this.childDocs.length} + {this.playButtonFrames} + </div> + <div className="miniPres-divider"></div> + {this.props.PanelWidth() > 250 ? <div className="miniPres-button-text" onClick={() => this.layoutDoc.presStatus = "edit"}>EXIT</div> + : <div className="miniPres-button" onClick={() => this.layoutDoc.presStatus = "edit"}> + <FontAwesomeIcon icon={"times"} /> + </div>} + </div>); + } + + render() { + // calling this method for keyEvents + this.isPres; + // needed to ensure that the childDocs are loaded for looking up fields + this.childDocs.slice(); + const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + return <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined }} > + {this.topPanel} + {this.toolbar} + {this.newDocumentToolbarDropdown} + <div className="presBox-listCont"> {mode !== CollectionViewType.Invalid ? <CollectionView {...this.props} ContainingCollectionDoc={this.props.Document} @@ -346,9 +1729,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } Scripting.addGlobal(function lookupPresBoxField(container: Doc, field: string, data: Doc) { if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data); - if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 50 : 46; + if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 30 : 26; if (field === 'presStatus') return container.presStatus; if (field === '_itemIndex') return container._itemIndex; if (field === 'presBox') return container; return undefined; -}); +});
\ No newline at end of file diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx index 0fff0b57f..1b6056be6 100644 --- a/src/client/views/nodes/QueryBox.tsx +++ b/src/client/views/nodes/QueryBox.tsx @@ -1,41 +1,38 @@ -import React = require("react"); -import { IReactionDisposer } from "mobx"; -import { observer } from "mobx-react"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { Id } from '../../../fields/FieldSymbols'; -import { makeInterface, listSpec } from "../../../fields/Schema"; -import { StrCast, Cast } from "../../../fields/Types"; -import { ViewBoxAnnotatableComponent } from '../DocComponent'; -import { SearchBox } from "../search/SearchBox"; -import { FieldView, FieldViewProps } from './FieldView'; -import "./QueryBox.scss"; -import { List } from "../../../fields/List"; -import { SnappingManager } from "../../util/SnappingManager"; +// import React = require("react"); +// import { IReactionDisposer } from "mobx"; +// import { observer } from "mobx-react"; +// import { documentSchema } from "../../../new_fields/documentSchemas"; +// import { Id } from '../../../new_fields/FieldSymbols'; +// import { makeInterface, listSpec } from "../../../new_fields/Schema"; +// import { StrCast, Cast } from "../../../new_fields/Types"; +// import { ViewBoxAnnotatableComponent } from '../DocComponent'; +// import { SearchBox } from "../search/SearchBox"; +// import { FieldView, FieldViewProps } from './FieldView'; +// import "./QueryBox.scss"; +// import { List } from "../../../new_fields/List"; +// import { SnappingManager } from "../../util/SnappingManager"; -type QueryDocument = makeInterface<[typeof documentSchema]>; -const QueryDocument = makeInterface(documentSchema); +// type QueryDocument = makeInterface<[typeof documentSchema]>; +// const QueryDocument = makeInterface(documentSchema); -@observer -export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryDocument>(QueryDocument) { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); } - _docListChangedReaction: IReactionDisposer | undefined; - componentDidMount() { - } +// @observer +// export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryDocument>(QueryDocument) { +// public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); } +// _docListChangedReaction: IReactionDisposer | undefined; +// componentDidMount() { +// } - componentWillUnmount() { - this._docListChangedReaction?.(); - } +// componentWillUnmount() { +// this._docListChangedReaction?.(); +// } - render() { - const dragging = !SnappingManager.GetIsDragging() ? "" : "-dragging"; - return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} > - <SearchBox - id={this.props.Document[Id]} - setSearchQuery={q => this.dataDoc.searchQuery = q} - searchQuery={StrCast(this.dataDoc.searchQuery)} - setSearchFileTypes={q => this.dataDoc.searchFileTypes = new List<string>(q)} - searchFileTypes={Cast(this.dataDoc.searchFileTypes, listSpec("string"), [])} - filterQquery={StrCast(this.dataDoc.filterQuery)} /> - </div >; - } -}
\ No newline at end of file +// render() { +// const dragging = !SnappingManager.GetIsDragging() ? "" : "-dragging"; +// return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} > + +// <SearchBox Document={this.props.Document} /> +// </div >; +// } +// } + +// //<SearchBox id={this.props.Document[Id]} sideBar={side} Document={this.props.Document} searchQuery={StrCast(this.dataDoc.searchQuery)} filterQuery={this.dataDoc.filterQuery} /> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 646a94aa7..3283f568a 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -536,7 +536,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum highlight = (color: string) => { // creates annotation documents for current highlights const effectiveAcl = GetEffectiveAcl(this.props.Document); - const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) ? this.makeAnnotationDocument(color) : undefined; annotationDoc && this.addDocument?.(annotationDoc); return annotationDoc ?? undefined; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index fc65f34eb..b0bf54be6 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -2,7 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from "lodash"; -import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap, selectAll } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -93,6 +93,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _undoTyping?: UndoManager.Batch; private _disposers: { [name: string]: IReactionDisposer } = {}; private _dropDisposer?: DragManager.DragDropDisposer; + private _first: Boolean = true; + private _recordingStart: number = 0; + private _currentTime: number = 0; + private _linkTime: number | null = null; + private _pause: boolean = false; @computed get _recording() { return this.dataDoc.audioState === "recording"; } set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } @@ -140,6 +145,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp super(props); FormattedTextBox.Instance = this; this.updateHighlights(); + this._recordingStart = Date.now(); + this.layoutDoc._timeStampOnEnter = true; } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -197,9 +204,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } dispatchTransaction = (tx: Transaction) => { + let timeStamp; + clearTimeout(timeStamp); if (this._editorView) { + const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); if (metadata) { + const range = tx.selection.$from.blockRange(tx.selection.$to); let text = range ? tx.doc.textBetween(range.start, range.end) : ""; let textEndSelection = tx.selection.to; @@ -221,6 +232,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.dataDoc[key] = value; } } + const state = this._editorView.state.apply(tx); this._editorView.updateState(state); @@ -233,19 +245,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const json = JSON.stringify(state.toJSON()); let unchanged = true; const effectiveAcl = GetEffectiveAcl(this.dataDoc); + + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { this._applyingChange = true; - (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); + const lastmodified = "lastmodified"; + (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))) && (this.dataDoc[lastmodified] = new DateField(new Date(Date.now()))); if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (json.replace(/"selection":.*/, "") !== curLayout?.Data.replace(/"selection":.*/, "")) { + if (!this._pause && !this.layoutDoc._timeStampOnEnter) { + timeStamp = setTimeout(() => this.pause(), 10 * 1000); // 10 seconds delay for time stamp + } + + // if 10 seconds have passed, insert time stamp the next time you type + if (this._pause) { + this._pause = false; + this.insertTime(); + } !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily)); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); unchanged = false; + } + } else { // if we've deleted all the text in a note driven by a template, then restore the template data this.dataDoc[this.props.fieldKey] = undefined; this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); @@ -259,6 +285,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } else { + const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!); json.selection = state.toJSON().selection; this._editorView.updateState(EditorState.fromJSON(this.config, json)); @@ -266,11 +293,66 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + pause = () => this._pause = true; + + formatTime = (time: number) => { + const hours = Math.floor(time / 60 / 60); + const minutes = Math.floor(time / 60) - (hours * 60); + const seconds = time % 60; + + return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); + } + + // for inserting timestamps + insertTime = () => { + if (this._first) { + this._first = false; + DocListCast(this.dataDoc.links).map((l, i) => { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + this._linkTime = NumCast(l.anchor2_timecode); + if (Doc.AreProtosEqual(la2, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + this._linkTime = NumCast(l.anchor1_timecode); + } + + }); + } + this._currentTime = Date.now(); + let time; + this._linkTime ? time = this.formatTime(Math.round(this._linkTime + this._currentTime / 1000 - this._recordingStart / 1000)) : time = null; + + if (this._editorView) { + const state = this._editorView.state; + const now = Date.now(); + let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) }); + if (!this._break && state.selection.to !== state.selection.from) { + for (let i = state.selection.from; i <= state.selection.to; i++) { + const pos = state.doc.resolve(i); + const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark); + if (um) { + mark = um; + break; + } + } + } + if (time) { + let value = ""; + this._break = false; + value = this.layoutDoc._timeStampOnEnter ? "[" + time + "] " : "\n" + "[" + time + "] "; + const from = state.selection.from; + const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); + this._editorView.dispatch(this._editorView.state.tr.insertText(value)); + } + } + } + updateTitle = () => { if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.rootDoc.customTitle) { let node = this._editorView.state.doc; - while (node.firstChild) node = node.firstChild; + while (node.firstChild && node.firstChild.type.name !== "text") node = node.firstChild; const str = node.textContent; const titlestr = str.substr(0, Math.min(40, str.length)); this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); @@ -292,18 +374,40 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link)); } } - public highlightSearchTerms = (terms: string[]) => { + public highlightSearchTerms = (terms: string[], alt: boolean) => { if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { + const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); + const length = res[0].length; let tr = this._editorView.state.tr; const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); + + const lastSel = Math.min(flattened.length - 1, this._searchIndex); flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark)); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); + if (alt === true) { + if (this._searchIndex > 1) { + this._searchIndex += -2; + } + else if (this._searchIndex === 1) { + this._searchIndex = length - 1; + } + else if (this._searchIndex === 0 && length !== 1) { + this._searchIndex = length - 2; + } + + } + else { + + } + const index = this._searchIndex; + + Doc.GetProto(this.dataDoc).searchIndex = index; } } @@ -314,6 +418,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); const end = this._editorView.state.doc.nodeSize - 2; this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + } if (FormattedTextBox.PasteOnLoad) { const pdfDocId = FormattedTextBox.PasteOnLoad.clipboardData?.getData("dash/pdfOrigin"); @@ -500,6 +605,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp uicontrols.push({ description: `${this.layoutDoc._showSidebar ? "Hide" : "Show"} Sidebar`, event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); + uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" }); !Doc.UserDoc().noviceMode && uicontrols.push({ description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt" @@ -536,6 +642,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); }, icon: "eye" }); + appearanceItems.push({ description: "Create progressivized slide...", event: this.progressivizeText, icon: "desktop" }); cm.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const options = cm.findByDescription("Options..."); @@ -547,6 +654,67 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._downX = this._downY = Number.NaN; } + progressivizeText = () => { + const list = this.ProseRef?.getElementsByTagName("li"); + const mainBulletText: string[] = []; + const mainBulletList: Doc[] = []; + if (list) { + const newBullets: Doc[] = this.recursiveProgressivize(1, list)[0]; + mainBulletList.push.apply(mainBulletList, newBullets); + } + console.log(mainBulletList.length); + const title = Docs.Create.TextDocument(StrCast(this.rootDoc.title), { title: "Title", _width: 800, _height: 70, x: 20, y: -10, _fontSize: '20pt', backgroundColor: "rgba(0,0,0,0)", appearFrame: 0, _fontWeight: 700 }); + mainBulletList.push(title); + const doc = Docs.Create.FreeformDocument(mainBulletList, { + title: StrCast(this.rootDoc.title), + x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, + _width: 400, _height: 225, _fitToBox: true, + }); + this.props.addDocument?.(doc); + } + + recursiveProgressivize = (nestDepth: number, list: HTMLCollectionOf<HTMLLIElement>, d?: number, y?: number, before?: string): [Doc[], number] => { + const mainBulletList: Doc[] = []; + let b = d ? d : 0; + let yLoc = y ? y : 0; + let nestCount = 0; + let count: string = before ? before : ''; + const fontSize: string = (16 - (nestDepth * 2)) + 'pt'; + const xLoc: number = (nestDepth * 20); + const width: number = 390 - xLoc; + const height: number = 55 - (nestDepth * 5); + Array.from(list).forEach(listItem => { + const mainBullets: number = Number(listItem.getAttribute("data-bulletstyle")); + if (mainBullets === nestDepth) { + if (listItem.childElementCount > 1) { + b++; + nestCount++; + yLoc += height; + count = before ? count + nestCount + "." : nestCount + "."; + const text = listItem.getElementsByTagName("p")[0].innerText; + const length = text.length; + const bullet1 = Docs.Create.TextDocument(count + " " + text, { title: "Slide text", _width: width, _autoHeight: true, x: xLoc, y: (yLoc), _fontSize: fontSize, backgroundColor: "rgba(0,0,0,0)", appearFrame: d ? d : b }); + // yLoc += NumCast(bullet1._height); + mainBulletList.push(bullet1); + const newList = this.recursiveProgressivize(nestDepth + 1, listItem.getElementsByTagName("li"), b, yLoc, count); + mainBulletList.push.apply(mainBulletList, newList[0]); + yLoc += newList.length * (55 - ((nestDepth + 1) * 5)); + } else { + b++; + nestCount++; + yLoc += height; + count = before ? count + nestCount + "." : nestCount + "."; + const text = listItem.innerText; + const length = text.length; + const bullet1 = Docs.Create.TextDocument(count + " " + text, { title: "Slide text", _width: width, _autoHeight: true, x: xLoc, y: (yLoc), _fontSize: fontSize, backgroundColor: "rgba(0,0,0,0)", appearFrame: d ? d : b }); + // yLoc += NumCast(bullet1._height); + mainBulletList.push(bullet1); + } + } + }); + return [mainBulletList, yLoc]; + } + recordDictation = () => { DictationManager.Controls.listen({ interimHandler: this.setCurrentBulletContent, @@ -738,8 +906,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.setupEditor(this.config, this.props.fieldKey); + this._disposers.searchAlt = reaction(() => this.rootDoc.searchMatchAlt, + search => search ? this.highlightSearchTerms([Doc.SearchQuery()], false) : this.unhighlightSearchTerms(), + { fireImmediately: true }); this._disposers.search = reaction(() => this.rootDoc.searchMatch, - search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(), + search => search ? this.highlightSearchTerms([Doc.SearchQuery()], true) : this.unhighlightSearchTerms(), { fireImmediately: this.rootDoc.searchMatch ? true : false }); this._disposers.record = reaction(() => this._recording, @@ -1294,6 +1465,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } e.stopPropagation(); if (e.key === "Tab" || e.key === "Enter") { + if (e.key === "Enter" && this.layoutDoc._timeStampOnEnter) { + this.insertTime(); + } e.preventDefault(); } if (e.key === " " || this._lastTimedMark?.attrs.userid !== Doc.CurrentUserEmail) { @@ -1374,6 +1548,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"), pointerEvents: interactive ? undefined : "none", fontSize: Cast(this.layoutDoc._fontSize, "string", null), + fontWeight: Cast(this.layoutDoc._fontWeight, "number", null), fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), transition: "opacity 1s" }} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 6e268be48..459632ec8 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -274,7 +274,7 @@ export default class RichTextMenu extends AntimodeMenu { } !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily)))); !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize)))); - !activeColors.length && (activeSizes.push(StrCast(this.TextView.layoutDoc.color, StrCast(Doc.UserDoc().fontColor)))); + !activeColors.length && (activeColors.push(StrCast(this.TextView.layoutDoc.color, StrCast(Doc.UserDoc().fontColor)))); } !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily))); !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize))); @@ -903,8 +903,9 @@ export default class RichTextMenu extends AntimodeMenu { ref_node = pos.nodeBefore; } if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { - if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) + if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) { ref_node = pos.nodeAfter; + } } if (!ref_node && pos.pos > 0) { let skip = false; diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index bcd6f716b..ce784c3d9 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -31,7 +31,7 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.getAttribute("targetids") }], location: dom.getAttribute("location"), }; + return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.dataset.targetids }], location: dom.getAttribute("location"), }; } }], toDOM(node: any) { @@ -40,10 +40,10 @@ export const marks: { [index: string]: MarkSpec } = { return node.attrs.docref && node.attrs.title ? ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] : node.attrs.allLinks.length === 1 ? - ["a", { ...node.attrs, class: linkids, targetids, style: `text-decoration: ${linkids === " " ? "underline" : undefined}`, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href }, 0] : + ["a", { ...node.attrs, class: linkids, dataTargetids: targetids, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href, style: `text-decoration: ${linkids === " " ? "underline" : undefined}` }, 0] : ["div", { class: "prosemirror-anchor" }, ["span", { class: "prosemirror-linkBtn" }, - ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}` }, 0], + ["a", { ...node.attrs, class: linkids, dataTargetids: targetids, title: `${node.attrs.title}` }, 0], ["input", { class: "prosemirror-hrefoptions" }], ], ["div", { class: "prosemirror-links" }, ...node.attrs.allLinks.map((item: { href: string, title: string }) => diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 5a43a076b..cfa9a1844 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -9,8 +9,8 @@ import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; -import { createSchema, makeInterface } from "../../../fields/Schema"; -import { ScriptField } from "../../../fields/ScriptField"; +import { createSchema, makeInterface, listSpec } from "../../../fields/Schema"; +import { ScriptField, ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; @@ -106,6 +106,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _scrollTopReactionDisposer?: IReactionDisposer; private _filterReactionDisposer?: IReactionDisposer; private _searchReactionDisposer?: IReactionDisposer; + private _searchReactionDisposer2?: IReactionDisposer; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _selectionText: string = ""; @@ -336,6 +337,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu nextAnnotation = () => { this.Index = Math.min(this.Index + 1, this.allAnnotations.length - 1); this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]); + this.Document.searchIndex = this.Index; + } @action @@ -344,6 +347,11 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } @action + scrollToFrame = (duration: number, top: number) => { + this._mainCont.current && smoothScroll(duration, this._mainCont.current, top); + } + + @action scrollToAnnotation = (scrollToAnnotation: Doc) => { if (scrollToAnnotation) { const offset = this.visibleHeight() / 2; @@ -403,6 +411,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu phraseSearch: true, query: searchString }); + this.Document.searchIndex = this.Index; } else if (this._mainCont.current) { const executeFind = () => { @@ -416,7 +425,9 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu }; this._mainCont.current.addEventListener("pagesloaded", executeFind); this._mainCont.current.addEventListener("pagerendered", executeFind); + this.Document.searchIndex = this.Index; } + } @action diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss index dbe6b0d4f..1e776384a 100644 --- a/src/client/views/presentationview/PresElementBox.scss +++ b/src/client/views/presentationview/PresElementBox.scss @@ -1,11 +1,18 @@ +$light-blue: #AEDDF8; +$dark-blue: #5B9FDD; +$light-background: #ececec; + .presElementBox-item { - display: inline-block; - background-color: #eeeeee; + display: grid; + grid-template-columns: max-content max-content max-content max-content; + background-color: #d5dce2; + font-family: Roboto; + letter-spacing: normal; + position: relative; pointer-events: all; width: 100%; height: 100%; - outline-color: maroon; - outline-style: dashed; + font-weight: 400; border-radius: 6px; -webkit-touch-callout: none; -webkit-user-select: none; @@ -13,10 +20,36 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - transition: all .1s; + transition: all .3s; padding: 0px; padding-bottom: 3px; + .presElementBox-highlight { + position: absolute; + transform: translate(-100px, -4px); + z-index: -1; + width: calc(100% + 200px); + height: calc(100% + 8px); + background-color: $light-blue; + } + + .presElementBox-highlightTop { + position: absolute; + transform: translate(-100px, -4px); + z-index: -1; + width: calc(100% + 200px); + height: calc(50% + 4px); + } + + .presElementBox-highlightBottom { + position: absolute; + transform: translate(-100px, 0px); + z-index: -1; + top: 50%; + width: calc(100% + 200px); + height: calc(50% + 4px); + } + .documentView-node { position: absolute; z-index: 1; @@ -33,62 +66,82 @@ .presElementBox-item:hover { transition: all .1s; - background: #AAAAAA; + background: #98b7da; border-radius: 6px; } .presElementBox-active { - background: gray; color: black; border-radius: 6px; - box-shadow: black 2px 2px 5px; + border: solid 2px $dark-blue; } -.presElementBox-closeIcon { - border-radius: 20px; - transform: scale(0.7); - position: absolute; - right: 0; - top: 0; - padding: 8px; -} - - .presElementBox-buttons { - display: flow-root; - position: relative; + display: grid; + grid-template-rows: 15px; + top: 15px; + left: -20; + position: absolute; width: 100%; height: auto; .presElementBox-interaction { - color: gray; - float: left; - padding: 0px; - width: 20px; - height: 20px; + display: none; } .presElementBox-interaction-selected { - color: white; + color: grey; + background-color: rgba(0, 0, 0, 0); float: left; padding: 0px; width: 20px; height: 20px; - border: solid 1px darkgray; } } -.presElementBox-name { +.presElementBox-number { font-size: 12px; + width: 20; + font-weight: 700; + text-align: right; + justify-content: center; + align-content: center; + left: -20; position: absolute; display: inline-block; - width: calc(100% - 45px); + overflow: hidden; +} + +.presElementBox-name { + align-self: center; + font-size: 13px; + font-family: Roboto; + font-weight: 500; + position: relative; + top: 1px; + padding-left: 10px; + padding-right: 10px; + letter-spacing: normal; + width: max-content; text-overflow: ellipsis; overflow: hidden; white-space: pre; } +.presElementBox-time { + align-self: center; + position: relative; + top: 2px; + padding-right: 10px; + font-size: 10; + font-weight: 300; + font-family: Roboto; + z-index: 300; + letter-spacing: normal; +} + .presElementBox-embedded { + grid-column: 1/8; position: relative; display: flex; width: auto; @@ -100,51 +153,56 @@ width: 100%; height: 100%; position: absolute; - left: 0; + border-radius: 3px; top: 0; - background: transparent; - z-index: 2; + left: 0; + z-index: 1; + overflow: hidden; } -@media only screen and (max-device-width: 480px) { - .presElementBox-buttons { - display: inline-flex; - position: absolute; - top: 0; - right: 0; - z-index: 3; - width: 50%; - - .presElementBox-interaction { - width: 50; - height: 50; - } - - .presElementBox-interaction-selected { - width: 50; - height: 50; - } - } - - .presElementBox-item { - display: inline-flex; - overflow: hidden; - } - - .presElementBox-closeIcon { - transform: scale(1.5); - right: 10; - top: 10; - } +.presElementBox-closeIcon { + position: absolute; + border-radius: 100%; + z-index: 300; + right: 3px; + top: 3px; + width: 20px; + height: 20px; + display: flex; + font-size: 75%; + background-color: black; + color: white; + justify-content: center; + align-items: center; +} - .presElementBox-name { - font-size: 30px; - top: 10px; - z-index: 3; - width: 50%; - } +.presElementBox-expand { + position: absolute; + border-radius: 100%; + z-index: 300; + right: 26px; + top: 3px; + width: 20px; + height: 20px; + display: flex; + font-size: 75%; + background-color: black; + color: white; + justify-content: center; + align-items: center; +} - .presElementBox-embedded { - transform: translate(0, 90px) scale(1.5); - } +.presElementBox-expand-selected { + position: absolute; + border-radius: 100%; + right: 3px; + bottom: 3px; + width: 20px; + height: 20px; + z-index: 300; + display: flex; + background-color: black; + color: white; + justify-content: center; + align-items: center; }
\ No newline at end of file diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 4c0972736..a6dbb76ef 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -5,8 +5,8 @@ import { Doc, DataSym, DocListCast } from "../../../fields/Doc"; import { documentSchema } from '../../../fields/documentSchemas'; import { Id } from "../../../fields/FieldSymbols"; import { createSchema, makeInterface, listSpec } from '../../../fields/Schema'; -import { Cast, NumCast, BoolCast, ScriptCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnFalse, returnTrue, returnOne, returnZero, numberRange } from "../../../Utils"; +import { Cast, NumCast, BoolCast, ScriptCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, emptyPath, returnFalse, returnTrue, returnOne, returnZero, numberRange, setupMoveUpEvents } from "../../../Utils"; import { Transform } from "../../util/Transform"; import { CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from '../DocComponent'; @@ -15,7 +15,10 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./PresElementBox.scss"; import React = require("react"); import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { PresBox } from "../nodes/PresBox"; import { DocumentType } from "../../documents/DocumentTypes"; +import { Tooltip } from "@material-ui/core"; +import { DragManager } from "../../util/DragManager"; export const presSchema = createSchema({ presentationTargetDoc: Doc, @@ -38,19 +41,18 @@ const PresDocument = makeInterface(presSchema, documentSchema); @observer export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDocument>(PresDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); } - _heightDisposer: IReactionDisposer | undefined; // these fields are conditionally computed fields on the layout document that take this document as a parameter @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) - @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation elemnt template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up - @computed get presStatus() { return BoolCast(this.lookupField("presStatus")); } + @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up + @computed get presStatus() { return StrCast(this.lookupField("presStatus")); } @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); } @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); } @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; } componentDidMount() { this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], - params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 200 : 0), { fireImmediately: true }); + params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true }); } componentWillUnmount() { this._heightDisposer?.(); @@ -69,7 +71,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc this.targetDoc.opacity = 1; } } else { - if (this.presStatus && this.indexInPres > this.itemIndex && this.targetDoc) { + if (this.presStatus !== "edit" && this.indexInPres > this.itemIndex && this.targetDoc) { this.targetDoc.opacity = 0; } } @@ -90,7 +92,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc } } else { if (this.rootDoc.presFadeButton) this.rootDoc.presFadeButton = false; - if (this.presStatus && this.indexInPres < this.itemIndex && this.targetDoc) { + if (this.presStatus !== "edit" && this.indexInPres < this.itemIndex && this.targetDoc) { this.targetDoc.opacity = 0; } } @@ -125,7 +127,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc } } else { this.rootDoc.presHideAfterButton = false; - if (this.presStatus && (this.indexInPres < this.itemIndex) && this.targetDoc) { + if (this.presStatus !== "edit" && (this.indexInPres < this.itemIndex) && this.targetDoc) { this.targetDoc.opacity = 0.5; } } @@ -166,10 +168,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc */ ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord]; - embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); + @action + presExpandDocumentClick = () => { + this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; + } + + embedHeight = () => 100; + // embedWidth = () => this.props.PanelWidth(); + // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); embedWidth = () => this.props.PanelWidth() - 20; /** - * The function that is responsible for rendering the a preview or not for this + * The function that is responsible for rendering a preview or not for this * presentation element. */ @computed get renderEmbeddedInline() { @@ -207,33 +216,144 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc </div>; } + @computed get duration() { + let durationInS: number; + if (this.targetDoc.presDuration) durationInS = NumCast(this.targetDoc.presDuration) / 1000; + else durationInS = 2; + return "D: " + durationInS + "s"; + } + + @computed get transition() { + let transitionInS: number; + if (this.targetDoc.presTransition) transitionInS = NumCast(this.targetDoc.presTransition) / 1000; + else transitionInS = 0.5; + return "M: " + transitionInS + "s"; + } + + private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); + + headerDown = (e: React.PointerEvent<HTMLDivElement>) => { + const element = document.elementFromPoint(e.clientX, e.clientY)?.parentElement; + e.stopPropagation(); + e.preventDefault(); + if (element) { + if (PresBox.Instance._eleArray.includes(element)) { + setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); + } + } + } + + headerUp = (e: React.PointerEvent<HTMLDivElement>) => { + e.stopPropagation(); + e.preventDefault(); + DragManager.docsBeingDragged = []; + this._highlightTopRef.current!.style.borderBottom = "0px"; + this._highlightBottomRef.current!.style.borderBottom = "0px"; + } + + startDrag = (e: PointerEvent, down: number[], delta: number[]) => { + const activeItem = this.rootDoc; + const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray().map(doc => doc)); + const dragItem: HTMLElement[] = []; + PresBox.Instance._dragArray.map(ele => { + const drag = ele; + drag.style.backgroundColor = "#d5dce2"; + drag.style.borderRadius = '5px'; + dragItem.push(drag); + }); + if (activeItem) { + DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY); + activeItem.dragging = true; + return true; + } + return false; + } + + private _highlightTopRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _highlightBottomRef: React.RefObject<HTMLDivElement> = React.createRef(); + + + onPointerTop = (e: React.PointerEvent<HTMLDivElement>) => { + if (DragManager.docsBeingDragged.length > 0) { + this._highlightTopRef.current!.style.borderTop = "solid 2px #5B9FDD"; + } + } + + onPointerBottom = (e: React.PointerEvent<HTMLDivElement>) => { + if (DragManager.docsBeingDragged.length > 0) { + this._highlightBottomRef.current!.style.borderBottom = "solid 2px #5B9FDD"; + } + } + + onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => { + if (DragManager.docsBeingDragged.length > 0) { + this._highlightBottomRef.current!.style.borderBottom = "0px"; + this._highlightTopRef.current!.style.borderTop = "0px"; + } + } + render() { const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree; - const className = "presElementBox-item" + (this.itemIndex === this.indexInPres ? " presElementBox-active" : ""); + const className = "presElementBox-item" + (PresBox.Instance._selectedArray.includes(this.rootDoc) ? " presElementBox-active" : ""); const pbi = "presElementBox-interaction"; return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : ( <div className={className} key={this.props.Document[Id] + this.indexInPres} + ref={this._itemRef} style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }} - onClick={e => { this.props.focus(this.rootDoc); e.stopPropagation(); }}> - {treecontainer ? (null) : <> - <strong className="presElementBox-name"> - {`${this.indexInPres + 1}. ${this.targetDoc?.title}`} - </strong> - <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => { - this.props.removeDocument?.(this.rootDoc); - e.stopPropagation(); - }}>X</button> - <br /> - </>} - <div className="presElementBox-buttons"> + onClick={e => { + e.stopPropagation(); + e.preventDefault(); + // Command/ control click + if (e.ctrlKey || e.metaKey) { + PresBox.Instance.multiSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!); + // Shift click + } else if (e.shiftKey) { + PresBox.Instance.shiftSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!); + // Regular click + } else { + this.props.focus(this.rootDoc); + PresBox.Instance._eleArray = []; + PresBox.Instance._eleArray.push(this._itemRef.current!); + PresBox.Instance._dragArray = []; + PresBox.Instance._dragArray.push(this._dragRef.current!); + } + }} + onPointerDown={this.headerDown} + onPointerUp={this.headerUp} + > + <> + <div className="presElementBox-number"> + {`${this.indexInPres + 1}.`} + </div> + <div ref={this._dragRef} className="presElementBox-name" style={{ maxWidth: (PresBox.Instance.toolbarWidth - 70) }}> + {`${this.targetDoc?.title}`} + </div> + <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.transition}</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.duration}</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Presentation pin view"}</div></>}><div className="presElementBox-time" style={{ fontWeight: 700, display: this.rootDoc.presPinView && PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>V</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div + className="presElementBox-closeIcon" + onClick={e => { + this.props.removeDocument?.(this.rootDoc); + e.stopPropagation(); + }}> + <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"presElementBox-expand" + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> + <FontAwesomeIcon icon={(this.rootDoc.presExpandInlineButton ? "angle-up" : "angle-down")} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + </> + <div ref={this._highlightTopRef} onPointerOver={this.onPointerTop} onPointerLeave={this.onPointerLeave} className="presElementBox-highlightTop" style={{ zIndex: 299, backgroundColor: "rgba(0,0,0,0)" }} /> + <div ref={this._highlightBottomRef} onPointerOver={this.onPointerBottom} onPointerLeave={this.onPointerLeave} className="presElementBox-highlightBottom" style={{ zIndex: 299, backgroundColor: "rgba(0,0,0,0)" }} /> + <div className="presElementBox-highlight" style={{ backgroundColor: PresBox.Instance._selectedArray.includes(this.rootDoc) ? "#AEDDF8" : "rgba(0,0,0,0)" }} /> + <div className="presElementBox-buttons" style={{ display: this.rootDoc.presExpandInlineButton ? "grid" : "none" }}> <button title="Zoom" className={pbi + (this.rootDoc.presZoomButton ? "-selected" : "")} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} onPointerDown={e => e.stopPropagation()} /></button> <button title="Navigate" className={pbi + (this.rootDoc.presNavButton ? "-selected" : "")} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} onPointerDown={e => e.stopPropagation()} /></button> <button title="Hide Before" className={pbi + (this.rootDoc.presHideTillShownButton ? "-selected" : "")} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={"file"} onPointerDown={e => e.stopPropagation()} /></button> - <button title="Fade After" className={pbi + (this.rootDoc.presFadeButton ? "-selected" : "")} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={"file-download"} onPointerDown={e => e.stopPropagation()} /></button> <button title="Hide After" className={pbi + (this.rootDoc.presHideAfterButton ? "-selected" : "")} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={"file-download"} onPointerDown={e => e.stopPropagation()} /></button> - <button title="Group With Up" className={pbi + (this.rootDoc.presGroupButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.rootDoc.presGroupButton = !this.rootDoc.presGroupButton; }}><FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} /></button> - <button title="Progressivize" className={pbi + (this.rootDoc.pres ? "-selected" : "")} onClick={this.progressivize}><FontAwesomeIcon icon={"tasks"} onPointerDown={e => e.stopPropagation()} /></button> - <button title="Expand Inline" className={pbi + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; }}><FontAwesomeIcon icon={"arrow-down"} onPointerDown={e => e.stopPropagation()} /></button> + <button title="Progressivize" className={pbi + (this.rootDoc.presProgressivize ? "-selected" : "")} onClick={this.progressivize}><FontAwesomeIcon icon={"tasks"} onPointerDown={e => e.stopPropagation()} /></button> + <button title="Effect" className={pbi + (this.rootDoc.presEffect ? "-selected" : "")}>E</button> </div> {this.renderEmbeddedInline} </div> diff --git a/src/client/views/search/FieldFilters.scss b/src/client/views/search/FieldFilters.scss deleted file mode 100644 index e1d0d8df5..000000000 --- a/src/client/views/search/FieldFilters.scss +++ /dev/null @@ -1,12 +0,0 @@ -.field-filters { - width: 100%; - display: grid; - // grid-template-columns: 18% 20% 60%; - grid-template-columns: 20% 25% 60%; -} - -.field-filters-required { - width: 100%; - display: grid; - grid-template-columns: 50% 50%; -}
\ No newline at end of file diff --git a/src/client/views/search/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx deleted file mode 100644 index 7a33282d2..000000000 --- a/src/client/views/search/FieldFilters.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from 'react'; -import { observable } from 'mobx'; -import { CheckBox } from './CheckBox'; -import { Keys } from './FilterBox'; -import "./FieldFilters.scss"; - -export interface FieldFilterProps { - titleFieldStatus: boolean; - dataFieldStatus: boolean; - authorFieldStatus: boolean; - updateTitleStatus(stat: boolean): void; - updateAuthorStatus(stat: boolean): void; - updateDataStatus(stat: boolean): void; -} - -export class FieldFilters extends React.Component<FieldFilterProps> { - - static Instance: FieldFilters; - - @observable public _resetBoolean = false; - @observable public _resetCounter: number = 0; - - constructor(props: FieldFilterProps) { - super(props); - FieldFilters.Instance = this; - } - - resetFieldFilters() { - this._resetBoolean = true; - } - - render() { - return ( - <div className="field-filters"> - <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.titleFieldStatus} updateStatus={this.props.updateTitleStatus} title={Keys.TITLE} /> - <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.authorFieldStatus} updateStatus={this.props.updateAuthorStatus} title={Keys.AUTHOR} /> - <CheckBox default={false} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={"Deleted Docs"} /> - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss deleted file mode 100644 index 094ea9cc5..000000000 --- a/src/client/views/search/FilterBox.scss +++ /dev/null @@ -1,178 +0,0 @@ -@import "../globalCssVariables"; -@import "./NaviconButton.scss"; - -.filter-form { - padding: 25px; - width: 440px; - position: relative; - right: 1px; - color: grey; - flex-direction: column; - display: inline-block; - transform-origin: top; - overflow: auto; - border-bottom: solid black 3px; - - .top-filter-header { - - #header { - text-transform: uppercase; - letter-spacing: 2px; - font-size: 13; - width: 80%; - } - - .close-icon { - width: 20%; - opacity: .6; - position: relative; - display: block; - - .line { - display: block; - background: $alt-accent; - width: 20; - height: 3; - position: absolute; - right: 0; - border-radius: ($height-line / 2); - - &.line-1 { - transform: rotate(45deg); - top: 45%; - } - - &.line-2 { - transform: rotate(-45deg); - top: 45%; - } - } - } - - .close-icon:hover { - opacity: 1; - } - - } - - .filter-options { - - .filter-div { - margin-top: 10px; - margin-bottom: 10px; - display: inline-block; - width: 100%; - border-color: rgba(178, 206, 248, .2); // $darker-alt-accent - border-top-style: solid; - - .filter-header { - display: flex; - align-items: center; - margin-bottom: 10px; - letter-spacing: 2px; - - .filter-title { - font-size: 13; - text-transform: uppercase; - margin-top: 10px; - margin-bottom: 10px; - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - } - } - - .filter-header:hover .filter-title { - transform: scale(1.05); - } - - .filter-panel { - max-height: 0px; - width: 100%; - overflow: hidden; - opacity: 0; - transform-origin: top; - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - text-align: center; - } - } - } - - .filter-buttons { - border-color: rgba(178, 206, 248, .2); // $darker-alt-accent - border-top-style: solid; - padding-top: 10px; - } -} - -.active-filters { - display: flex; - flex-direction: row-reverse; - justify-content: flex-end; - width: 100%; - margin-right: 30px; - position: relative; - - .active-icon { - max-width: 40px; - flex: initial; - - &.icon { - width: 40px; - text-align: center; - margin-bottom: 5px; - position: absolute; - } - - &.container { - display: flex; - flex-direction: column; - width: 40px; - } - - &.description { - text-align: center; - top: 40px; - position: absolute; - width: 40px; - font-size: 9px; - opacity: 0; - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - } - - &.icon:hover+.description { - opacity: 1; - } - } - - .col-icon { - height: 35px; - margin-left: 5px; - width: 35px; - background-color: black; - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - - .save-filter, - .reset-filter, - .all-filter { - background-color: gray; - } - - .save-filter:hover, - .reset-filter:hover, - .all-filter:hover { - background-color: $darker-alt-accent; - } - } -}
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx deleted file mode 100644 index eb61f9a14..000000000 --- a/src/client/views/search/FilterBox.tsx +++ /dev/null @@ -1,431 +0,0 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; -import "./SearchBox.scss"; -import { faTimes, faCheckCircle, faObjectGroup } from '@fortawesome/free-solid-svg-icons'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { Doc } from '../../../fields/Doc'; -import { Id } from '../../../fields/FieldSymbols'; -import { DocumentType } from "../../documents/DocumentTypes"; -import { Cast, StrCast } from '../../../fields/Types'; -import * as _ from "lodash"; -import { IconBar } from './IconBar'; -import { FieldFilters } from './FieldFilters'; -import { SelectionManager } from '../../util/SelectionManager'; -import { DocumentView } from '../nodes/DocumentView'; -import { CollectionFilters } from './CollectionFilters'; -import * as $ from 'jquery'; -import "./FilterBox.scss"; -import { SearchBox } from './SearchBox'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -library.add(faTimes); -library.add(faCheckCircle); -library.add(faObjectGroup); - -export enum Keys { - TITLE = "title", - AUTHOR = "author", - DATA = "data" -} - -@observer -export class FilterBox extends React.Component { - - static Instance: FilterBox; - public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; - - //if true, any keywords can be used. if false, all keywords are required. - //this also serves as an indicator if the word status filter is applied - @observable private _basicWordStatus: boolean = true; - @observable private _filterOpen: boolean = false; - //if icons = all icons, then no icon filter is applied - @observable private _icons: string[] = this._allIcons; - //if all of these are true, no key filter is applied - @observable private _anyKeywordStatus: boolean = true; - @observable private _allKeywordStatus: boolean = true; - @observable private _titleFieldStatus: boolean = true; - @observable private _authorFieldStatus: boolean = true; - @observable private _dataFieldStatus: boolean = true; - //this also serves as an indicator if the collection status filter is applied - @observable public _deletedDocsStatus: boolean = false; - @observable private _collectionStatus = false; - @observable private _collectionSelfStatus = true; - @observable private _collectionParentStatus = true; - @observable private _wordStatusOpen: boolean = false; - @observable private _typeOpen: boolean = false; - @observable private _colOpen: boolean = false; - @observable private _fieldOpen: boolean = false; - public _pointerTime: number = -1; - - constructor(props: Readonly<{}>) { - super(props); - FilterBox.Instance = this; - } - setupAccordion() { - $('document').ready(function () { - const acc = document.getElementsByClassName('filter-header'); - // tslint:disable-next-line: prefer-for-of - for (let i = 0; i < acc.length; i++) { - acc[i].addEventListener("click", function (this: HTMLElement) { - this.classList.toggle("active"); - - const panel = this.nextElementSibling as HTMLElement; - if (panel.style.maxHeight) { - panel.style.overflow = "hidden"; - panel.style.maxHeight = ""; - panel.style.opacity = "0"; - } else { - setTimeout(() => { - panel.style.overflow = "visible"; - }, 200); - setTimeout(() => { - panel.style.opacity = "1"; - }, 50); - panel.style.maxHeight = panel.scrollHeight + "px"; - - } - }); - - const el = acc[i] as HTMLElement; - el.click(); - } - }); - } - - @action.bound - minimizeAll() { - $('document').ready(function () { - const acc = document.getElementsByClassName('filter-header'); - - // tslint:disable-next-line: prefer-for-of - for (var i = 0; i < acc.length; i++) { - const classList = acc[i].classList; - if (classList.contains("active")) { - acc[i].classList.toggle("active"); - const panel = acc[i].nextElementSibling as HTMLElement; - panel.style.overflow = "hidden"; - panel.style.maxHeight = ""; - } - } - }); - } - - @action.bound - resetFilters = () => { - this._basicWordStatus = true; - IconBar.Instance.selectAll(); - FieldFilters.Instance.resetFieldFilters(); - } - - basicRequireWords(query: string): string { - const oldWords = query.split(" "); - const newWords: string[] = []; - oldWords.forEach(word => { - const newWrd = "+" + word; - newWords.push(newWrd); - }); - query = newWords.join(" "); - - return query; - } - - basicFieldFilters(query: string, type: string): string { - const oldWords = query.split(" "); - let mod = ""; - - if (type === Keys.AUTHOR) { - mod = " author_t:"; - } if (type === Keys.DATA) { - //TODO - } if (type === Keys.TITLE) { - mod = " title_t:"; - } - - const newWords: string[] = []; - oldWords.forEach(word => { - const newWrd = mod + word; - newWords.push(newWrd); - }); - - query = newWords.join(" "); - - return query; - } - - applyBasicFieldFilters(query: string) { - let finalQuery = ""; - - if (this._titleFieldStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE); - } - if (this._authorFieldStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); - } - if (this._deletedDocsStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); - } - return finalQuery; - } - - get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } - - //TODO: basically all of this - //gets all of the collections of all the docviews that are selected - //if a collection is the only thing selected, search only in that collection (not its container) - getCurCollections(): Doc[] { - const selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments(); - const collections: Doc[] = []; - - selectedDocs.forEach(async element => { - const layout: string = StrCast(element.props.Document.layout); - //checks if selected view (element) is a collection. if it is, adds to list to search through - if (layout.indexOf("Collection") > -1) { - //makes sure collections aren't added more than once - if (!collections.includes(element.props.Document)) { - collections.push(element.props.Document); - } - } - //makes sure collections aren't added more than once - if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) { - collections.push(element.props.ContainingCollectionDoc); - } - }); - - return collections; - } - - getFinalQuery(query: string): string { - //alters the query so it looks in the correct fields - //if this is true, then not all of the field boxes are checked - //TODO: data - if (this.fieldFiltersApplied) { - query = this.applyBasicFieldFilters(query); - query = query.replace(/\s+/g, ' ').trim(); - } - - //alters the query based on if all words or any words are required - //if this._wordstatus is false, all words are required and a + is added before each - if (!this._basicWordStatus) { - query = this.basicRequireWords(query); - query = query.replace(/\s+/g, ' ').trim(); - } - - //if should be searched in a specific collection - if (this._collectionStatus) { - query = this.addCollectionFilter(query); - query = query.replace(/\s+/g, ' ').trim(); - } - return query; - } - - addCollectionFilter(query: string): string { - const collections: Doc[] = this.getCurCollections(); - const oldWords = query.split(" "); - - const collectionString: string[] = []; - collections.forEach(doc => { - const proto = doc.proto; - const protoId = (proto || doc)[Id]; - const colString: string = "{!join from=data_l to=id}id:" + protoId + " "; - collectionString.push(colString); - }); - - let finalColString = collectionString.join(" "); - finalColString = finalColString.trim(); - return "+(" + finalColString + ")" + query; - } - - get filterTypes() { - return this._icons.length === 9 ? undefined : this._icons; - } - - @action - filterDocsByType(docs: Doc[]) { - if (this._icons.length === 9) { - return docs; - } - const finalDocs: Doc[] = []; - docs.forEach(doc => { - const layoutresult = Cast(doc.type, "string"); - if (layoutresult && this._icons.includes(layoutresult)) { - finalDocs.push(doc); - } - }); - return finalDocs; - } - - getABCicon() { - return ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35"> - <path d="M25.4 47.9c-1.3 1.3-1.9 2.8-1.9 4.8 0 3.8 2.3 6.1 6.1 6.1 5.1 0 8-3.3 9-6.2 0.2-0.7 0.4-1.4 0.4-2.1v-6.1c-0.1 0-0.1 0-0.2 0C32.2 44.5 27.7 45.6 25.4 47.9z" /> - <path d="M64.5 28.6c-2.2 0-4.1 1.5-4.7 3.8l0 0.2c-0.1 0.3-0.1 0.7-0.1 1.1v3.3c0 0.4 0.1 0.8 0.2 1.1 0.6 2.2 2.4 3.6 4.6 3.6 3.2 0 5.2-2.6 5.2-6.7C69.5 31.8 68 28.6 64.5 28.6z" /> - <path d="M43.9 0C19.7 0 0 19.7 0 43.9s19.7 43.9 43.9 43.9 43.9-19.6 43.9-43.9S68.1 0 43.9 0zM40.1 65.5l-0.5-4c-3 3.1-7.4 4.9-12.1 4.9 -6.8 0-13.6-4.4-13.6-12.8 0-4 1.3-7.4 4-10 4.1-4.1 11.1-6.2 20.8-6.3 0-5.5-2.9-8.4-8.3-8.4 -3.6 0-7.4 1.1-10.2 2.9l-1.1 0.7 -2.4-6.9 0.7-0.4c3.7-2.4 8.9-3.8 14.1-3.8 10.9 0 16.7 6.2 16.7 17.9V54.6c0 4.1 0.2 7.2 0.7 9.7L49 65.5H40.1zM65.5 67.5c1.8 0 3-0.5 4-0.9l0.5-0.2 0.8 3.4 -0.3 0.2c-1 0.5-3 1.1-5.5 1.1 -5.8 0-9.7-4-9.7-9.9 0-6.1 4.3-10.3 10.4-10.3 2.1 0 4 0.5 4.9 1l0.3 0.2 -1 3.5 -0.5-0.3c-0.7-0.4-1.8-0.8-3.7-0.8 -3.7 0-6.1 2.6-6.1 6.6C59.5 64.8 61.9 67.5 65.5 67.5zM65 45.3c-2.5 0-4.5-0.9-5.9-2.7l-0.1 2.3h-3.8l0-0.5c0.1-1.2 0.2-3.1 0.2-4.8V16.7h4.3v10.8c1.4-1.6 3.5-2.5 6-2.5 2.2 0 4.1 0.8 5.5 2.3 1.8 1.8 2.8 4.5 2.8 7.7C73.8 42.1 69.3 45.3 65 45.3z" /> - </svg> - ); - } - - getTypeIcon() { - return ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35"> - <path d="M43.9 0C19.7 0 0 19.7 0 43.9s19.7 43.9 43.9 43.9 43.9-19.6 43.9-43.9S68.1 0 43.9 0zM43.9 12.2c4.1 0 7.5 3.4 7.5 7.5 0 4.1-3.4 7.5-7.5 7.5 -4.1 0-7.5-3.4-7.5-7.5C36.4 15.5 39.7 12.2 43.9 12.2zM11.9 50.4l7.5-13 7.5 13H11.9zM47.6 75.7h-7.5l-3.7-6.5 3.8-6.5h7.5l3.8 6.5L47.6 75.7zM70.7 70.7c-0.2 0.2-0.4 0.3-0.7 0.3s-0.5-0.1-0.7-0.3l-25.4-25.4 -25.4 25.4c-0.2 0.2-0.4 0.3-0.7 0.3s-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1 0-1.4l25.4-25.4 -25.4-25.4c-0.4-0.4-0.4-1 0-1.4s1-0.4 1.4 0l25.4 25.4 25.4-25.4c0.4-0.4 1-0.4 1.4 0s0.4 1 0 1.4l-25.4 25.4 25.4 25.4C71.1 69.7 71.1 70.3 70.7 70.7zM61.4 51.4v-15h15v15H61.4z" /> - </svg> - ); - } - - getKeyIcon() { - return ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35"> - <path d="M38.5 32.4c0 3.4-2.7 6.1-6.1 6.1 -3.4 0-6.1-2.7-6.1-6.1 0-3.4 2.8-6.1 6.1-6.1C35.8 26.3 38.5 29 38.5 32.4zM87.8 43.9c0 24.2-19.6 43.9-43.9 43.9S0 68.1 0 43.9C0 19.7 19.7 0 43.9 0S87.8 19.7 87.8 43.9zM66.8 60.3L50.2 43.7c-0.5-0.5-0.6-1.2-0.4-1.8 2.4-5.6 1.1-12.1-3.2-16.5 -5.9-5.8-15.4-5.8-21.2 0l0 0c-4.3 4.3-5.6 10.8-3.2 16.5 3.2 7.6 12 11.2 19.7 8 0.6-0.3 1.4-0.1 1.8 0.4l3.1 3.1h3.9c1.2 0 2.2 1 2.2 2.2v3.6h3.6c1.2 0 2.2 1 2.2 2.2v4l1.6 1.6h6.5V60.3z" /> - </svg> - ); - } - - getColIcon() { - return ( - <div className="col-icon"> - <FontAwesomeIcon icon={faObjectGroup} size="lg" /> - </div> - ); - } - - @action.bound - openFilter = () => { - this._filterOpen = !this._filterOpen; - SearchBox.Instance.closeResults(); - this.setupAccordion(); - } - - //if true, any keywords can be used. if false, all keywords are required. - @action.bound - handleWordQueryChange = () => { - this._basicWordStatus = !this._basicWordStatus; - } - - @action.bound - updateIcon(newArray: string[]) { this._icons = newArray; } - - @action.bound - getIcons(): string[] { return this._icons; } - - stopProp = (e: React.PointerEvent) => { - e.stopPropagation(); - this._pointerTime = e.timeStamp; - } - - @action.bound - public closeFilter() { - this._filterOpen = false; - } - - @action.bound - updateAnyKeywordStatus(newStat: boolean) { this._anyKeywordStatus = newStat; } - - @action.bound - updateAllKeywordStatus(newStat: boolean) { this._allKeywordStatus = newStat; } - - @action.bound - updateTitleStatus(newStat: boolean) { this._titleFieldStatus = newStat; } - - @action.bound - updateAuthorStatus(newStat: boolean) { this._authorFieldStatus = newStat; } - - @action.bound - updateDataStatus(newStat: boolean) { this._deletedDocsStatus = newStat; } - - @action.bound - updateCollectionStatus(newStat: boolean) { this._collectionStatus = newStat; } - - @action.bound - updateSelfCollectionStatus(newStat: boolean) { this._collectionSelfStatus = newStat; } - - @action.bound - updateParentCollectionStatus(newStat: boolean) { this._collectionParentStatus = newStat; } - - getAnyKeywordStatus() { return this._anyKeywordStatus; } - getAllKeywordStatus() { return this._allKeywordStatus; } - getCollectionStatus() { return this._collectionStatus; } - getSelfCollectionStatus() { return this._collectionSelfStatus; } - getParentCollectionStatus() { return this._collectionParentStatus; } - getTitleStatus() { return this._titleFieldStatus; } - getAuthorStatus() { return this._authorFieldStatus; } - getDataStatus() { return this._deletedDocsStatus; } - - getActiveFilters() { - return ( - <div className="active-filters"> - {!this._basicWordStatus ? <div className="active-icon container"> - <div className="active-icon icon">{this.getABCicon()}</div> - <div className="active-icon description">Required Words Applied</div> - </div> : undefined} - {!(this._icons.length === 9) ? <div className="active-icon container"> - <div className="active-icon icon">{this.getTypeIcon()}</div> - <div className="active-icon description">Type Filters Applied</div> - </div> : undefined} - {!(this._authorFieldStatus && this._dataFieldStatus && this._titleFieldStatus) ? - <div className="active-icon container"> - <div className="active-icon icon">{this.getKeyIcon()}</div> - <div className="active-icon description">Field Filters Applied</div> - </div> : undefined} - {this._collectionStatus ? <div className="active-icon container"> - <div className="active-icon icon">{this.getColIcon()}</div> - <div className="active-icon description">Collection Filters Active</div> - </div> : undefined} - </div> - ); - } - - // Useful queries: - // Delegates of a document: {!join from=id to=proto_i}id:{protoId} - // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId} //id of collections prototype - render() { - return ( - <div> - <div style={{ display: "flex", flexDirection: "row-reverse" }}> - <SearchBox /> - {/* {this.getActiveFilters()} */} - </div> - {this._filterOpen ? ( - <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex", background: "black" } : { display: "none" }}> - <div className="top-filter-header" style={{ display: "flex", width: "100%" }}> - <div id="header">Filter Search Results</div> - <div style={{ marginLeft: "auto" }}></div> - <div className="close-icon" onClick={this.closeFilter}> - <span className="line line-1"></span> - <span className="line line-2"></span></div> - </div> - <div className="filter-options"> - <div className="filter-div"> - <div className="filter-header"> - <div className='filter-title words'>Required words</div> - </div> - <div className="filter-panel" > - <button className="all-filter" onClick={this.handleWordQueryChange}>Include All Keywords</button> - </div> - </div> - <div className="filter-div"> - <div className="filter-header"> - <div className="filter-title icon">Filter by type of node</div> - </div> - <div className="filter-panel"><IconBar /></div> - </div> - <div className="filter-div"> - <div className="filter-header"> - <div className="filter-title field">Filter by Basic Keys</div> - </div> - <div className="filter-panel"><FieldFilters - titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._deletedDocsStatus} authorFieldStatus={this._authorFieldStatus} - updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} /> </div> - </div> - </div> - <div className="filter-buttons" style={{ display: "flex", justifyContent: "space-around" }}> - <button className="save-filter" >Save Filters</button> - <button className="reset-filter" onClick={this.resetFilters}>Reset Filters</button> - </div> - </div> - ) : - undefined} - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index bb62113a1..3f06ba7d3 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -17,10 +17,9 @@ .searchBox-bar { height: 32px; display: flex; - justify-content: flex-end; + justify-content: center; align-items: center; - padding-left: 2px; - + background-color: black; .searchBox-barChild { &.searchBox-collection { @@ -30,24 +29,29 @@ } &.searchBox-input { + margin:5px; + border-radius:20px; + border:black; display: block; width: 130px; -webkit-transition: width 0.4s; transition: width 0.4s; align-self: stretch; - + outline:none; } .searchBox-input:focus { width: 500px; - outline: 3px solid lightblue; + outline:none; } &.searchBox-filter { align-self: stretch; + button{ + transform:none; + } button:hover{ - transform:scale(1.0); - background:"#121721"; + transform:none; } } @@ -81,8 +85,6 @@ .no-result { width: 500px; background: $light-color-secondary; - border-color: $intermediate-color; - border-bottom-style: solid; padding: 10px; height: 50px; text-transform: uppercase; @@ -96,20 +98,20 @@ background: #121721; flex-direction: column; transform-origin: top; - transition: height 0.3s ease, display 0.6s ease; + transition: height 0.3s ease, display 0.6s ease, overflow 0.6s ease; height:0px; overflow:hidden; .filter-header { - display: flex; + //display: flex; position: relative; - flex-wrap:wrap; + //flex-wrap:wrap; right: 1px; color: grey; - flex-direction: row-reverse; + //flex-direction: row-reverse; transform-origin: top; - justify-content: space-evenly; + //justify-content: space-evenly; margin-bottom: 5px; overflow:hidden; transition:height 0.3s ease-out; @@ -130,9 +132,7 @@ color: grey; transform-origin: top; border-top: 0px; - //padding-top: 5px; - margin-left: 10px; - margin-right: 10px; + overflow:hidden; transition:height 0.3s ease-out; height:0px; @@ -144,30 +144,25 @@ color: grey; transform-origin: top; border-top: 0px; - //padding-top: 5px; - margin-left: 10px; - margin-right: 10px; overflow:hidden; transition:height 0.3s ease-out; height:0px; - .filter-keybar { - display: flex; - flex-wrap: wrap; - justify-content: space-evenly; - height: auto; - width: 100%; - flex-direction: row-reverse; - margin-top:5px; + + // .filter-keybar { + // display: flex; + // flex-wrap: wrap; + // justify-content: space-evenly; + // height: auto; + // width: 100%; + // flex-direction: row-reverse; + // margin-top:5px; - .filter-item { - position: relative; - border:1px solid grey; - border-radius: 16px; - - } - } - - + // .filter-item { + // position: relative; + // border:1px solid grey; + // border-radius: 16px; + // } + // } } } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 99fa6da21..1e44a379b 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,59 +1,66 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faTimes } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction, IReactionDisposer, reaction } from 'mobx'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from '@material-ui/core'; +import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as rp from 'request-promise'; -import { Doc } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { Utils } from '../../../Utils'; +import { returnFalse, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; -import "./SearchBox.scss"; -import { SearchItem } from './SearchItem'; -import { IconBar } from './IconBar'; -import { FieldView } from '../nodes/FieldView'; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DocumentView } from '../nodes/DocumentView'; import { SelectionManager } from '../../util/SelectionManager'; -import { listSpec } from '../../../fields/Schema'; - -library.add(faTimes); +import { Transform } from '../../util/Transform'; +import { CollectionView, CollectionViewType } from '../collections/CollectionView'; +import { ViewBoxBaseComponent } from "../DocComponent"; +import { DocumentView } from '../nodes/DocumentView'; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import "./SearchBox.scss"; -export interface SearchProps { - id: string; - searchQuery: string; - filterQquery?: string; - setSearchQuery: (q: string) => {}; - searchFileTypes: string[]; - setSearchFileTypes: (types: string[]) => {}; -} +export const searchSchema = createSchema({ + id: "string", + Document: Doc, + searchQuery: "string", +}); export enum Keys { TITLE = "title", AUTHOR = "author", - DATA = "data" + DATA = "data", + TEXT = "text" } +type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>; +const SearchBoxDocument = makeInterface(documentSchema, searchSchema); + +//React.Component<SearchProps> @observer -export class SearchBox extends React.Component<SearchProps> { +export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) { - private get _searchString() { return this.props.searchQuery; } - private set _searchString(value) { this.props.setSearchQuery(value); } + get _searchString() { return this.layoutDoc.searchQuery; } + @computed set _searchString(value) { this.layoutDoc.searchQuery = (value); } @observable private _resultsOpen: boolean = false; - @observable private _searchbarOpen: boolean = false; + @observable _searchbarOpen: boolean = false; @observable private _results: [Doc, string[], string[]][] = []; @observable private _openNoResults: boolean = false; @observable private _visibleElements: JSX.Element[] = []; + @observable private _visibleDocuments: Doc[] = []; private _resultsSet = new Map<Doc, number>(); private _resultsRef = React.createRef<HTMLDivElement>(); public inputRef = React.createRef<HTMLInputElement>(); private _isSearch: ("search" | "placeholder" | undefined)[] = []; + private _isSorted: ("sorted" | "placeholder" | undefined)[] = []; + private _numTotalResults = -1; private _endIndex = -1; @@ -63,55 +70,104 @@ export class SearchBox extends React.Component<SearchProps> { private _curRequest?: Promise<any> = undefined; public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } - + private new_buckets: { [characterName: string]: number } = {}; //if true, any keywords can be used. if false, all keywords are required. //this also serves as an indicator if the word status filter is applied @observable private _basicWordStatus: boolean = false; @observable private _nodeStatus: boolean = false; @observable private _keyStatus: boolean = false; + @observable private newAssign: boolean = true; constructor(props: any) { super(props); SearchBox.Instance = this; this.resultsScrolled = this.resultsScrolled.bind(this); + } + @observable setupButtons = false; + componentDidMount = () => { + if (this.setupButtons === false) { - componentDidMount = action(() => { + runInAction(() => this.setupButtons = true); + } if (this.inputRef.current) { this.inputRef.current.focus(); - this._searchbarOpen = true; + runInAction(() => { this._searchbarOpen = true; }); } - if (this.props.searchQuery) { // bcz: why was this here? } && this.props.filterQquery) { - this._searchString = this.props.searchQuery; - this.submitSearch(); + if (this.rootDoc.searchQuery && this.newAssign) { + const sq = this.rootDoc.searchQuery; + runInAction(() => { + + // this._deletedDocsStatus=this.props.filterQuery!.deletedDocsStatus; + // this._authorFieldStatus=this.props.filterQuery!.authorFieldStatus + // this._titleFieldStatus=this.props.filterQuery!.titleFieldStatus; + // this._basicWordStatus=this.props.filterQuery!.basicWordStatus; + // this._icons=this.props.filterQuery!.icons; + this.newAssign = false; + }); + runInAction(() => { + this.layoutDoc._searchString = StrCast(sq); + this.submitSearch(); + }); } - }); + } @action getViews = (doc: Doc) => SearchUtil.GetViewsOfDocument(doc) + + @observable newsearchstring: string = ""; @action.bound onChange(e: React.ChangeEvent<HTMLInputElement>) { - this._searchString = e.target.value; + this.layoutDoc._searchString = e.target.value; + this.newsearchstring = e.target.value; - this._openNoResults = false; - this._results = []; - this._resultsSet.clear(); - this._visibleElements = []; - this._numTotalResults = -1; - this._endIndex = -1; - this._curRequest = undefined; - this._maxSearchIndex = 0; + + if (e.target.value === "") { + this._results.forEach(result => { + Doc.UnBrushDoc(result[0]); + result[0].searchMatch = undefined; + }); + + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([]); + if (this.currentSelectedCollection !== undefined) { + this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); + this.currentSelectedCollection = undefined; + this.props.Document.selectedDoc = undefined; + + } + runInAction(() => { this.open = false; }); + this._openNoResults = false; + this._results = []; + this._resultsSet.clear(); + this._visibleElements = []; + this._numTotalResults = -1; + this._endIndex = -1; + this._curRequest = undefined; + this._maxSearchIndex = 0; + } } enter = (e: React.KeyboardEvent) => { if (e.key === "Enter") { + this.layoutDoc._searchString = this.newsearchstring; + + if (StrCast(this.layoutDoc._searchString) !== "" || !this.searchFullDB) { + runInAction(() => this.open = true); + } + else { + runInAction(() => this.open = false); + + } this.submitSearch(); } } + @observable open: boolean = false; + + public static async convertDataUri(imageUri: string, returnedFilename: string) { try { const posting = Utils.prepend("/uploadURI"); @@ -134,10 +190,11 @@ export class SearchBox extends React.Component<SearchProps> { //this also serves as an indicator if the word status filter is applied @observable private _filterOpen: boolean = false; //if icons = all icons, then no icon filter is applied - get _icons() { return this.props.searchFileTypes; } - set _icons(value) { - this.props.setSearchFileTypes(value); - } + // get _icons() { return this.props.searchFileTypes; } + // set _icons(value) { + // this.props.setSearchFileTypes(value); + // } + @observable _icons: string[] = this._allIcons; //if all of these are true, no key filter is applied @observable private _titleFieldStatus: boolean = true; @observable private _authorFieldStatus: boolean = true; @@ -162,10 +219,11 @@ export class SearchBox extends React.Component<SearchProps> { query = query.replace(/\s+/g, ' ').trim(); } - //if should be searched in a specific collection + // if should be searched in a specific collection if (this._collectionStatus) { query = this.addCollectionFilter(query); query = query.replace(/\s+/g, ' ').trim(); + } return query; } @@ -176,14 +234,14 @@ export class SearchBox extends React.Component<SearchProps> { @action filterDocsByType(docs: Doc[]) { - if (this._icons.length === this._allIcons.length) { - return docs; - } const finalDocs: Doc[] = []; + const blockedTypes: string[] = ["preselement", "docholder", "collection", "search", "searchitem", "script", "fonticonbox", "button", "label"]; docs.forEach(doc => { const layoutresult = Cast(doc.type, "string"); - if (layoutresult && this._icons.includes(layoutresult)) { - finalDocs.push(doc); + if (layoutresult && !blockedTypes.includes(layoutresult)) { + if (layoutresult && this._icons.includes(layoutresult)) { + finalDocs.push(doc); + } } }); return finalDocs; @@ -216,7 +274,6 @@ export class SearchBox extends React.Component<SearchProps> { getCurCollections(): Doc[] { const selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments(); const collections: Doc[] = []; - selectedDocs.forEach(async element => { const layout: string = StrCast(element.props.Document.layout); //checks if selected view (element) is a collection. if it is, adds to list to search through @@ -236,6 +293,85 @@ export class SearchBox extends React.Component<SearchProps> { } + currentSelectedCollection: DocumentView | undefined = undefined; + docsforfilter: Doc[] = []; + + searchCollection(query: string) { + const selectedCollection: DocumentView = SelectionManager.SelectedDocuments()[0]; + query = query.toLowerCase(); + if (selectedCollection !== undefined) { + this.currentSelectedCollection = selectedCollection; + if (this.filter === true) { + this.props.Document.selectedDoc = selectedCollection.props.Document; + } + let docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); + const found: [Doc, string[], string[]][] = []; + const docsforFilter: Doc[] = []; + let newarray: Doc[] = []; + + while (docs.length > 0) { + newarray = []; + docs.forEach((d) => { + if (d.data !== undefined) { + newarray.push(...DocListCast(d.data)); + } + const hlights: string[] = []; + const protos = Doc.GetAllPrototypes(d); + protos.forEach(proto => { + Object.keys(proto).forEach(key => { + if (StrCast(d[key]).toLowerCase().includes(query) && !hlights.includes(key)) { + hlights.push(key); + } + }); + }); + if (hlights.length > 0) { + found.push([d, hlights, []]); + docsforFilter.push(d); + } + }); + docs = newarray; + } + this._results = found; + this.docsforfilter = docsforFilter; + if (this.filter === true) { + selectedCollection.props.Document._searchDocs = new List<Doc>(docsforFilter); + docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); + while (docs.length > 0) { + newarray = []; + docs.forEach((d) => { + if (d.data !== undefined) { + d._searchDocs = new List<Doc>(docsforFilter); + const newdocs = DocListCast(d.data); + newdocs.forEach((newdoc) => { + newarray.push(newdoc); + }); + } + }); + docs = newarray; + } + } + this._numTotalResults = found.length; + } + else { + this.noresults = "No collection selected :("; + } + + } + + + documentKeys(doc: Doc) { + const keys: { [key: string]: boolean } = {}; + // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. + // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be + // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. + // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu + // is displayed (unlikely) it won't show up until something else changes. + //TODO Types + Doc.GetAllPrototypes(doc).map + (proto => Object.keys(proto).forEach(key => keys[key] = false)); + return Array.from(Object.keys(keys)); + } + applyBasicFieldFilters(query: string) { let finalQuery = ""; @@ -248,26 +384,24 @@ export class SearchBox extends React.Component<SearchProps> { if (this._deletedDocsStatus) { finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); } + if (this._deletedDocsStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TEXT); + } return finalQuery; } basicFieldFilters(query: string, type: string): string { - const oldWords = query.split(" "); let mod = ""; - - if (type === Keys.AUTHOR) { - mod = " author_t:"; - } if (type === Keys.DATA) { - //TODO - } if (type === Keys.TITLE) { - mod = " title_t:"; + switch (type) { + case Keys.AUTHOR: mod = " author_t:"; break; + case Keys.DATA: break; // TODO + case Keys.TITLE: mod = " _title_t:"; break; + case Keys.TEXT: mod = " text_t:"; break; } const newWords: string[] = []; - oldWords.forEach(word => { - const newWrd = mod + word; - newWords.push(newWrd); - }); + const oldWords = query.split(" "); + oldWords.forEach(word => newWords.push(mod + word)); query = newWords.join(" "); @@ -276,30 +410,64 @@ export class SearchBox extends React.Component<SearchProps> { get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } - @action - submitSearch = async () => { - const query = this._searchString; + submitSearch = async (reset?: boolean) => { + if (reset) { + this.layoutDoc._searchString = ""; + } + this.props.Document._docFilters = undefined; + this.noresults = ""; + + this.dataDoc[this.fieldKey] = new List<Doc>([]); + this.headercount = 0; + this.children = 0; + this.buckets = []; + this.new_buckets = {}; + const query = StrCast(this.layoutDoc._searchString); + Doc.SetSearchQuery(query); this.getFinalQuery(query); + this._results.forEach(result => { + Doc.UnBrushDoc(result[0]); + result[0].searchMatch = undefined; + }); this._results = []; this._resultsSet.clear(); this._isSearch = []; + this._isSorted = []; this._visibleElements = []; + this._visibleDocuments = []; + if (StrCast(this.props.Document.searchQuery)) { + if (this._timeout) { clearTimeout(this._timeout); this._timeout = undefined; } + this._timeout = setTimeout(() => { + console.log("Resubmitting search"); + }, 60000); + } + if (query !== "") { this._endIndex = 12; this._maxSearchIndex = 0; this._numTotalResults = -1; - await this.getResults(query); - + this.searchFullDB ? await this.getResults(query) : this.searchCollection(query); runInAction(() => { this._resultsOpen = true; this._searchbarOpen = true; this._openNoResults = true; this.resultsScrolled(); + }); } } + @observable searchFullDB = true; + + @observable _timeout: any = undefined; + + @observable firststring: string = ""; + @observable secondstring: string = ""; + + @observable bucketcount: number[] = []; + @observable buckets: Doc[] | undefined; + getAllResults = async (query: string) => { return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); } @@ -309,7 +477,7 @@ 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(" ")})`; // this line was causing issues for me, check solr logging -syip2 + // const typeExpr = !types ? "" : ` (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})`; // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello const query = [baseExpr, includeDeleted, includeIcons].join(" AND ").replace(/AND $/, ""); return query; @@ -317,21 +485,20 @@ export class SearchBox extends React.Component<SearchProps> { getDataStatus() { return this._deletedDocsStatus; } - private NumResults = 25; private lockPromise?: Promise<void>; getResults = async (query: string) => { + console.log("Get"); if (this.lockPromise) { await this.lockPromise; } this.lockPromise = new Promise(async res => { while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { - this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: this.NumResults, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => { + this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: this.NumResults, hl: true, "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { // happens at the beginning if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { this._numTotalResults = res.numFound; } - const highlighting = res.highlighting || {}; const highlightList = res.docs.map(doc => highlighting[doc[Id]]); const lines = new Map<string, string[]>(); @@ -340,19 +507,33 @@ export class SearchBox extends React.Component<SearchProps> { const highlights: typeof res.highlighting = {}; docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); const filteredDocs = this.filterDocsByType(docs); + runInAction(() => { - //this._results.push(...filteredDocs); - filteredDocs.forEach(doc => { + filteredDocs.forEach((doc, i) => { const index = this._resultsSet.get(doc); const highlight = highlights[doc[Id]]; const line = lines.get(doc[Id]) || []; const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []; - if (index === undefined) { - this._resultsSet.set(doc, this._results.length); - this._results.push([doc, hlights, line]); - } else { - this._results[index][1].push(...hlights); - this._results[index][2].push(...line); + doc ? console.log(Cast(doc.context, Doc)) : null; + if (this.findCommonElements(hlights)) { + } + else { + const layoutresult = Cast(doc.type, "string"); + if (layoutresult) { + if (this.new_buckets[layoutresult] === undefined) { + this.new_buckets[layoutresult] = 1; + } + else { + this.new_buckets[layoutresult] = this.new_buckets[layoutresult] + 1; + } + } + if (index === undefined) { + this._resultsSet.set(doc, this._results.length); + this._results.push([doc, hlights, line]); + } else { + this._results[index][1].push(...hlights); + this._results[index][2].push(...line); + } } }); }); @@ -363,15 +544,16 @@ export class SearchBox extends React.Component<SearchProps> { await this._curRequest; } + this.resultsScrolled(); res(); }); return this.lockPromise; } - + @observable noresults = ""; collectionRef = React.createRef<HTMLSpanElement>(); startDragCollection = async () => { - const res = await this.getAllResults(this.getFinalQuery(this._searchString)); + const res = await this.getAllResults(this.getFinalQuery(StrCast(this.layoutDoc._searchString))); const filtered = this.filterDocsByType(res.docs); const docs = filtered.map(doc => { const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); @@ -404,7 +586,7 @@ export class SearchBox extends React.Component<SearchProps> { y += 300; } } - return Docs.Create.QueryDocument({ _autoHeight: true, title: this._searchString, filterQuery: this.filterQuery, searchQuery: this._searchString }); + return Docs.Create.SearchDocument({ _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString), searchQuery: StrCast(this.layoutDoc._searchString) }); } @action.bound @@ -417,7 +599,7 @@ export class SearchBox extends React.Component<SearchProps> { @action.bound closeSearch = () => { - this.closeResults(); + //this.closeResults(); this._searchbarOpen = false; } @@ -427,23 +609,30 @@ export class SearchBox extends React.Component<SearchProps> { this._results = []; this._resultsSet.clear(); this._visibleElements = []; + this._visibleDocuments = []; this._numTotalResults = -1; this._endIndex = -1; this._curRequest = undefined; } + @observable children: number = 0; @action resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { if (!this._resultsRef.current) return; + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([]); + const scrollY = e ? e.currentTarget.scrollTop : this._resultsRef.current ? this._resultsRef.current.scrollTop : 0; const itemHght = 53; const startIndex = Math.floor(Math.max(0, scrollY / itemHght)); - const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this._resultsRef.current.getBoundingClientRect().height / itemHght))); - - this._endIndex = endIndex === -1 ? 12 : endIndex; - + //const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this._resultsRef.current.getBoundingClientRect().height / itemHght))); + const endIndex = 30; + //this._endIndex = endIndex === -1 ? 12 : endIndex; + this._endIndex = 30; + const headers = new Set<string>(["title", "author", "lastModified", "text"]); if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) { - this._visibleElements = [<div className="no-result">No Search Results</div>]; + if (this.noresults === "") { + this.noresults = "No search results :("; + } return; } @@ -456,16 +645,19 @@ export class SearchBox extends React.Component<SearchProps> { else if (this._visibleElements.length !== this._numTotalResults) { // undefined until a searchitem is put in there this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - // indicates if things are placeholders + this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults); + // indicates if things are placeholders this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - } + this._isSorted = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); + } for (let i = 0; i < this._numTotalResults; i++) { //if the index is out of the window then put a placeholder in //should ones that have already been found get set to placeholders? if (i < startIndex || i > endIndex) { if (this._isSearch[i] !== "placeholder") { this._isSearch[i] = "placeholder"; + this._isSorted[i] = "placeholder"; this._visibleElements[i] = <div className="searchBox-placeholder" key={`searchBox-placeholder-${i}`}>Loading...</div>; } } @@ -473,30 +665,61 @@ export class SearchBox extends React.Component<SearchProps> { if (this._isSearch[i] !== "search") { let result: [Doc, string[], string[]] | undefined = undefined; if (i >= this._results.length) { - this.getResults(this._searchString); + this.getResults(StrCast(this.layoutDoc._searchString)); if (i < this._results.length) result = this._results[i]; if (result) { const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; + const lines = new List<string>(result[2]); + result[0].lines = lines; + result[0].highlighting = highlights.join(", "); + highlights.forEach((item) => headers.add(item)); + this._visibleDocuments[i] = result[0]; this._isSearch[i] = "search"; + Doc.BrushDoc(result[0]); + result[0].searchMatch = true; + Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); + this.children++; } } else { result = this._results[i]; if (result) { const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; - this._isSearch[i] = "search"; + const lines = new List<string>(result[2]); + highlights.forEach((item) => headers.add(item)); + result[0].lines = lines; + result[0].highlighting = highlights.join(", "); + result[0].searchMatch = true; + if (i < this._visibleDocuments.length) { + this._visibleDocuments[i] = result[0]; + this._isSearch[i] = "search"; + Doc.BrushDoc(result[0]); + Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); + this.children++; + } } } } } } + const schemaheaders: SchemaHeaderField[] = []; + this.headerscale = headers.size; + headers.forEach((item) => schemaheaders.push(new SchemaHeaderField(item, "#f1efeb"))); + this.headercount = schemaheaders.length; + this.props.Document._schemaHeaders = new List<SchemaHeaderField>(schemaheaders); if (this._maxSearchIndex >= this._numTotalResults) { this._visibleElements.length = this._results.length; + this._visibleDocuments.length = this._results.length; this._isSearch.length = this._results.length; } } + @observable headercount: number = 0; + @observable headerscale: number = 0; + + findCommonElements(arr2: string[]) { + const arr1 = ["layout", "data"]; + return arr1.some(item => arr2.includes(item)); + } @computed get resFull() { return this._numTotalResults <= 8; } @@ -504,165 +727,197 @@ export class SearchBox extends React.Component<SearchProps> { @computed get resultHeight() { return this._numTotalResults * 70; } - //if true, any keywords can be used. if false, all keywords are required. - @action.bound - handleWordQueryChange = () => { - this._basicWordStatus = !this._basicWordStatus; - } - - @action.bound - handleNodeChange = () => { - this._nodeStatus = !this._nodeStatus; - if (this._nodeStatus) { - this.expandSection(`node${this.props.id}`); - } - else { - this.collapseSection(`node${this.props.id}`); - } - } - - @action.bound - handleKeyChange = () => { - this._keyStatus = !this._keyStatus; - if (this._keyStatus) { - this.expandSection(`key${this.props.id}`); - } - else { - this.collapseSection(`key${this.props.id}`); - } - } - - @action.bound - handleFilterChange = () => { - this._filterOpen = !this._filterOpen; - if (this._filterOpen) { - this.expandSection(`filterhead${this.props.id}`); - document.getElementById(`filterhead${this.props.id}`)!.style.padding = "5"; - } - else { - this.collapseSection(`filterhead${this.props.id}`); + addButtonDoc = (doc: Doc) => Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); + remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); + moveButtonDoc = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc); + @computed get searchItemTemplate() { return Cast(Doc.UserDoc().searchItemTemplate, Doc, null); } - } + getTransform = () => { + return this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight } - - @computed - get menuHeight() { - return document.getElementById("hi")?.clientHeight; + panelHeight = () => { + return this.props.PanelHeight(); } - - - collapseSection(thing: string) { - const id = this.props.id; - const element = document.getElementById(thing)!; - // get the height of the element's inner content, regardless of its actual size - const sectionHeight = element.scrollHeight; - - // temporarily disable all css transitions - const elementTransition = element.style.transition; - element.style.transition = ''; - - // on the next frame (as soon as the previous style change has taken effect), - // explicitly set the element's height to its current pixel height, so we - // aren't transitioning out of 'auto' - requestAnimationFrame(function () { - element.style.height = sectionHeight + 'px'; - element.style.transition = elementTransition; - - // on the next frame (as soon as the previous style change has taken effect), - // have the element transition to height: 0 - requestAnimationFrame(function () { - element.style.height = 0 + 'px'; - thing === `filterhead${id}` ? document.getElementById(`filterhead${id}`)!.style.padding = "0" : null; - }); - }); - - // mark the section as "currently collapsed" - element.setAttribute('data-collapsed', 'true'); - } - - expandSection(thing: string) { - const element = document.getElementById(thing)!; - // get the height of the element's inner content, regardless of its actual size - const sectionHeight = element.scrollHeight; - - // have the element transition to the height of its inner content - element.style.height = sectionHeight + 'px'; - - // when the next css transition finishes (which should be the one we just triggered) - element.addEventListener('transitionend', function handler(e) { - // remove this event listener so it only gets triggered once - element.removeEventListener('transitionend', handler); - - // remove "height" from the element's inline styles, so it can return to its initial value - element.style.height = "auto"; - //element.style.height = undefined; - }); - - // mark the section as "currently not collapsed" - element.setAttribute('data-collapsed', 'false'); - + selectElement = (doc: Doc) => { + //this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); } - autoset(thing: string) { - const element = document.getElementById(thing)!; - element.removeEventListener('transitionend', function (e) { }); - - // remove "height" from the element's inline styles, so it can return to its initial value - element.style.height = "auto"; - //element.style.height = undefined; + addDocument = (doc: Doc) => { + return null; } - @action.bound - updateTitleStatus() { this._titleFieldStatus = !this._titleFieldStatus; } - - @action.bound - updateAuthorStatus() { this._authorFieldStatus = !this._authorFieldStatus; } - - @action.bound - updateDataStatus() { this._deletedDocsStatus = !this._deletedDocsStatus; } + @observable filter = false; + //Make id layour document render() { - + this.props.Document._chromeStatus === "disabled"; + this.props.Document._searchDoc = true; + const cols = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length; + let length = 0; + cols > 5 ? length = 1076 : length = cols * 205 + 51; + let height = 0; + const rows = this.children; + rows > 8 ? height = 31 + 31 * 8 : height = 31 * rows + 31; return ( - <div className="searchBox-container"> + <div style={{ pointerEvents: "all" }} className="searchBox-container"> <div className="searchBox-bar"> - <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, () => this._searchString ? this.startDragCollection() : undefined)} ref={this.collectionRef} title="Drag Results as Collection"> - <FontAwesomeIcon icon="object-group" size="lg" /> - </span> - <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} - className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} - style={{ width: this._searchbarOpen ? "500px" : "100px" }} /> - <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={() => this.handleFilterChange()}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button> - </div> - - <div id={`filterhead${this.props.id}`} className="filter-form" > - <div id={`filterhead2${this.props.id}`} className="filter-header" style={this._filterOpen ? {} : {}}> - <button className="filter-item" style={this._basicWordStatus ? { background: "#aaaaa3", } : {}} onClick={this.handleWordQueryChange}>Keywords</button> - <button className="filter-item" style={this._keyStatus ? { background: "#aaaaa3" } : {}} onClick={this.handleKeyChange}>Keys</button> - <button className="filter-item" style={this._nodeStatus ? { background: "#aaaaa3" } : {}} onClick={this.handleNodeChange}>Nodes</button> - </div> - <div id={`node${this.props.id}`} className="filter-body" style={this._nodeStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> - <IconBar setIcons={(icons: string[]) => { - this._icons = icons; - }} /> - </div> - <div className="filter-key" id={`key${this.props.id}`} style={this._keyStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> - <div className="filter-keybar"> - <button className="filter-item" style={this._titleFieldStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateTitleStatus}>Title</button> - <button className="filter-item" style={this._deletedDocsStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateDataStatus}>Deleted Docs</button> - <button className="filter-item" style={this._authorFieldStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateAuthorStatus}>Author</button> + <div style={{ position: "absolute", left: 15 }}>{Doc.CurrentUserEmail}</div> + <div style={{ display: "flex", alignItems: "center" }}> + <FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" + style={{ color: "black", padding: 1, left: 35, position: "relative" }} /> + + <div style={{ cursor: "default", left: 250, position: "relative", }}> + <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} ><div> + <FontAwesomeIcon icon={"filter"} size="lg" + style={{ padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} + onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined); }} + onClick={action(() => { + const dofilter = (currentSelectedCollection: DocumentView) => { + let docs = DocListCast(currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(currentSelectedCollection.dataDoc)]); + while (docs.length > 0) { + const newarray: Doc[] = []; + docs.filter(d => d.data !== undefined).forEach((d) => { + d._searchDocs = new List<Doc>(this.docsforfilter); + newarray.push(...DocListCast(d.data)); + }); + docs = newarray; + } + }; + this.filter = !this.filter && !this.searchFullDB; + if (this.filter === true && this.currentSelectedCollection !== undefined) { + this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>(this.docsforfilter); + + dofilter(this.currentSelectedCollection); + + this.currentSelectedCollection.props.Document._docFilters = new List<string>(Cast(this.props.Document._docFilters, listSpec("string"), [])); + this.props.Document.selectedDoc = this.currentSelectedCollection.props.Document; + } + else if (this.filter === false && this.currentSelectedCollection !== undefined) { + + dofilter(this.currentSelectedCollection); + + this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); + this.currentSelectedCollection.props.Document._docFilters = undefined; + this.props.Document.selectedDoc = undefined; + } + } + )} /> + </div></Tooltip></div> + <input value={this.newsearchstring} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} + className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} + style={{ padding: 1, paddingLeft: 20, paddingRight: 20, color: "black", height: 20, width: 250 }} /> + <div style={{ + height: 25, + paddingLeft: "4px", + paddingRight: "4px", + border: "1px solid gray", + borderRadius: "0.3em", + borderBottom: this.open === false ? "1px solid" : "none", + }}> + <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> + <div style={{ display: "contents" }}> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this.searchFullDB} onChange={() => { + runInAction(() => { + this.searchFullDB = !this.searchFullDB; + this.dataDoc[this.fieldKey] = new List<Doc>([]); + if (this.currentSelectedCollection !== undefined) { + let newarray: Doc[] = []; + let docs: Doc[] = []; + docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); + while (docs.length > 0) { + newarray = []; + docs.forEach((d) => { + if (d.data !== undefined) { + d._searchDocs = new List<Doc>(); + const newdocs = DocListCast(d.data); + newdocs.forEach((newdoc) => { + newarray.push(newdoc); + }); + } + }); + docs = newarray; + } + this.currentSelectedCollection.props.Document._docFilters = undefined; + this.currentSelectedCollection.props.Document._searchDocs = undefined; + this.currentSelectedCollection = undefined; + } + this.submitSearch(); + }); + }} /> + Collection + </label> + </div> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input style={{ marginLeft: -16, marginTop: -1 }} type="radio" checked={this.searchFullDB} onChange={() => { + runInAction(() => { + this.searchFullDB = !this.searchFullDB; + this.dataDoc[this.fieldKey] = new List<Doc>([]); + this.filter = false; + if (this.currentSelectedCollection !== undefined) { + let newarray: Doc[] = []; + let docs: Doc[] = []; + docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); + while (docs.length > 0) { + newarray = []; + docs.forEach((d) => { + if (d.data !== undefined) { + d._searchDocs = new List<Doc>(); + const newdocs = DocListCast(d.data); + newdocs.forEach((newdoc) => { + newarray.push(newdoc); + }); + } + }); + docs = newarray; + } + this.currentSelectedCollection.props.Document._docFilters = undefined; + this.currentSelectedCollection.props.Document._searchDocs = undefined; + this.currentSelectedCollection = undefined; + } + this.submitSearch(); + }); + }} /> + DB + </label> + </div> + </div> + </form> </div> </div> + + </div> + <div style={{ zIndex: 20000, color: "black" }}> + {this._searchbarOpen === true ? + <div style={{ display: "flex", justifyContent: "center", }}> + {this.noresults === "" ? <div style={{ display: this.open === true ? "flex" : "none", overflow: "auto", }}> + <CollectionView {...this.props} + Document={this.props.Document} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelHeight={this.open === true ? () => height : () => 0} + PanelWidth={this.open === true ? () => length : () => 0} + overflow={cols > 5 || rows > 8 ? true : false} + focus={this.selectElement} + ScreenToLocalTransform={Transform.Identity} + /> + </div> : + <div style={{ display: "flex", justifyContent: "center" }}><div style={{ height: 200, top: 54, minWidth: 400, position: "absolute", backgroundColor: "rgb(241, 239, 235)", display: "flex", justifyContent: "center", alignItems: "center", border: "black 1px solid", }}> + <div>{this.noresults}</div> + </div></div>} + </div> : undefined} </div> + <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ display: this._resultsOpen ? "flex" : "none", height: this.resFull ? "auto" : this.resultHeight, overflow: "visibile" // this.resFull ? "auto" : "visible" }} ref={this._resultsRef}> - {this._visibleElements} </div> - </div> + </div > ); } -}
\ No newline at end of file +} diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss deleted file mode 100644 index 469f062b2..000000000 --- a/src/client/views/search/SearchItem.scss +++ /dev/null @@ -1,163 +0,0 @@ -@import "../globalCssVariables"; - -.searchItem-overview { - display: flex; - flex-direction: reverse; - justify-content: flex-end; - z-index: 0; -} - -.searchBox-placeholder, -.searchItem-overview .searchItem { - width: 100%; - background: $light-color-secondary; - border-color: $intermediate-color; - border-bottom-style: solid; - padding: 10px; - min-height: 50px; - max-height: 150px; - height: auto; - z-index: 0; - display: flex; - overflow: visible; - - .searchItem-body { - display: flex; - flex-direction: row; - width: 100%; - - .searchItem-title-container { - width: 100%; - overflow: hidden; - - .searchItem-title { - text-transform: uppercase; - text-align: left; - width: 100%; - font-weight: bold; - } - } - - .searchItem-info { - display: flex; - justify-content: flex-end; - - .icon-icons { - width: 50px - } - - .icon-live { - width: 175px; - height: 0px; - } - - .icon-icons { - height:auto; - } - .icon-icons, - .icon-live { - margin: auto; - overflow: visible; - - .searchItem-type { - display: inline-block; - width: 100%; - position: absolute; - justify-content: center; - align-items: center; - position: relative; - margin-right: 5px; - } - - .pdfBox-cont { - overflow: hidden; - - img { - width: 100% !important; - height: auto !important; - } - } - - .searchItem-type:hover+.searchItem-label { - opacity: 1; - } - - .searchItem-label { - font-size: 10; - position: relative; - right: 0px; - text-transform: capitalize; - opacity: 0; - -webkit-transition: opacity 0.2s ease-in-out; - -moz-transition: opacity 0.2s ease-in-out; - -o-transition: opacity 0.2s ease-in-out; - transition: opacity 0.2s ease-in-out; - } - } - - .icon-live:hover { - .pdfBox-cont { - img { - width: 100% !important; - } - } - } - } - - .searchItem-info:hover { - width: 60%; - } - } -} - -.searchItem:hover~.searchBox-instances, -.searchBox-instances:hover, -.searchBox-instances:active { - opacity: 1; - background: $lighter-alt-accent; -} - -.searchItem:hover { - transition: all 0.2s; - background: $lighter-alt-accent; -} - -.searchItem-highlighting { - overflow: hidden; - text-overflow: ellipsis; - white-space: pre; -} - -.searchBox-instances { - opacity: 1; - width:40px; - height:40px; - background: gray; - transition: all 0.2s ease; - color: black; - overflow: hidden; - right:-100; - display:inline-block; -} - - -.searchItem-overview:hover { - z-index: 1; -} - -.searchBox-placeholder { - min-height: 50px; - margin-left: 150px; - width: calc(100% - 150px); - text-transform: uppercase; - text-align: left; - font-weight: bold; -} - -.collection { - display: flex; -} - -.collection-item { - width: 35px; -}
\ No newline at end of file diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx deleted file mode 100644 index 2436bf418..000000000 --- a/src/client/views/search/SearchItem.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faChartBar, faFile, faFilePdf, faFilm, faFingerprint, faGlobeAsia, faImage, faLink, faMusic, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero, returnEmptyString, returnEmptyFilter } from "../../../Utils"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DocumentManager } from "../../util/DocumentManager"; -import { DragManager, SetupDrag } from "../../util/DragManager"; -import { SearchUtil } from "../../util/SearchUtil"; -import { Transform } from "../../util/Transform"; -import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionViewType } from "../collections/CollectionView"; -import { ParentDocSelector } from "../collections/ParentDocumentSelector"; -import { ContextMenu } from "../ContextMenu"; -import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { SearchBox } from "./SearchBox"; -import "./SearchItem.scss"; -import "./SelectorContextMenu.scss"; - -export interface SearchItemProps { - doc: Doc; - query: string; - highlighting: string[]; - lines: string[]; -} - -library.add(faCaretUp); -library.add(faObjectGroup); -library.add(faStickyNote); -library.add(faFile); -library.add(faFilePdf); -library.add(faFilm); -library.add(faMusic); -library.add(faLink); -library.add(faChartBar); -library.add(faGlobeAsia, faFingerprint); - -@observer -export class SelectorContextMenu extends React.Component<SearchItemProps> { - @observable private _docs: { col: Doc, target: Doc }[] = []; - @observable private _otherDocs: { col: Doc, target: Doc }[] = []; - - constructor(props: SearchItemProps) { - super(props); - this.fetchDocuments(); - } - - async fetchDocuments() { - const aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc); - const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.doc[Id]}"` }); - const map: Map<Doc, Doc> = new Map; - const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); - allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); - docs.forEach(doc => map.delete(doc)); - runInAction(() => { - this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.doc })); - this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target })); - - }); - } - - getOnClick({ col, target }: { col: Doc, target: Doc }) { - return () => { - col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; - if (col._viewType === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target._width) / 2; - const newPanY = NumCast(target.y) + NumCast(target._height) / 2; - col._panX = newPanX; - col._panY = newPanY; - } - CollectionDockingView.AddRightSplit(col); - }; - } - render() { - return ( - <div className="parents"> - <p className="contexts">Contexts:</p> - {[...this._docs, ...this._otherDocs].map(doc => { - const item = React.createRef<HTMLDivElement>(); - return <div className="collection" key={doc.col[Id] + doc.target[Id]} ref={item}> - <div className="collection-item" onPointerDown={ - SetupDrag(item, () => doc.col, undefined, undefined, () => SearchBox.Instance.closeSearch())}> - <FontAwesomeIcon icon={faStickyNote} /> - </div> - <a onClick={this.getOnClick(doc)}>{doc.col.title}</a> - </div>; - })} - </div> - ); - } -} - -export interface LinkMenuProps { - doc1: Doc; - doc2: Doc; -} - -@observer -export class LinkContextMenu extends React.Component<LinkMenuProps> { - - highlightDoc = (doc: Doc) => () => Doc.BrushDoc(doc); - - unHighlightDoc = (doc: Doc) => () => Doc.UnBrushDoc(doc); - - getOnClick = (col: Doc) => () => CollectionDockingView.AddRightSplit(col); - - render() { - return ( - <div className="parents"> - <p className="contexts">Anchors:</p> - <div className="collection"><a onMouseEnter={this.highlightDoc(this.props.doc1)} onMouseLeave={this.unHighlightDoc(this.props.doc1)} onClick={this.getOnClick(this.props.doc1)}>Doc 1: {this.props.doc2.title}</a></div> - <div><a onMouseEnter={this.highlightDoc(this.props.doc2)} onMouseLeave={this.unHighlightDoc(this.props.doc2)} onClick={this.getOnClick(this.props.doc2)}>Doc 2: {this.props.doc1.title}</a></div> - </div> - ); - } - -} - -@observer -export class SearchItem extends React.Component<SearchItemProps> { - - @observable _selected: boolean = false; - - onClick = () => { - // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like - DocumentManager.Instance.jumpToDocument(this.props.doc, false); - } - @observable _useIcons = true; - @observable _displayDim = 50; - - componentDidMount() { - Doc.SetSearchQuery(this.props.query); - this.props.doc.searchMatch = true; - } - componentWillUnmount() { - this.props.doc.searchMatch = undefined; - } - - //@computed - @action - public DocumentIcon() { - const layoutresult = StrCast(this.props.doc.type); - if (!this._useIcons) { - const returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); - const returnYDimension = () => this._displayDim; - const docview = <div - onPointerDown={action(() => { - this._useIcons = !this._useIcons; - this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); - })} - onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))} > - <ContentFittingDocumentView - Document={this.props.doc} - LibraryPath={emptyPath} - rootSelected={returnFalse} - fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1} - addDocument={returnFalse} - removeDocument={returnFalse} - addDocTab={returnFalse} - pinToPres={returnFalse} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - ScreenToLocalTransform={Transform.Identity} - renderDepth={1} - PanelWidth={returnXDimension} - PanelHeight={returnYDimension} - NativeWidth={returnZero} - NativeHeight={returnZero} - focus={emptyFunction} - moveDocument={returnFalse} - parentActive={returnFalse} - whenActiveChanged={returnFalse} - bringToFront={returnFalse} - ContentScaling={returnOne} - /> - </div>; - return docview; - } - const button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf : - layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage : - layoutresult.indexOf(DocumentType.RTF) !== -1 ? faStickyNote : - layoutresult.indexOf(DocumentType.VID) !== -1 ? faFilm : - layoutresult.indexOf(DocumentType.COL) !== -1 ? faObjectGroup : - layoutresult.indexOf(DocumentType.AUDIO) !== -1 ? faMusic : - layoutresult.indexOf(DocumentType.LINK) !== -1 ? faLink : - layoutresult.indexOf(DocumentType.WEB) !== -1 ? faGlobeAsia : - faCaretUp; - return <div onClick={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} > - <FontAwesomeIcon icon={button} size="2x" /> - </div>; - } - - collectionRef = React.createRef<HTMLDivElement>(); - - @action - pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); } - - nextHighlight = (e: React.PointerEvent) => { - e.preventDefault(); - e.button === 0 && SearchBox.Instance.openSearch(e); - this.props.doc.searchMatch = false; - setTimeout(() => this.props.doc.searchMatch = true, 0); - } - highlightDoc = (e: React.PointerEvent) => { - if (this.props.doc.type === DocumentType.LINK) { - if (this.props.doc.anchor1 && this.props.doc.anchor2) { - - const doc1 = Cast(this.props.doc.anchor1, Doc, null); - const doc2 = Cast(this.props.doc.anchor2, Doc, null); - Doc.BrushDoc(doc1); - Doc.BrushDoc(doc2); - } - } else { - Doc.BrushDoc(this.props.doc); - } - e.stopPropagation(); - } - - unHighlightDoc = (e: React.PointerEvent) => { - if (this.props.doc.type === DocumentType.LINK) { - if (this.props.doc.anchor1 && this.props.doc.anchor2) { - - const doc1 = Cast(this.props.doc.anchor1, Doc, null); - const doc2 = Cast(this.props.doc.anchor2, Doc, null); - Doc.UnBrushDoc(doc1); - Doc.UnBrushDoc(doc2); - } - } else { - Doc.UnBrushDoc(this.props.doc); - } - } - - onContextMenu = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ - description: "Copy ID", event: () => { - Utils.CopyText(this.props.doc[Id]); - }, - icon: "fingerprint" - }); - ContextMenu.Instance.displayMenu(e.clientX, e.clientY); - } - - _downX = 0; - _downY = 0; - _target: any; - onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { - this._downX = e.clientX; - this._downY = e.clientY; - e.stopPropagation(); - this._target = e.currentTarget; - document.removeEventListener("pointermove", this.onPointerMoved); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMoved); - document.addEventListener("pointerup", this.onPointerUp); - } - onPointerMoved = (e: PointerEvent) => { - if (Math.abs(e.clientX - this._downX) > Utils.DRAG_THRESHOLD || - Math.abs(e.clientY - this._downY) > Utils.DRAG_THRESHOLD) { - document.removeEventListener("pointermove", this.onPointerMoved); - document.removeEventListener("pointerup", this.onPointerUp); - const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc; - DragManager.StartDocumentDrag([this._target], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); - } - } - onPointerUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onPointerMoved); - document.removeEventListener("pointerup", this.onPointerUp); - } - - @computed - get contextButton() { - return <ParentDocSelector Document={this.props.doc} addDocTab={(doc, where) => CollectionDockingView.AddRightSplit(doc)} />; - } - - render() { - const doc1 = Cast(this.props.doc.anchor1, Doc); - const doc2 = Cast(this.props.doc.anchor2, Doc); - return <div className="searchItem-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}> - <div className="searchItem" onPointerDown={this.nextHighlight} onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc}> - <div className="searchItem-body" onClick={this.onClick}> - <div className="searchItem-title-container"> - <div className="searchItem-title">{StrCast(this.props.doc.title)}</div> - <div className="searchItem-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}</div> - {this.props.lines.filter((m, i) => i).map((l, i) => <div id={i.toString()} className="searchItem-highlighting">`${l}`</div>)} - </div> - </div> - <div className="searchItem-info" style={{ width: this._useIcons ? "30px" : "100%" }}> - <div className={`icon-${this._useIcons ? "icons" : "live"}`}> - <div className="searchItem-type" title="Click to Preview" onPointerDown={this.onPointerDown}>{this.DocumentIcon()}</div> - <div className="searchItem-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div> - </div> - </div> - <div className="searchItem-context" title="Drag as document"> - {(doc1 instanceof Doc && doc2 instanceof Doc) && this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={doc1} doc2={doc2} /> : - this.contextButton} - </div> - </div> - </div>; - } -}
\ No newline at end of file diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index f4505d475..6bfe91378 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -508,9 +508,9 @@ export namespace Doc { alias.aliasOf = doc; alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`); alias.author = Doc.CurrentUserEmail; + alias[AclSym] = doc[AclSym]; - if (!doc.aliases) doc.aliases = new List<Doc>([alias]); - else Doc.AddDocToList(doc, "aliases", alias); + Doc.AddDocToList(doc[DataSym], "aliases", alias); return alias; } @@ -627,7 +627,7 @@ export namespace Doc { const zip = new JSZip(); - zip.file("doc.json", docString); + zip.file(doc.title + ".json", docString); // // Generate a directory within the Zip file structure // var img = zip.folder("images"); @@ -639,7 +639,7 @@ export namespace Doc { zip.generateAsync({ type: "blob" }) .then((content: any) => { // Force down of the Zip file - saveAs(content, "download.zip"); + saveAs(content, doc.title + ".zip"); // glr: Possibly change the name of the document to match the title? }); } // @@ -1266,4 +1266,4 @@ Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: bo return docs.length ? new List(docs) : prevValue; }); Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: "match" | "check" | "x" | undefined) { Doc.setDocFilter(container, key, value, modifiers); }); -Scripting.addGlobal(function setDocFilterRange(container: Doc, key: string, range: number[]) { Doc.setDocFilterRange(container, key, range); });
\ No newline at end of file +Scripting.addGlobal(function setDocFilterRange(container: Doc, key: string, range: number[]) { Doc.setDocFilterRange(container, key, range); }); diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 7cfd74cc4..dbe51b24a 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -2,6 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, custom, createSimpleSchema, list, object, map } from "serializr"; import { ObjectField } from "./ObjectField"; import { Copy, ToScriptString, ToString, Update } from "./FieldSymbols"; +import { Scripting } from "../client/util/Scripting"; export enum InkTool { None = "none", @@ -44,9 +45,11 @@ export class InkField extends ObjectField { } [ToScriptString]() { - return "invalid"; + return "new InkField([" + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}} `) + "])"; } [ToString]() { return "InkField"; } } + +Scripting.addGlobal("InkField", InkField);
\ No newline at end of file diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index f55483a5b..4cc3a7cc7 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -53,6 +53,9 @@ async function deserializeScript(script: ScriptField) { if (script.script.originalScript === 'convertToButtons(dragData)') { return (script as any).script = (ScriptField.ConvertToButtons ?? (ScriptField.ConvertToButtons = ComputedField.MakeFunction('convertToButtons(dragData)', { dragData: "DocumentDragData" })))?.script; } + if (script.script.originalScript === 'self.userDoc.noviceMode') { + return (script as any).script = (ScriptField.NoviceMode ?? (ScriptField.NoviceMode = ComputedField.MakeFunction('self.userDoc.noviceMode')))?.script; + } const captures: ProxyField<Doc> = (script as any).captures; if (captures) { const doc = (await captures.value())!; @@ -85,6 +88,7 @@ export class ScriptField extends ObjectField { public static OpenOnRight: Opt<ScriptField>; public static DeiconifyView: Opt<ScriptField>; public static ConvertToButtons: Opt<ScriptField>; + public static NoviceMode: Opt<ScriptField>; constructor(script: CompiledScript, setterscript?: CompiledScript) { super(); diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 8cf8f47b7..ada13226e 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -19,6 +19,10 @@ export const documentSchema = createSchema({ 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 + isLabel: "boolean", // whether the document is a label or not (video / audio) + audioStart: "number", // the time frame where the audio should begin playing + audioEnd: "number", // the time frame where the audio should stop playing + markers: listSpec(Doc), // list of markers for audio / video x: "number", // x coordinate when in a freeform view y: "number", // y coordinate when in a freeform view z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview diff --git a/src/fields/util.ts b/src/fields/util.ts index 957b2c8cd..4c71572db 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -10,7 +10,6 @@ import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; import { returnZero } from "../Utils"; -import { addSyntheticLeadingComment } from "typescript"; function _readOnlySetter(): never { @@ -221,7 +220,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc // maps over the aliases of the document if (target.aliases) { DocListCast(target.aliases).map(alias => { - distributeAcls(key, acl, alias); + distributeAcls(key, acl, alias, inheritingFromCollection); }); } diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index 753c31fcf..7251e07a1 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -176,7 +176,7 @@ export namespace SolrManager { "audio": ["_t", "url"], "web": ["_t", "url"], "date": ["_d", value => new Date(value.date).toISOString()], - "proxy": ["_i", "fieldId"], + // "proxy": ["_i", "fieldId"], "list": ["_l", list => { const results = []; for (const value of list.fields) { diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index bd242db5e..bd8fe97eb 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -14,7 +14,6 @@ import { normalize } from "path"; import RouteSubscriber from "../RouteSubscriber"; const imageDataUri = require('image-data-uri'); import { isWebUri } from "valid-url"; -import { launch } from "puppeteer"; import { Opt } from "../../fields/Doc"; import { SolrManager } from "./SearchManager"; @@ -280,25 +279,26 @@ function delay(ms: number) { * * On failure, returns undefined. */ -async function captureYoutubeScreenshot(targetUrl: string): Promise<Opt<Buffer>> { - const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); - const page = await browser.newPage(); - await page.setViewport({ width: 1920, height: 1080 }); +async function captureYoutubeScreenshot(targetUrl: string){ + // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); + // const page = await browser.newPage(); + // // await page.setViewport({ width: 1920, height: 1080 }); - await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any }); + // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any }); - const videoPlayer = await page.$('.html5-video-player'); - videoPlayer && await page.focus("video"); - await delay(7000); - const ad = await page.$('.ytp-ad-skip-button-text'); - await ad?.click(); - await videoPlayer?.click(); - await delay(1000); - // hide youtube player controls. - await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none'); + // const videoPlayer = await page.$('.html5-video-player'); + // videoPlayer && await page.focus("video"); + // await delay(7000); + // const ad = await page.$('.ytp-ad-skip-button-text'); + // await ad?.click(); + // await videoPlayer?.click(); + // await delay(1000); + // // hide youtube player controls. + // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none'); - const buffer = await videoPlayer?.screenshot({ encoding: "binary" }); - await browser.close(); + // const buffer = await videoPlayer?.screenshot({ encoding: "binary" }); + // await browser.close(); - return buffer; + // return buffer; + return null; }
\ No newline at end of file diff --git a/src/server/websocket.ts b/src/server/websocket.ts index d81450b32..63cfa41f0 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -16,6 +16,7 @@ import { DocumentsCollection } from "./IDatabase"; import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from "./Message"; import { Search } from "./Search"; import { resolvedPorts } from './server_Initialization'; +import { Opt } from "../fields/Doc"; export namespace WebSocket { @@ -31,7 +32,7 @@ export namespace WebSocket { if (socketPort) { resolvedPorts.socket = Number(socketPort); } - let socketEndpoint: Server; + let socketEndpoint: Opt<Server>; await new Promise<void>(resolve => socketEndpoint = createServer(SSL.Credentials, app).listen(resolvedPorts.socket, resolve)); io = sio(socketEndpoint!, SSL.Credentials as any); } else { @@ -229,7 +230,8 @@ export namespace WebSocket { "script": ["_t", value => value.script.originalScript], "RichTextField": ["_t", value => value.Text], "date": ["_d", value => new Date(value.date).toISOString()], - "proxy": ["_i", "fieldId"], + // "proxy": ["_i", "fieldId"], + // "proxy": ["", "fieldId"], "list": ["_l", list => { const results = []; for (const value of list.fields) { @@ -243,25 +245,27 @@ export namespace WebSocket { }; function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { + if (val === null || val === undefined) { return; } const type = val.__type || typeof val; + let suffix = suffixMap[type]; if (!suffix) { return; } - if (Array.isArray(suffix)) { const accessor = suffix[1]; if (typeof accessor === "function") { val = accessor(val); } else { val = val[accessor]; + } suffix = suffix[0]; - } + } return { suffix, value: val }; } @@ -283,7 +287,7 @@ export namespace WebSocket { dynfield = true; const val = docfield[key]; key = key.substring(7); - Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null }); + Object.values(suffixMap).forEach(suf => { update[key + getSuffix(suf)] = { set: null }; }); const term = ToSearchTerm(val); if (term !== undefined) { const { suffix, value } = term; diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index 452882e09..068ac2159 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -6,8 +6,13 @@ declare module 'cors'; declare module 'webrtc-adapter'; declare module 'bezier-curve'; -declare module 'fit-curve' +declare module 'fit-curve'; +declare module 'react-audio-waveform'; +declare module 'reveal'; +declare module 'react-reveal'; +declare module 'react-reveal/makeCarousel'; +declare module 'react-resizable-rotatable-draggable'; declare module '@react-pdf/renderer' { import * as React from 'react'; diff --git a/tsconfig.json b/tsconfig.json index 803245ca6..b06cec79f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "es2015" ], "typeRoots": [ - "node_modules/@types" + "node_modules/@types", + "./src/typings" ], "types": [ "youtube" |